预计阅读本页时间:-
第9章
下载文件和图片
在之前的章节中,我们学习了从网页中爬取信息的方法,这只是爬虫最典型的一种应用,除此之外,下载文件也是实际应用中很常见的一种需求,例如使用爬虫爬取网站中的图片、视频、WORD文档、PDF文件、压缩包等。本章来学习在Scrapy中如何下载文件和图片。
9.1 FilesPipeline和ImagesPipeline
Scrapy框架内部提供了两个Item Pipeline,专门用于下载文件和图片:
● FilesPipeline
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
● ImagesPipeline
我们可以将这两个Item Pipeline看作特殊的下载器,用户使用时只需要通过item的一个特殊字段将要下载文件或图片的url传递给它们,它们会自动将文件或图片下载到本地,并将下载结果信息存入item的另一个特殊字段,以便用户在导出文件中查阅。下面详细介绍如何使用它们。
9.1.1 FilesPipeline使用说明
通过一个简单的例子讲解FilesPipeline的使用,在如下页面中可以下载多本PDF格式的小说:
<html> <body> ... <a href='/book/sg.pdf'>下载《三国演义》</a> <a href='/book/shz.pdf'>下载《水浒传》</a> <a href='/book/hlm.pdf'>下载《红楼梦》</a> <a href='/book/xyj.pdf'>下载《西游记》</a> ... </body> </html>
使用FilesPipeline下载页面中所有PDF文件,可按以下步骤进行:
步骤 01 在配置文件settings.py中启用FilesPipeline,通常将其置于其他Item Pipeline之前:
ITEM_PIPELINES = {'scrapy.pipelines.files.FilesPipeline': 1}
步骤 02 在配置文件settings.py中,使用FILES_STORE指定文件下载目录,如:
FILES_STORE = '/home/liushuo/Download/scrapy'
步骤 03 在Spider解析一个包含文件下载链接的页面时,将所有需要下载文件的url地址收集到一个列表,赋给item的file_urls字段(item['file_urls'])。FilesPipeline在处理每一项item时,会读取item['file_urls'],对其中每一个url进行下载,Spider示例代码如下:
class DownloadBookSpider(scrapy.Spider): ... def parse(response): item = {} # 下载列表 item['file_urls'] = [] for url in response.xpath('//a/@href').extract(): download_url = response.urljoin(url) # 将url 填入下载列表 item['file_urls'].append(download_url) yield item
当FilesPipeline下载完item['file_urls']中的所有文件后,会将各文件的下载结果信息收集到另一个列表,赋给item的files字段(item['files'])。下载结果信息包括以下内容:
● Path文件下载到本地的路径(相对于FILES_STORE的相对路径)。
● Checksum文件的校验和。
● url文件的url地址。
9.1.2 ImagesPipeline使用说明
图片也是文件,所以下载图片本质上也是下载文件,ImagesPipeline是FilesPipeline的子类,使用上和FilesPipeline大同小异,只是在所使用的item字段和配置选项上略有差别,如表9-1所示。
表9-1 ImagesPipeline和FilesPipeline

ImagesPipeline在FilesPipleline的基础上针对图片增加了一些特有的功能:
● 为图片生成缩略图
开启该功能,只需在配置文件settings.py中设置IMAGES_THUMBS,它是一个字典,每一项的值是缩略图的尺寸,代码如下:
IMAGES_THUMBS = { 'small': (50, 50), 'big': (270, 270), }
开启该功能后,下载一张图片时,本地会出现3张图片(1张原图片,2张缩略图),路径如下:
[IMAGES_STORE]/full/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg [IMAGES_STORE]/thumbs/small/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg [IMAGES_STORE]/thumbs/big/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg
● 过滤掉尺寸过小的图片
开启该功能,需在配置文件settings.py中设置IMAGES_MIN_WIDTH和IMAGES_MIN_HEIGHT,它们分别指定图片最小的宽和高,代码如下:
IMAGES_MIN_WIDTH = 110 IMAGES_MIN_HEIGHT = 110
开启该功能后,如果下载了一张105×200的图片,该图片就会被抛弃掉,因为它的宽度不符合标准。
9.2 项目实战:爬取matplotlib例子源码文件
下面我们来完成一个使用FilesPipeline下载文件的实战项目。matplotlib是一个非常著名的Python绘图库,广泛应用于科学计算和数据分析等领域。在matplotlib网站上提供了许多应用例子代码,在浏览器中访问http://matplotlib.org/examples/index.html,可看到图9-1所示的例子列表页面。
其中有几百个例子,被分成多个类别,单击第一个例子,进入其页面,如图9-2所示。
用户可以在每个例子页面中阅读源码,也可以点击页面中的source code按钮下载源码文件。如果我们想把所有例子的源码文件都下载到本地,可以编写一个爬虫程序完成这个任务。
9.2.1 项目需求
下载http://matplotlib.org网站中所有例子的源码文件到本地。

