2020-03-23
最近自己又玩了玩爬虫,由于目标网站跳转很多而且是要下载文件,所以选择了Scrapy框架。
对于Scrapy框架久仰大名,知道它内部封装实现了异步并发,但前两年专职做爬虫的时候用的是NodeJs自行开发的分布式微服务任务处理框架,所以也没有认真研究过Scrapy,这次正好借机学习研究并应用一番。
起
首先当然是要看文档,这次为了节省时间,我看的是中文版文档。在这还是要小小地吐槽一下,翻译质量确实不高,专业术语压根错的离谱,但毕竟人生苦短,有经验的人应该都不会轻易被误导。
文档虽然翻译不精准,但步骤和案例也算比较详细,所以前面步骤都如Scrapy 教程一般顺利:
- 用
scrapy startproject MyProject
创建项目 - 在
/spiders/
目录下修改编写自己的爬虫逻辑,通过yield
和callback
来实现异步和回调 - 使用
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