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文件夹。

Snip20171012_4.png

导入模块

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

Snip20171012_3.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容