(原创)使用Scrapy下载文件时的几个tips

2020-03-23
最近自己又玩了玩爬虫,由于目标网站跳转很多而且是要下载文件,所以选择了Scrapy框架。
对于Scrapy框架久仰大名,知道它内部封装实现了异步并发,但前两年专职做爬虫的时候用的是NodeJs自行开发的分布式微服务任务处理框架,所以也没有认真研究过Scrapy,这次正好借机学习研究并应用一番。

首先当然是要看文档,这次为了节省时间,我看的是中文版文档。在这还是要小小地吐槽一下,翻译质量确实不高,专业术语压根错的离谱,但毕竟人生苦短,有经验的人应该都不会轻易被误导。

文档虽然翻译不精准,但步骤和案例也算比较详细,所以前面步骤都如Scrapy 教程一般顺利:

  • scrapy startproject MyProject创建项目
  • /spiders/目录下修改编写自己的爬虫逻辑,通过yieldcallback来实现异步和回调
  • 使用scrapy shell "target_url"来单页调试
  • 使用response.xpath()来解析页面,跟BeautifuSoap4一样(Python和Node都有这个库)

然后在详细说一下处理文件下载的部分,简单情况下如下载和处理文件和图像文档一般:

  • 1.通过配置setting.py来启用文件管道和设置文件下载存储地址
ITEM_PIPELINES = {'scrapy.pipelines.files.FilesPipeline': 1}
FILES_STORE = '/path/to/valid/dir'
  • 2.在items.py进行声明,这样拿到目标文件的url之后直接赋给item.file_urls后再调用yield item就可以了不用管了
import scrapy
class xxxxdiscrapyItem(scrapy.Item):
    file_urls=scrapy.Field()
    file_paths=scrapy.Field()

是不是很简单?
But, demo is demo,我的文件下载全部失败了,加了一堆print之后看到问题在于“下载文件的时候没有在headers里设置referer,导致被识别为爬虫,因而403”。

这个其实比较奇怪,因为观察日志可以看到前面页面跳转的时候,一直都是带着referer的,不知道为啥到了下载文件的时候就没有,啥都不带了,有知道的小伙伴请帮忙释疑

一番搜索后找到的最为合意的帖子是这篇scrapy爬取福利图片解决防盗链的问题,当然这是官方文档扩展媒体管道的基础上进行操作的。

根据这篇博客进行代码修改,把获取到target_url的页面page_url设置它的referer后依然有问题,一开始是各种fail,填完坑后看日志记录没啥异常,但是开始统计每分钟下载多少多少个item了,但文件依然一个都没有下载下来,又是一番对比才解决:

  • 1. file_path的重写有坑,需要给参数默认值

怎么发现这个坑的,我已经忘了,但我知道填掉它是因为我找到了scarpy中这个函数的源代码https://github.com/scrapy/scrapy/blob/master/scrapy/pipelines/files.py

    def file_path(self, request, response=None, info=None):
        media_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
        media_ext = os.path.splitext(request.url)[1]
        # Handles empty and wild extensions by trying to guess the
        # mime type then extension or default to empty string otherwise
        if media_ext not in mimetypes.types_map:
            media_ext = ''
            media_type = mimetypes.guess_type(request.url)[0]
            if media_type:
                media_ext = mimetypes.guess_extension(media_type)
        return 'full/%s%s' % (media_guid, media_ext)

或许这就是那些老鸟所说的Python多态的暗坑之一吧
但这一点在教程:扩展媒体管道中丝毫都没有体现出来,我甚至去查过英文原版的,一样没有提及,所以这锅翻译不背。

  • 2. ITEM_PIPELINES的设置需要更改
    因为自定义的MyFilesPipeline会重载原生的FilesPipeline
ITEM_PIPELINES = {
   'xxxxScrapy.pipelines.MyFilesPipeline': 1,
}

MyFilesPipeline的代码如下:

import os
from urllib.parse import urlparse

from scrapy.pipelines.files import FilesPipeline
from scrapy import Request
from scrapy.exceptions import DropItem

class MyFilesPipeline(FilesPipeline):

    # 示例中没有提及这里需要给response和info设置默认值
    def file_path(self, request, response=None, info=None):
        return os.path.basename(urlparse(request.url).path)


    def get_media_requests(self, item, info):
        for file_url in item['file_urls']:
            yield Request(file_url, headers={'referer':item['referer'], 'scheme':"https"})


    def item_completed(self, results, item, info):
        file_paths = [x['path'] for ok, x in results if ok]
        if not file_paths:
            raise DropItem("Item contains no files")
        item['file_paths'] = file_paths
        return item
  • 3.items的定义需要增加声明
import scrapy

class xxxxscrapyItem(scrapy.Item):
    file_urls=scrapy.Field()
    file_paths=scrapy.Field()
    referer=scrapy.Field()

如果不添加referer的 声明的话,就会看到所有的item处理结果都是Item contains no files,因为KeyError: 'xxxxxxItem does not support field: referer'

其实对于referer缺失还有一种临时性的解决方案,那就是不重写FilesPipeline而是启用DownloaderMidderware然后在process_request的时候进行处理:

  • 1)启用DownloaderMidderware
DOWNLOADER_MIDDLEWARES = {
   'xxxxScrapy.middlewares.xxxxscrapyDownloaderMiddleware': 543,
 }
  • 2)添加处理
    def process_request(self, request, spider):
        # Called for each request that goes through the downloader
        # middleware.

        # Must either:
        # - return None: continue processing this request
        # - or return a Response object
        # - or return a Request object
        # - or raise IgnoreRequest: process_exception() methods of
        #   installed downloader middleware will be called

        if not request.headers.get("referer"):
            request.headers['referer'] = request.url
            pprint(request.headers)

        return None

至于说它是 临时性方案,是因为它只是把自己的url塞进referer了,如果反爬虫机制检测稍微精细一点,它就失败了,毕竟它不够真。

还有一个要注意的问题就是,如果把yield item的操作封装了的话,比如封装为goto_download函数,那么调用这个函数的时候一定要添加return,否则就跟(原创)一个Promise...then...catch的小坑一样会迷失,这是异步编程的规范问题。

    # the urls should be a list
    # need be returned when calling it as "return self.goto_download(urls, referer)"
    def goto_download(self, urls, referer):
        if isinstance(urls, str):
            urls = [urls]
        
        item = xxxxscrapyItem()
        item['file_urls'] = urls
        item['referer'] = referer                       # important!
        yield item

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容