Python爬虫之Scrapy框架爬取XXXFM音频文件

本文介绍使用Scrapy爬虫框架爬取某FM音频文件。

框架介绍

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

官方文档

安装Scrapy

使用pip安装

pip install Scrapy

创建项目

打开系统终端,cd到项目安装文件夹,输入命令:

scrapy startproject FmFiles

其中*FmFiles**为项目名称。

创建Scrapy项目后,用Pycharm打开项目,在编译器下输入代码,也可以直接在终端下输入代码。

本文介绍使用Pycharm编写项目代码。

项目设置

Scrapy设定(settings)提供了定制Scrapy组件的方法。您可以控制包括核心(core),插件(extension),pipelinespider组件。

设定为代码提供了提取以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文件夹。

Python爬虫之Scrapy框架爬取XXXFM音频文件

导入模块

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文件开始爬取。

Python爬虫之Scrapy框架爬取XXXFM音频文件