图9-1

图9-2
9.2.2 页面分析
先来看如何在例子列表页面http://matplotlib.org/examples/index.html中获取所有例子页面的链接。使用scrapy shell命令下载页面,然后调用view函数在浏览器中查看页面,如图9-3所示。
$ scrapy shell http://matplotlib.org/examples/index.html ... >>> view(response)

图9-3
观察发现,所有例子页面的链接都在<div class="toctree-wrapper compound">下的每一个<li class="toctree-l2">中,例如:
<a class="reference internal" href="animation/animate_decay.html">animate_decay</a>
使用LinkExtractor提取所有例子页面的链接,代码如下:
>>> from scrapy.linkextractors import LinkExtractor >>> le = LinkExtractor(restrict_css='div.toctree-wrapper.compound li.toctree-l2') >>> links = le.extract_links(response) >>> [link.url for link in links] ['http://matplotlib.org/examples/animation/animate_decay.html', 'http://matplotlib.org/examples/animation/basic_example.html', 'http://matplotlib.org/examples/animation/basic_example_writer.html', 'http://matplotlib.org/examples/animation/bayes_update.html', 'http://matplotlib.org/examples/animation/double_pendulum_animated.html', 'http://matplotlib.org/examples/animation/dynamic_image.html', 'http://matplotlib.org/examples/animation/dynamic_image2.html', 'http://matplotlib.org/examples/animation/histogram.html', 'http://matplotlib.org/examples/animation/moviewriter.html', 'http://matplotlib.org/examples/animation/rain.html', 'http://matplotlib.org/examples/animation/random_data.html', 'http://matplotlib.org/examples/animation/simple_3danim.html', 'http://matplotlib.org/examples/animation/simple_anim.html', 'http://matplotlib.org/examples/animation/strip_chart_demo.html', 'http://matplotlib.org/examples/animation/subplots.html', 'http://matplotlib.org/examples/animation/unchained.html', 'http://matplotlib.org/examples/api/agg_oo.html', 'http://matplotlib.org/examples/api/barchart_demo.html', 'http://matplotlib.org/examples/api/bbox_intersect.html', ... 'http://matplotlib.org/examples/user_interfaces/svg_tooltip.html', 'http://matplotlib.org/examples/user_interfaces/toolmanager.html', 'http://matplotlib.org/examples/user_interfaces/wxcursor_demo.html', 'http://matplotlib.org/examples/widgets/buttons.html', 'http://matplotlib.org/examples/widgets/check_buttons.html', 'http://matplotlib.org/examples/widgets/cursor.html', 'http://matplotlib.org/examples/widgets/lasso_selector_demo.html', 'http://matplotlib.org/examples/widgets/menu.html', 'http://matplotlib.org/examples/widgets/multicursor.html', 'http://matplotlib.org/examples/widgets/radio_buttons.html', 'http://matplotlib.org/examples/widgets/rectangle_selector.html', 'http://matplotlib.org/examples/widgets/slider_demo.html', 'http://matplotlib.org/examples/widgets/span_selector.html'] >>> len(links) 507
例子列表页面分析完毕,总共找到了507个例子。
接下来分析例子页面。调用fetch函数下载第一个例子页面,并调用view函数在浏览器中查看页面,如图9-4所示。
>>> fetch('http://matplotlib.org/examples/animation/animate_decay.html') ... >>> view(response)

