本文介绍使用Scrapy爬虫框架爬取某FM音频文件。
框架介绍
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
安装Scrapy
使用pip安装
pip install Scrapy
创建项目
打开系统终端,cd到项目安装文件夹,输入命令:
scrapy startproject FmFiles
其中*FmFiles**为项目名称。
创建Scrapy项目后,用Pycharm打开项目,在编译器下输入代码,也可以直接在终端下输入代码。
本文介绍使用Pycharm编写项目代码。
项目设置
Scrapy设定(settings)提供了定制Scrapy组件的方法。您可以控制包括核心(core),插件(extension),pipeline及spider组件。
设定为代码提供了提取以key-value映射的配置值的的全局命名空间(namespace)。 设定可以通过下面介绍的多种机制进行设置。
设定(settings)同时也是选择当前激活的Scrapy项目的方法(如果您有多个的话)。
进入FmFiles子文件夹下名为settings的Python文件,本项目下需要覆盖以下几个默认设置:
- 不遵守robots.txt文件,该文件定义了爬虫相关协议,包括不允许爬虫的代理、IP等信息。
ROBOTSTXT_OBEY = False
- 自定义pipeline文件所在路径
ITEM_PIPELINES = {
'FmFiles.pipelines.FmfilesPipeline': 300,
}
- 设置爬取的文件保存路径
FILES_STORE = '文件保存根路径'
- 开启媒体重定向。默认情况下,媒体文件pipeline会忽略重定向,即向媒体文件URL请求的HTTP重定向将意味着媒体下载被认为是失败的。
MEDIA_ALLOW_REDIRECTS = True
- 调整文件保留延迟天数。媒体文件pipeline会避免下载最近下载过的文件,默认延迟90天。
FILES_EXPIRES = 120
定义Item
Item是保存爬取到的数据的容器;其使用方法和Python字典类似, 并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。
下载的图像和文件默认保存在指定根目录下的full子文件夹下,且文件名默认为URI(资源的唯一标识符),本项目需修改每个文件的文件名和所在文件夹名,且爬虫关闭后需删除该full子文件夹。
进入settingsPython文件,代码如下:
import scrapy
class FmfilesItem(scrapy.Item):
# define the fields for your item here like:
# 专辑名称
file_album = scrapy.Field()
# 专辑中文件名
file_name = scrapy.Field()
# 专辑中文件url
file_url = scrapy.Field()
pass
编写爬虫文件
首先创建爬虫文件,进入终端输入命令:
scrapy genspider fmfiles ximalaya.com
其中genspider为创建爬虫的scrapy命令,fmfiles是建立的爬虫名称(爬虫的唯一识别字符串),ximalaya.com的爬取网站的限制域名。
爬虫文件建立后,进入spiders文件夹下的fmfiles文件夹。
导入模块
import scrapy
import os
import json
from scrapy.selector import Selector
from FmFiles.items import FmfilesItem
from FmFiles.settings import FILES_STORE
定义爬虫类相关属性:
name = 'fmfiles'
allowed_domains = ['']
# PC端起始url
pc_url = 'http://www.ximalaya.com/'
# 移动端起始url
mobile_url = 'http://m.ximalaya.com/'
allowed_domains是爬虫限制域名,所有进入爬取队列的url必须符合这个域名,否则不爬取,该项目不限制。
该项目通过输入手机端或电脑端音频专辑所在url爬取该专辑下所有音频文件,故需要PC端和移动端的起始url以识别。
pc_url为PC端音频专辑的起始url。
mobile_url为移动端音频专辑的起始url。
获取文件外输入的专辑url列表
该项目从主执行文件中输入PC端或移动端的多个专辑url,建立main Python文件,输入执行scrapy爬虫命令的代码:
from scrapy.cmdline import execute
if __name__ == '__main__':
# 在此添加专辑url列表或在命令行执行scrapy crawl fmfiles -a urls={多个专辑url,以逗号隔开}
album_urls = ['http://www.ximalaya.com/1000202/album/2667276/']
urls = ','.join(album_urls)
execute_str = 'scrapy crawl fmfiles -a urls=' + urls
execute(execute_str.split())
其中execute()执行来自系统终端命令行语句,参数为单个命令的列表。
urls为爬虫所需外部参数的键值,与爬虫初始化器中属性名一致。
在爬虫文件获取输入参数值:
def __init__(self, urls=None):
super(FmfilesSpider, self).__init__()
self.urls = urls.split(',')
解析输入参数
判断urls参数是来自PC端还是移动端的音频专辑url,在请求爬取url方法中输入代码:
def start_requests(self):
for url in self.urls:
if url.startswith(self.mobile_url):
yield self.request_album_url(url)
elif url.startswith(self.pc_url):
yield scrapy.Request(url=url, callback=self.parse_pc)
若为移动端专辑url,直接请求该url获取各个资源url;若为PC端专辑url,还需在请求html中解析出相应的移动端专辑url。原因是移动端url的反爬虫措施较PC端少,更易爬取。
其中request_album_url(url)函数解析专辑url并交给scrapy请求该url,定义如下:
def request_album_url(self, album_url=''):
if len(album_url) == 0:
return None
album_url = album_url.strip().strip('/')
album_id = album_url.split('/')[-1]
return scrapy.Request(album_url,
meta={'aid': album_id},
callback=self.parse,
dont_filter=True)
其中parse_pc函数为请求PC端专辑url后的回调函数,在下面讲解。
解析PC端专辑url
使用XPath语法解析html结构中的元素和内容,scrapy官方关于XPath语法部分
def parse_pc(self, response):
# 从PC端专辑html中解析出移动端专辑url
mobile_url = response.xpath('//head/link[contains(@rel, "alternate")]/@href').extract_first()
yield self.request_album_url(mobile_url)
解析移动端专辑主页url
def parse(self, response):
# 专辑名称
album_name = response.xpath('//article/div/div/h2/text()').extract_first().strip()
# self.album_name = album_name
filepath = FILES_STORE + album_name
if not os.path.exists(filepath):
os.mkdir(filepath)
meta = response.meta
yield self.json_formrequest(aname=album_name,
aid=meta['aid'])
其中json_formrequest函数提交资源文件json表单,表单参数为json所在url、专辑id、资源页数(一页20个文件)。
def json_formrequest(self, aname='', aid=0, page=1):
moreurl = '/album/more_tracks'
# album_id = album_url.split('/')[-1]
page = str(page)
formrequest = scrapy.FormRequest(url='http://m.ximalaya.com' + moreurl,
formdata={'url': moreurl,
'aid': str(aid),
'page': str(page)},
meta={'aname': str(aname),
'aid': str(aid),
'page': str(page)},
method='GET',
callback=self.parse_json,
dont_filter=True)
return formrequest
解析资源json文件并保存到item
def parse_json(self, response):
jsondata = json.loads(response.text)
if jsondata['res'] is False:
return None
next_page = jsondata['next_page']
selector = Selector(text=jsondata['html'])
file_nodes = selector.xpath('//li[@class="item-block"]')
if file_nodes is None:
return None
meta = response.meta
for file_node in file_nodes:
file_name = file_node.xpath('a[1]/h4/text()').extract_first().strip()
file_url = file_node.xpath('a[2]/@sound_url').extract_first().strip()
item = FmfilesItem()
item['file_album'] = meta['aname']
item['file_name'] = file_name + '.' + file_url.split('.')[-1]
item['file_url'] = file_url
yield item
if int(next_page) == 0:
return None
if int(next_page) == (int(meta['page']) + 1):
yield self.json_formrequest(aname=meta['aname'],
aid=meta['aid'],
page=next_page)
编写管道文件
导入模块
import scrapy
import os
from scrapy.pipelines.files import FilesPipeline
from FmFiles.settings import FILES_STORE
from scrapy.exceptions import DropItem
定义管道类属性
动态获取资源文件默认父目录“full”,并保存为属性:
__file_dir = None
该属性在爬虫关闭后会执行删除文件夹操作。
编写管道执行函数
通过上述爬虫文件中获取到资源文件所在url后,交给scrapy的文件管道类下载,重写scrapy媒体文件下载函数:
def get_media_requests(self, item, info):
file_url = item['file_url']
yield scrapy.Request(file_url)
重写该函数后scrapy会自动处理url并下载。
资源下载完成后修改文件名:
def item_completed(self, results, item, info):
file_paths = [x['path'] for ok, x in results if ok]
if not self.__file_dir:
self.__file_dir = file_paths[0].split('/')[0]
if not file_paths:
raise DropItem("Item contains no files")
os.rename(FILES_STORE + file_paths[0],
FILES_STORE + item['file_album'] + '/' + item['file_name'])
return item
爬虫结束后删除默认父文件夹:
def close_spider(self, spider):
if self.__file_dir is None:
return None
ori_filepath = FILES_STORE + self.__file_dir
if os.path.exists(ori_filepath):
os.rmdir(ori_filepath)
执行爬虫
代码书写完毕后,执行main文件开始爬取。