图9-4
在一个例子页面中,例子源码文件的下载地址可在<a class="reference external">中找到:
>>> href = response.css('a.reference.external::attr(href)').extract_first() >>> href 'animate_decay.py' >>> response.urljoin(href) 'http://matplotlib.org/examples/animation/animate_decay.py'
到此,页面分析的工作完成了。
9.2.3 编码实现
接下来,我们按以下4步完成该项目:
(1)创建Scrapy项目,并使用scrapy genspider命令创建Spider。
(2)在配置文件中启用FilesPipeline,并指定文件下载目录。
(3)实现ExampleItem(可选)。
(4)实现ExamplesSpider。
步骤 01 首先创建Scrapy项目,取名为matplotlib_examples,再使用scrapy genspider命令创建Spider:
$ scrapy startproject matplotlib_examples $ cd matplotlib_examples $ scrapy genspider examples matplotlib.org
步骤 02 在配置文件settings.py中启用FilesPipeline,并指定文件下载目录,代码如下:
ITEM_PIPELINES = { 'scrapy.pipelines.files.FilesPipeline': 1, } FILES_STORE = 'examples_src'
步骤 03 实现ExampleItem,需定义file_urls和files两个字段,在items.py中完成如下代码:
class ExampleItem(scrapy.Item): file_urls = scrapy.Field() files = scrapy.Field()
步骤 04 实现ExamplesSpider。首先设置起始爬取点:
import scrapy class ExamplesSpider(scrapy.Spider): name = "examples" allowed_domains = ["matplotlib.org"] start_urls = ['http://matplotlib.org/examples/index.html'] def parse(self, response): pass
parse方法是例子列表页面的解析函数,在该方法中提取每个例子页面的链接,用其构造Request对象并提交,提取链接的细节已在页面分析时讨论过,实现parse方法的代码如下:
import scrapy from scrapy.linkextractors import LinkExtractor class ExamplesSpider(scrapy.Spider): name = "examples" allowed_domains = ["matplotlib.org"] start_urls = ['http://matplotlib.org/examples/index.html'] def parse(self, response): le = LinkExtractor(restrict_css='div.toctree-wrapper.compound', deny='/index.html$') print(len(le.extract_links(response))) for link in le.extract_links(response): yield scrapy.Request(link.url, callback=self.parse_example) def parse_example(self, response): pass
上面代码中,我们将例子页面的解析函数设置为parse_example方法,下面来实现这个方法。例子页面中包含了例子源码文件的下载链接,在parse_example方法中获取源码文件的url,将其放入一个列表,赋给ExampleItem的file_urls字段。实现parse_example方法的代码如下:
import scrapy from scrapy.linkextractors import LinkExtractor from ..items import ExampleItem class ExamplesSpider(scrapy.Spider): name = "examples" allowed_domains = ["matplotlib.org"] start_urls = ['http://matplotlib.org/examples/index.html'] def parse(self, response): le = LinkExtractor(restrict_css='div.toctree-wrapper.compound', deny='/index.html$') print(len(le.extract_links(response))) for link in le.extract_links(response): yield scrapy.Request(link.url, callback=self.parse_example) def parse_example(self, response): href = response.css('a.reference.external::attr(href)').extract_first() url = response.urljoin(href) example = ExampleItem() example['file_urls'] = [url] return example
编码完成后,运行爬虫,并观察结果:
$ scrapy crawl examples -o examples.json ... $ ls examples.json examples_src matplotlib_examples scrapy.cfg
运行结束后,在文件examples.json中可以查看到文件下载结果信息:
$ cat examples.json [ {"file_urls": ["http://matplotlib.org/mpl_examples/axes_grid/demo_floating_axes.py"], "files": [{"url": "http://matplotlib.org/mpl_examples/axes_grid/demo_floating_axes.py", "checksum": "502d1cd62086fb1d4de033cef2e495c0", "path": "full/d9b551310a6668ccf43871e896f2fe6e0228567d.py"}]}, {"file_urls": ["http://matplotlib.org/mpl_examples/axes_grid/demo_curvelinear_grid.py"], "files": [{"url": "http://matplotlib.org/mpl_examples/axes_grid/demo_curvelinear_grid.py", "checksum": "5cb91103f11079b40400afc0c1f4a508", "path": "full/366386c23c5b715c49801efc7f8d55d2c74252e2.py"}]}, {"file_urls": ["http://matplotlib.org/mpl_examples/axes_grid/make_room_for_ylabel_using_axesgrid.py"], "files": [{"url": "http://matplotlib.org/mpl_examples/axes_grid/make_room_for_ylabel_using_axesgrid.py", "checksum": "dcf561f97ab0905521c1957cacd2da00", "path": "full/919cbbe6d725237e3b6051f544f6109e7189b4fe.py"}]}, ...省略部分内容... {"file_urls": ["http://matplotlib.org/mpl_examples/api/custom_projection_example.py"], "files": [{"url": "http://matplotlib.org/mpl_examples/api/custom_projection_example.py", "checksum": "bde485f9d5ceb4b4cc969ef692df5eee", "path": "full/d56af342d7130ddd9dbf55c00664eae9a432bf70.py"}]}, {"file_urls": ["http://matplotlib.org/examples/animation/dynamic_image2.py"], "files": [{"url": "http://matplotlib.org/examples/animation/dynamic_image2.py", "checksum": "98b6a6021ba841ef4a2cd36c243c516d", "path": "full/fe635002562e8685583c1b35a8e11e8cde0a6321.py"}]}, {"file_urls": ["http://matplotlib.org/examples/animation/basic_example.py"], "files": [{"url": "http://matplotlib.org/examples/animation/basic_example.py", "checksum": "1d4afc0910f6abc519e6ecd32c66896a", "path": "full/083c113c1dac96bbc74adfc5b08cad68ec9c16db.py"}]}
再来查看文件下载目录exmaples_src:

如上所示,507个源码文件被下载到了examples_src/full目录下,并且每个文件的名字都是一串长度相等的奇怪数字,这些数字是下载文件url的sha1散列值。例如,某文件url为:
http://matplotlib.org/mpl_examples/axes_grid/demo_floating_axes.py
该url的sha1散列值为:
d9b551310a6668ccf43871e896f2fe6e0228567d
那么该文件的存储路径为:
# [FILES_STORE]/full/[SHA1_HASH_VALUE].py examples_src/full/d9b551310a6668ccf43871e896f2fe6e0228567d.py
这种命名方式可以防止重名的文件相互覆盖,但这样的文件名太不直观了,无法从文件名了解文件内容,我们期望把这些例子文件按照类别下载到不同目录下,为完成这个任务,可以写一个单独的脚本,依据examples.json文件中的信息将文件重命名,也可以修改FilesPipeline为文件命名的规则,这里采用后一种方式。
阅读FilesPipeline的源码发现,原来是其中的file_path方法决定了文件的命名,相关代码如下:
class FilesPipeline(MediaPipeline): ... def file_path(self, request, response=None, info=None): ... # check if called from file_key with url as first argument if not isinstance(request, Request): _warn() url = request else: url = request.url # detect if file_key() method has been overridden if not hasattr(self.file_key, '_base'): _warn() return self.file_key(url) ## end of deprecation warning block media_guid = hashlib.sha1(to_bytes(url)).hexdigest() media_ext = os.path.splitext(url)[1] return 'full/%s%s' % (media_guid, media_ext) ...
现在,我们实现一个FilesPipeline的子类,覆写file_path方法来实现所期望的文件命名规则,这些源码文件url的最后两部分是类别和文件名,例如:
http://matplotlib.org/mpl_examples/(axes_grid/demo_floating_axes.py)
可用以上括号中的部分作为文件路径,在pipelines.py实现MyFilesPipeline,代码如下:
from scrapy.pipelines.files import FilesPipeline from urllib.parse import urlparse from os.path import basename, dirname, join class MyFilesPipeline(FilesPipeline): def file_path(self, request, response=None, info=None): path = urlparse(request.url).path return join(basename(dirname(path)), basename(path))
修改配置文件,使用MyFilesPipeline替代FilesPipeline:
ITEM_PIPELINES = { #'scrapy.pipelines.files.FilesPipeline': 1, 'matplotlib_examples.pipelines.MyFilesPipeline': 1, }
删除之前下载的所有文件,重新运行爬虫后,再来查看examples_src目录:

从上述结果看出,507个文件按类别被下载到26个目录下,这正是我们所期望的。
到此,文件下载的项目完成了。
9.3 项目实战:下载360图片
我们再来完成一个使用ImagesPipeline下载图片的实战项目。360图片是一个知名的图片搜索网站,在浏览器中打开http://image.so.com,页面如图9-5所示。

图9-5
其中,艺术分类下有大量字画图片,我们可以编写爬虫爬取这些图片。
9.3.1 项目需求
下载360图片网站中艺术分类下的所有图片到本地。
9.3.2 页面分析
在图9-5所示的页面中向下滚动鼠标滚轮,便会有更多图片加载出来,图片加载是由JavaScript脚本完成的,在图9-6中可以看到jQuery发送的请求,其响应结果是一个json串。

图9-6
复制图中jQuery发送请求的url,使用scrapy shell进行访问,查看响应结果的内容(json):
$ scrapy shell 'http://image.so.com/zj?ch=art&sn=30&listtype=new&temp=1' ... >>> import json >>> res = json.loads(response.body.decode('utf8')) >>> res {'count': 30, 'end': False, 'lastid': 60, 'list': [{'cover_height': 942, 'cover_imgurl': 'http://www.sinaimg.cn/dy/slidenews/26_img/2011_27/17290_50560_803601.jpg', 'cover_width': 950, 'dsptime': '', 'group_title': '李正天作品欣赏', 'grpseq': 1, 'id': 'e4e6dbc8c5deaf2799d396569904227f', 'imageid': '5332cbd95b1098f0e9325a16ce022a74', 'index': 31, 'label': '', 'qhimg_height': 236, 'qhimg_thumb_url': 'http://p0.so.qhimgs1.com/sdr/238__/t01ab50e7f19a03afa0.jpg', 'qhimg_url': 'http://p0.so.qhimgs1.com/t01ab50e7f19a03afa0.jpg', 'qhimg_width': 238, 'tag': '新浪艺术名家人物库', 'total_count': 70}, {'cover_height': 1798, 'cover_imgurl': 'http://www.sinaimg.cn/dy/slidenews/26_img/2011_15/18496_33310_603704.jpg', 'cover_width': 950, 'dsptime': '', 'group_title': '崔自默作品欣赏', 'grpseq': 1, 'id': 'f08148a113c6c2e6104a77798d285d88', 'imageid': 'c6662a238bb6faf9b22a335db6707fff', 'index': 32, 'label': '', 'qhimg_height': 450, 'qhimg_thumb_url': 'http://p0.so.qhmsg.com/sdr/238__/t01b187fc2ce65e29b5.jpg', 'qhimg_url': 'http://p0.so.qhmsg.com/t01b187fc2ce65e29b5.jpg', 'qhimg_width': 238, 'tag': '新浪艺术名家人物库', 'total_count': 53}, {'cover_height': 950, 'cover_imgurl': 'http://www.sinaimg.cn/dy/slidenews/26_img/2011_32/18496_59078_243228.jpg', 'cover_width': 950, 'dsptime': '', 'group_title': '徐宁作品欣赏', 'grpseq': 1, 'id': 'ed8686ac7f10dfb52d68baca348a08be', 'imageid': '51c2b804fb6d402486737c29c5301a84', 'index': 33, 'label': '', 'qhimg_height': 238, 'qhimg_thumb_url': 'http://p2.so.qhmsg.com/sdr/238__/t017f259639fd6c8287.jpg', 'qhimg_url': 'http://p2.so.qhmsg.com/t017f259639fd6c8287.jpg', 'qhimg_width': 238, 'tag': '新浪艺术名家人物库', 'total_count': 161}, ...省略中间部分... {'cover_height': 377, 'cover_imgurl': 'http://www.sinaimg.cn/dy/slidenews/26_img/2011_03/16418_23122_876413.jpg', 'cover_width': 950, 'dsptime': '', 'group_title': '王国斌作品欣赏', 'grpseq': 1, 'id': '8e173e45250d90d2dc7316777e2be59b', 'imageid': 'c7d7e74dc18685f5c100d235522d5e4b', 'index': 59, 'label': '', 'qhimg_height': 94, 'qhimg_thumb_url': 'http://p2.so.qhimgs1.com/sdr/238__/t014d248b01108afebe.jpg', 'qhimg_url': 'http://p2.so.qhimgs1.com/t014d248b01108afebe.jpg', 'qhimg_width': 238, 'tag': '新浪艺术名家人物库', 'total_count': 13}, {'cover_height': 1215, 'cover_imgurl': 'http://www.sinaimg.cn/dy/slidenews/26_img/2011_09/17732_26034_613620.jpg', 'cover_width': 900, 'dsptime': '', 'group_title': '王习三作品欣赏', 'grpseq': 1, 'id': '989031bb929f667f8eb920cfa21e32fa', 'imageid': 'f57b9882a93265edcd85e59d3fbb8a4c', 'index': 60, 'label': '王习三', 'qhimg_height': 321, 'qhimg_thumb_url': 'http://p4.so.qhmsg.com/sdr/238__/t015381735d7c0aa2a9.jpg', 'qhimg_url': 'http://p4.so.qhmsg.com/t015381735d7c0aa2a9.jpg', 'qhimg_width': 238, 'tag': '新浪艺术名家人物库', 'total_count': 31}]}
如上所示,响应结果(json)中的list字段是一个图片信息列表,count字段是列表中图片信息的数量,每一项图片信息的qhimg_url字段是图片下载地址。
连续滚动鼠标滚轮加载图片,捕获更多jQuery发送的请求:
第1次加载:http://image.so.com/zj?ch=art&sn=30&listtype=new&temp=1
第2次加载:http://image.so.com/zj?ch=art&sn=60&listtype=new&temp=1
第3次加载:http://image.so.com/zj?ch=art&sn=90&listtype=new&temp=1
……
经过观察,可以总结出这些url的规律:
● ch参数 分类标签。
● sn参数 从第几张图片开始加载,即结果列表中第一张图片在服务器端的序号。
我们可以通过这个API每次获取固定数量的图片信息,从中提取每一张图片的url(下载地址),直到响应结果中的count字段为0(意味着没有更多图片了)。
到此,页面分析工作完成了。
9.3.3 编码实现
接下来,我们按以下3步完成该项目:
(1)创建Scrapy项目,并使用scrapy genspider命令创建Spider。
(2)在配置文件中启用ImagesPipeline,并指定图片下载目录。
(3)实现ImagesSpider。
步骤 01 首先创建Scrapy项目,取名为so_image,再使用scrapy genspider命令创建Spider。
$ scrapy startproject so_image $ cd so_image $ scrapy genspider images image.so.com
步骤 02 在配置文件settings.py中启用ImagesPipeline,并指定图片下载目录,代码如下:
ITEM_PIPELINES = { 'scrapy.pipelines.images.ImagesPipeline': 1, } IMAGES_STORE = 'download_images'
步骤 03 实现IamgesSpider,代码如下:
# -*- coding: utf-8 -*- import scrapy from scrapy import Request import json class ImagesSpider(scrapy.Spider): BASE_URL = 'http://image.so.com/zj?ch=art&sn=%s&listtype=new&temp=1' start_index = 0 # 限制最大下载数量,防止磁盘用量过大 MAX_DOWNLOAD_NUM = 1000 name = "images" start_urls = [BASE_URL % 0] def parse(self, response): # 使用json 模块解析响应结果 infos = json.loads(response.body.decode('utf-8')) # 提取所有图片下载url 到一个列表, 赋给item的'image_urls'字段 yield {'image_urls': [info['qhimg_url'] for info in infos['list']]} # 如count 字段大于0,并且下载数量不足MAX_DOWNLOAD_NUM,继续获取下一页 图片信息 self.start_index += infos['count'] if infos['count'] > 0 and self.start_index < self.MAX_DOWNLOAD_NUM: yield Request(self.BASE_URL % self.start_index)
编码完成后,运行爬虫:
$ scrapy crawl images
运行结束后,查看图片下载目录download_images,如图9-7所示,我们成功爬取到了607张艺术图片。

图9-7
到此,图片下载的项目完成了。
9.4 本章小结
本章我们学习了在Scrapy中下载文件和图片的方法,先简单介绍了Scrapy提供的FilesPipeline和ImagesPipeline,然后通过两个实战项目演示了FilesPipeline和ImagesPipeline的使用。