文章目录
入坑爬虫,学习scrapy查看 官方文档时的一些笔记。
初入爬虫
Scrapy是一个用于爬网网站和提取结构化数据的应用程序框架,可用于各种有用的应用程序,例如数据挖掘,信息处理或历史档案。
结构概览
scrapy中的engine
控制着数据的流动方向。
engine
从spiders
中获取到需要爬取的Request
对象engine
将这个Request
对象放到Scheduler
中进行任务调度,并且要求scheduler
返回下一个要爬取的Request
对象scheduler
将待爬取的Request
对象传递给engine
engine
经过download middlewates
的process_request()
方法后,到达downloader
downloader
开始下载Request
对应的网页,然后将返回的内容生成一个Response
对象。这个Response
对象将经过download middlewates
的process_response()
方法后,到达engine
engine
收到downloader
传递的response
对象以后,会将其通过spider middleaware
的process_spider_input()
传递到spider
中进行处理spider
再处理完response
对象以后,将提取处理的items
对象和下一个将要爬取的Request
对象通过spider middleaware
的process_spider_output()
传递到engine
中engine
将处理好的items
对象传递到item pipeline
中,然后将Request
对象传递给scheduler
,并询问下一个需要下载的Request
对象- 然后就开始重复1-8,直到所有的链接都被爬取完毕
安装
scrapy官网建议使用虚拟环境,避免与其他库冲突。
这里选用anaconda环境。conda install -c conda-forge scrapy
基本命令
scrapy提供了许多命令官方文档,分为全局可用和项目可用两大类。所有全局可用的在项目中也可以使用,但是可能会有不同的表现形式
1.全局可用命令
- 所有命令
scrapy
- 创建新项目
scrapy startproject tutorial
- 创建爬虫模块
scrapy genspider example example.com
- 请求网页资源
scrapy fetch <url>
在项目中执行该命令,会使用程序运行的上下文环境,比如项目中的user-agent,session, cookie
等信息 - 打开网页
scrapy view <url>
- 使用shell工具
scrapy shell [url]
- 查看配置信息
scrapy settings [options]
在项目中使用,会包含项目中的settings.cfg
- 直接运行一个爬虫,不用创建项目
scrapy runspider <spider_file.py>
2.项目可用命令
- 可执行爬虫列表
scrapy list
- 开始爬虫
scrapy crawl myspider
- 检测
scrapy check myspider
- 请求页面,并解析结果
scrapy parse <url> [options]
简单的爬虫项目
1. 创建新项目
在终端执行 scrapy startproject tutorial
2. 创建爬虫模块
example: 爬虫名 项目唯一 example.com: 需要爬取的网站 scrapy genspider example example.com
3. 编写代码
quotes_spider.py
import scrapy
class QuotesSpider(scrapy.Spider):
# 当前爬虫的名称,日志、启动时需要使用
name = "quotes"
# 当需要爬取的链接比较少,可以省略start_requests方法,选择start_urls。爬虫启动时,会爬取start_urls中的链接
# start_urls = [
# 'http://quotes.toscrape.com/page/1/',
# 'http://quotes.toscrape.com/page/2/',
# ]
def start_requests(self):
"""
:return: 可迭代对象,元素为爬虫将要爬取数据的网站
"""
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
"""
下载完成以后,对获取到内容以后进行处理
:param response: TextResponse对象,包含了网页信息
:return:
"""
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('save file {}'.format(filename))
4. 开始爬虫
scrapy crawl example
5. 开始爬虫,并保存数据
执行命令 scrapy crawl example -o data.jl
会将程序中yield
的数据保存在指定文件中
scrapy默认的将输出的数据追加到已存在的文件上。如果使用
json格式
,将会导致文件出错。因此这里选择jl格式
的数据。
6. 深度爬取数据
获取到的网页中,含有许多超链接,里面可能包含需要的数据,因此需要将这些链接提取出来,继续进行数据爬取。
修改quotes_spider.py
文件。
def parse(self, response):
"""
下载完成以后,对获取到内容以后进行处理
:param response: 网页信息
:return:
"""
for quote in response.css("div.quote"):
yield {
# ::text 获取文本
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags::text').getall()
}
# 提取网页中的URL,进行深度爬虫
next_page = response.css('li.next a::attr(href)').get()
# next_page = response.css('li.next a').attrib['href']
if next_page:
# 拼接下一个uri的地址,并发起请求
# yield response.follow(next_page, callback=self.parse)
next_url = response.urljoin(next_page)
yield scrapy.Request(url=next_url, callback=self.parse)
- 创建链接的快捷方式
scrapy提供了response.follow()
和response.follow_all()
方法,用来快速创建链接对象。
ps: 需要自己使用yield返回。使用follow_all()
时,需要使用yield from
- 可以将合并链接、发起请求简化为一个方法
yield response.follow(next_page, callback=self.parse)
- 传入选择器
for href in response.css('ul.pager a::attr(href)'):
yield response.follow(href, callback=self.parse)
- 对于使用了
a
标签的超链接,可以直接传入一个选择器或选择器的筛选条件
for a in response.css('ul.pager a'):
yield response.follow(a, callback=self.parse)
- 批量创建链接
# 选择器
anchors = response.css('ul.pager a')
yield from response.follow_all(anchors, callback=self.parse)
# 也可以传入css,xpath等筛选条件
yield from response.follow_all(css='ul.pager a', callback=self.parse)
7. 爬虫时传递参数
在开始爬虫时,使用-a key=value
传递参数。类在初始化时在__init__
方法中将key
设置为实例属性,
eg: scrapy crawl quotes -o quotes-humor.json -a tag=humor
6. 使用pycharm进行debug
在scrapy.cmdline
模块中提供了execute
函数,执行这个函数即可启动爬虫
- 在
project.cfg
同级目录下,新建一个debug.py
文件 - 加入以下代码
# -*- coding: utf-8 -*-
from scrapy.cmdline import execute
if __name__ == '__main__':
# execute()
execute(['scrapy', 'crawl', 'quotes-simple'])
- 多个爬虫程序时,可以省略
execute
参数,然后在pycharm运行时,指定要进行的操作
settings.py配置文件
DUPEFILTER_CLASS
URL过滤器
scrapy默认使用scrapy.dupefilters.RFPDupeFilter
,用来过滤已经爬过的网站
提高自我-核心概念
1.spiders
一个spider类对应着将要爬取数据的一个网站,在这个类中需要定义爬虫的名称,将要爬取的URL,以及对爬取数据的解析等。
通常来说,爬虫在进行着如下循环:
- 首先调用类中的
start_requests()
方法,这个方法中定义了一开始要爬取的地址start_urls
,并指定了对爬取回数据的处理函数call_back_function
。ps:start_requests()方法只会调用一次
- 在
call_back_function
中,用Selectors、BeautifulSoup、lxml
等工具解析TextResponse
对象中的数据,将解析好的数据传递出去。还可以返回新的Request
对象 - 最后通过item Pipeline或者FeedExport将数据保存下来
基类 Spider
所有的爬虫是scrapy.Spider
的子类
通用爬虫CrawlSpider
CrawlSpider
可以通过提供的rules
来提取网站中的链接,因此通常用来爬取URL格式比较规范的网站。
构造Rule的核心参数为LinkExtractor
,通过正则来过滤并返回需要继续爬取的URL,然后将爬取到的数据通过callback
方法来解析。
注意:
- callback可以是一个可以找到对应方法的字符串
- callback不要用parse
- 当callback不为空时,follow默认为False
import scrapy
from scrapy.spiders import Rule, CrawlSpider
from scrapy.linkextractors import LinkExtractor
class QuotesSpiderExtendCrawl(CrawlSpider):
name = "quote-crawl"
allow_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com']
# callback函数不要用parse,不然会怀疑人生
# follow为True时,会从已爬取的链接中再次使用如下规则进行过滤,有点儿递归的感觉
rules = (Rule(LinkExtractor(allow=(r"page/\d",)), callback='parse_list', follow=True),
Rule(LinkExtractor(allow=(r"author/.*",)), callback='parse_author'))
def parse_list(self, response):
"""
下载完成以后,对获取到内容以后进行处理
:param response: 网页信息
:return:
"""
for quote in response.css("div.quote"):
yield {
# ::text 获取文本
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags::text').getall()
}
def parse_author(self, response):
"""
提取作者信息
:param response:
:return:
"""
author_dict = dict(Name=response.css(".author-title::text").get(),
BornDate=response.css(".author-born-date::text").get(),
BornLocation=response.css(".author-born-location::text").get(),
Desc=response.css(".author-description::text").get()[:20])
self.log(json.dumps(author_dict))
yield author_dict
2. selectors
通常爬取的网页中信息量很大,而我们需要用的只是其中的一部分。因此需要从HTML中提取数据,BeautifulSoup和xml都可以实现。这里主要记录scrapy所提供的xpath
和css
这两种简单方式
Response
对象中的selector
属性为Selector
实例,提供了xpath()
和css()
方法。后来因为这两个方法使用太频繁,就在response中提供了这两个方法的快捷方式
选择器用于获取dom节点,支持链式语法。获取节点时,数据处理需谨慎,获取不到数据也最好不要让程序崩溃
There’s a lesson here: for most scraping code, you want it to be resilient to errors due to things not being found on a page, so that even if some parts fail to be scraped, you can at least get some data.
css选择器
::text
获取dom节点上的文字信息::attr(href)
获取指定的属性.get()
获取一个元素.getall()
获取所有选中元素.re()
使用正则过滤元素.attrib['href']
获取指定的属性
scrapy shell "http://quotes.toscrape.com/page/1/"
>>> response.css('title')
# 返回选择器对象
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
# ::text获取dom节点上的文字信息
# .get()获取一个元素
>>> response.css('title::text').get()
'Quotes to Scrape'
# .getall()获取一个list
>>> response.css('title::text').getall()
['Quotes to Scrape']
# .re()使用正则过滤数据
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']
# ::attr(itemprop)获取指定的属性
>>> response.css('div.quote')[0].css('span.text::attr(itemprop)').get()
'text'
# .attrib('itemprop')获取指定的属性
>>> response.css('div.quote')[0].css('span.text').attrib['itemprop']
'text'
xpath选择器
scrapy抽取数据时,支持使用css
和xpath
进行数据选择。但是css
的底层还是转化为xpath
,因此建议使用xpath
。
详细用法参考
/
使用绝对路径,从顶级开始//
使用的是相对路径.
当前路径..
父类路径nodename
所有指定节点@
获取节点属性
使用技巧
- 当根据
class
筛选节点时,使用CSS
选择器
如果选择xpath
,使用@class=A
会错过那些同时拥有A B
的节点 ,使用contains(@class, 'A')
会捕获到C-B-A
这种类名里包含的节点。
>>> from scrapy import Selector
>>> t = Selector(text='<div class="ABC">ABC</div><div class="A B C">A B C</div><div class="A">A</div>')
>>> t.xpath('//div[@class="A"]/text()').getall()
['A']
>>> t.xpath('//div[contains(@class,"A")]/text()').getall()
['ABC', 'A B C', 'A']
>>> t.css('div.A::text').getall()
['A B C', 'A']
//node[1]
和(//node)[1]
的区别
//node[1]
获取所有node
节点下的第一个节点
(//node)[1]
获取第一个node
节点下的第一个节点- 使用文本内容筛选节点,用
.
来代替.//text()
- 在
xpath
筛选时传入参数
# 使用$param作为占位符,然后通过关键字参数传参
>>> t.xpath('//div[contains(@class, $val)]/text()', val="A").getall()
['ABC', 'A B C', 'A']
3. items
scrapy将spider
返回的数据称为item
,是一个键值对的Python对象。以下为几种常用item类型
字典dict
scrapy定义的item对象scrapy.item.Item
scrapy的Item
对象是dict
的加强版,提供了copy
和deepcopy
方法,并且可以将key
定义为属性,使用时更加规范
from scrapy.item import Item, Field
from scrapy.loader.processors import MapCompose, Join, TakeFirst
from w3lib.html import remove_tags
class QuotesAuthorItem(scrapy.Item):
# input_processor和output_processor分别为输入输出处理函数
Name = scrapy.Field(input_processor=MapCompose(remove_tags),
output_processor=TakeFirst())
BornDate = scrapy.Field()
BornLocation = scrapy.Field()
Desc = scrapy.Field()
Remark = scrapy.Field()
Dataclass 装饰的对象
使用dataclass
装饰的对象,也可以将key
定义为属性
from dataclasses import dataclass
@dataclass
class CustomItem:
one_field: str
another_field: int
attr.s 装饰的对象
使用attr.s
装饰的对象,也可以将key
定义为属性
import attr
@attr.s
class CustomItem:
one_field = attr.ib()
another_field = attr.ib()
4. Items Loader
将Items
看做一个容器,那么items loader
就是用来决定数据进入那个容器的机制。
实例
导入QuotesAuthorItem,重写parse_author
方法
form ..items import QuotesAuthorItem
def parse_author(self, response):
# 默认返回的数据都是列表形式
item = ItemLoader(item=QuotesAuthorItem(), response=response)
item.add_css('Name', ".author-title::text")
item.add_css('BornDate', ".author-born-date::text")
item.add_css('BornLocation', ".author-born-location::text")
item.add_value('Desc', response.css(".author-description::text").get()[:20])
item.add_xpath('Remark', '//h1/text()')
yield item.load_item()
输入输出处理器
每一个item Loader
对于每一个属性,都有对应的输入处理器和输出处理器。当使用add_css(), add_xpath(), add_value()
时输入处理器会被调用,当数据提取完毕,调用item.load_item()
时输出处理器会被调用。item
最终获取的值,是经历过输出处理器处理过的值。
输入和输出处理器都必须接收一个可迭代对象作为其第一个参数。
这些函数的输出可以是任何东西。
输入处理器的结果将被添加到内部列表中(在Loader中),该列表包含(针对该字段)收集的值。
输出处理器的结果是最终将分配给该item的值。
常用处理器函数
w3lib.html.remove_tags
去除html
或xml
的标签scrapy.loader.processors.TakeFirst
取可迭代对象中的第一个元素scrapy.loader.processors.Join
拼接数据,默认空格scrapy.loader.processors.Compose(*functions, **default_loader_context)
输入值由第一个函数处理,输出值由第二个函数处理scrapy.loader.processors.MapCompose(*functions, **default_loader_context)
可以同时传入多个处理函数,然后每一个经过处理的元素都会被加入到一个列表中,然后会将这个列表作为参数,传入下一个处理函数中,直到所有函数都处理完以后才会返回结果。scrapy.loader.processors.SelectJmes(json_path)
声明的时候需要传入一个字符串,然后就可以在json格式
的数据中搜索到这个字符串对应的值
自定义Items Loader
scrapy中提供了需要通用的处理器,使用时直接实例化对应类
即可。也可以自定义处理器,通过在item loader
类属性后加_in
和_out
来指定对应的输入处理器和输出处理器。
ps:自定义输入处理器后,一定要记得
返回处理结果
,否则输出处理器会获取不到值
自定义处理器函数,需要作为参数实例化scrapy.loader.processors.MapCompose
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join
def value_in(value):
if isinstance(value, str):
print('{} in'.format(value))
return value
class TutorialAuthorLoader(ItemLoader):
default_output_processor = TakeFirst()
Name_in = MapCompose(value_in)
Name_out = Join()
嵌套——nested_xpath和nested_css
item loader
提供了nested_xpath()
和nested_css()
方法,当需要提取同一个父节点下的数据,并且这个父节点隐藏的很深的时候。可以通过这两个方法,先选中父节点,然后子节点的选择只需要相对父节点即可。类似于使用xpath
选择器时使用的相对路径。
# 通常写法
loader = ItemLoader(item=Item())
# load stuff not in the footer
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
loader.load_item()
# 嵌套写法
loader = ItemLoader(item=Item())
# load stuff not in the footer
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
# no need to call footer_loader.load_item()
loader.load_item()
5. scrapy shell
加强版的python shell。可以用来尝试获取选择器,提取数据
scrapy shell http://www.baidu.com
跳一跳
在编写程序时,经常需要对代码进行调试,但是依赖pycharm的调试框,每次只能展示一个结果,结果对比比较麻烦。贴心的是,scrapy
提供了一个inspect_response()
的方法,程序执行到这个方法时,会自动跳到终端,并且携带了上下文环境,当退出终端时,还可以继续执行刚才的代码,非常因吹斯汀。
6. Item Pipline
当数据从spider
中抛出(yield)
以后,会依次进入指定的item Pipline
中进行数据筛选,每一个Pipline
都可以决定数据继续处理,还是被舍弃掉。
常用场景
- 清洗
html
数据 - 校验数据是否是自己想要的
- 检查数据是否有重复
- 将数据存放到数据库中
自定义Pipeline
每一个Pipeline
组件都是一个独立的python类,必须实现一些必要的方法:
- 必须实现
process_item(self, item, spider)
其中spider就是对应的爬虫类,item则为爬虫返回的数据。这个方法必须在返回item object
、返回defererd
,raise DropItem exception
中三选一。··
process_item() must either: return an item object, return a Deferred or raise a DropItem exception. - 可选实现方法
- open_spider(self, spider) 爬虫开始时调用
- close_spider(self, spider) 爬虫结束后调用
- from_crawler(cls, crawler)
If present, this classmethod is called to create a pipeline instance from a Crawler. It must return a new instance of the pipeline. Crawler object provides access to all Scrapy core components like settings and signals; it is a way for pipeline to access them and hook its functionality into Scrapy.
当from_crawler
方法存在的时候,将以crawer为媒介创建并返回一个Pipeline
实例。crawler
对象提供了访问所有scrapy组件(比如settings,signals)访问的入口。Pipeline管道可以通过这种方式访问scrapy的方法,并且将自己的方法挂到scrapy中,等待数据处理时被调用。
PipeLine 实例
- 在Pipeline.py中实现自定义管道
import json
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
from .items import QuotesItem
class TutorialPipeline:
"""
过滤内容为空的数据
"""
def process_item(self, item, spider):
adapter = ItemAdapter(item)
print(item, type(item))
if isinstance(item, QuotesItem):
if adapter.get("Text"):
return item
else:
raise DropItem("没有内容%s" % item)
return item
class TutorialStorePipeline:
"""
数据存储
"""
collection_name = 'scrapy_items'
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
return item
- 在配置文件
settings.py
中,注册要使用的管道
ITEM_PIPELINES = {
# key为Pipeline所在包,value是数据流通的顺序,值范围通常在0-1000,数据会先进入值最小的管道,然后进入值大的管道
'tutorial.pipelines.TutorialPipeline': 300,
'tutorial.pipelines.TutorialStorePipeline': 301,
}
在管道中使用协程
当在管道中处理数据需要进行一些耗时的操作时,可以通过使用协程来分配内存资源,降低程序耗时。
class ImageDownloadPipeline:
async def process_item(self, item, spider):
adapter = ItemAdapter(item)
if adapter.get("Url"):
request = scrapy.Request(adapter.get("Url"))
response = await spider.crawler.engine.download(request, spider)
if response.status != 200:
return item
url = adapter["url"]
url_hash = hashlib.md5(url.encode("utf8")).hexdigest()
filename = "{}.png".format(url_hash)
with open(filename, "wb") as f:
f.write(response.body)
# Store filename in item.
adapter["screenshot_filename"] = filename
return item
7.Feed Exports
通过爬虫爬取的数据,通常需要导出到文件存储系统中(数据库,文件),以供其他系统使用。scrapy通过Feed Exports
实现这个功能,允许数据以多种格式存放到多种存储系统中。
支持数据序列化格式
- json
- json line
- csv
- xml
- pickle
- Marshal
支持的存储引擎
- Local filesystem
- ftp
- S3 (requires botocore)
- Standard output
相关配置
- FEED_EXPORT_ENCODING 编码格式
- FEED_STORE_EMPTY 是否储存空数据
- FEED_EXPORT_FIELDS 导出字段
- FEED_EXPORT_INDENT 缩进
- FEED_STORAGES 自定义存储引擎 {URI:storage class}
- FEED_STORAGE_FTP_ACTIVE ftp存储引擎
- FEED_STORAGE_S3_ACL S3存储
- FEED_STORAGES_BASE scrapy内置的存储引擎
- FEED_EXPORTERS 自定义数据序列化格式
- FEED_EXPORTERS_BASE 内置支持的序列化格式
以上所有的配置项,可以整合到FEED
中配置,查看详情
8. Requests和Responses
scrapy主要依靠Request
和Response
两个对象爬取web网站。通常Request
对象在爬虫开始时生成,然后在各个组件中传递,直到进入Downloader
下载器。接着Downloader
会请求Request
对象指定的地址,并且返回一个Response
对象给对应的爬虫类。
Request对象
通过cb_kwargs
给call_back
方法传递额外参数
def parse(self, response):
request = scrapy.Request('http://www.example.com/index.html',
callback=self.parse_page2,
cb_kwargs=dict(main_url=response.url))
request.cb_kwargs['foo'] = 'bar' # add more arguments for the callback
yield request
def parse_page2(self, response, main_url, foo):
yield dict(
main_url=main_url,
other_url=response.url,
foo=foo,
)
通过err_back
处理异常
def parse(self, response):
request = scrapy.Request('http://www.example.com/index.html',
callback=self.parse_page2,
errback=self.errback_page2,
cb_kwargs=dict(main_url=response.url))
yield request
def parse_page2(self, response, main_url):
pass
def errback_page2(self, failure):
if failure.check(HttpError):
# these exceptions come from HttpError spider middleware
# you can get the non-200 response
response = failure.value.response
self.logger.error('HttpError on %s', response.url)
yield dict(
# err_back中的额外参数,需要通过failure访问request对象,然后访问request对象的cb_kwargs属性
main_url=failure.request.cb_kwargs['main_url'],
)
Request中的一些特殊的meta值
ps: 关于cookie信息,request对象中可以携带cookie,并且会自动合并response设置的cookie信息,再下一次请求时,携带这些cookie信息。如果想禁用这一特性,可以在request的meta中设置
dont_merge_cookies
为True
停止从响应中下载数据
从bytes_received
信号处理器中抛出一个StopDownload
异常,会停止从response
中响应数据
import scrapy
class StopSpider(scrapy.Spider):
name = "stop"
start_urls = ["https://docs.scrapy.org/en/latest/"]
@classmethod
def from_crawler(cls, crawler):
spider = super().from_crawler(crawler)
crawler.signals.connect(spider.on_bytes_received, signal=scrapy.signals.bytes_received)
return spider
def parse(self, response):
# 'last_chars' show that the full response was not downloaded
yield {"len": len(response.text), "last_chars": response.text[-40:]}
def on_bytes_received(self, data, request, spider):
# 通常情况下,抛出异常之后,会调用其对应的err_back函数,这里设置fail=False以后,scrapy会认为程序并没有出错,并调用call_back函数
raise scrapy.exceptions.StopDownload(fail=False)
常见子类
- FormRequest 常用来传递表单格式参数,比如模拟登陆
# 通过post方式传递参数
def parse1(self, response):
return [FormRequest(url="http://www.example.com/post/action",
formdata={'name': 'John Doe', 'age': '27'},
callback=self.after_post)]
# 通过from_response模拟登陆
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={'username': 'john', 'password': 'secret'},
callback=self.after_login
)
- JsonRequest 传递JSON格式的参数
Response对象
通过follow和follow_all快速构建新的Request对象
# 拼接下一个uri的地址,并发起请求,和下边两行代码作用相同
yield response.follow(next_page, callback=self.parse)
next_url = response.urljoin(next_page)
yield scrapy.Request(url=next_url, callback=self.parse)
# follow_all生成一批Request对象
yield from response.follow_all(css='div.quote small.author +a', callback=self.parse_author)
常见子类
- TextResponse
- HtmlResponse
- XmlResponse
9. Link Extractors
链接生成器可以通过一系列规则从Response对象中筛选URL。通过rules
属性指定本爬虫类需要爬取的url地址,及其对应的处理函数。scrapy.linkextractors.LinkExtractor
和scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor
是同一个对象,前者主要是为了导入方便。可以通过其extract_links(response)
方法查看匹配到的URL。
rules = (Rule(LinkExtractor(allow=(r"page/\d",)), callback='parse_list', follow=True),
Rule(LinkExtractor(allow=(r"author/.*",)), callback='parse_author', errback='err_back_test', cb_kwargs={"filter_none": True}))
10. Middleware
middleware
中间件可以看做是一个桥梁,当数据从engine
到downloader
或spider
时,需要经过这些桥梁。可以在这些桥梁中设置特定方法,进行数据处理。
Downloader Middleware
下载中间件位于engine
和downloader
中间,用于处理Request
和Response
对象。
激活中间件
使用时,需要在settings.py
文件中声明.
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.CustomDownloaderMiddleware': 543,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}
声明的中间件会与
scrapy
默认配置的中间件合并,并按照值从小到大进行处理。禁用默认中间件需要将值设为None
即可。
内置中间件
- CookiesMiddleware 模拟浏览器管理cookie
- DefaultHeadersMiddleware 为请求设置settings中配置的请求头
- DownloadTimeoutMiddleware
- HttpAuthMiddleware 在需要认证的爬虫中设置
http_user
和http_pass
- HttpCacheMiddleware 缓存
- HttpCompressionMiddleware 压缩
- RedirectMiddleware 重定向
- MetaRefreshMiddleware
- RetryMiddleware 错误重试
- RobotsTxtMiddleware 网站允许爬虫资源
- DownloaderStats 下载统计 需要设置
DOWNLOADER_STATS=True
- UserAgentMiddleware
user agent
设置 - AjaxCrawlMiddleware
自定义中间件
当我们想diy一个中间件时,可以实现process_request
、process_response
、process_exception
方法。这些方法会在恰当的时间被scrapy调用,不需全部实现,满足自己的需求即可。最后记得要在settings.py
中设置激活。
class TutorialDownloaderMiddleware:
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: 继续走流程
# - or return a Response object:流程中断,不会调用其它的process_request,process_exception以及对应的下载方法,但是会调用process_response方法
# - or return a Request object:流程中断,并计划下载这个新的request对象
# - or raise IgnoreRequest: 流程中断,由process_exception接管,如果中间件没有处理,则往上抛由Request.errback处理。
# 如果代码未处理这个异常,这个异常就被忽略,并且不会被记录下来
return None
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object 继续走流程
# - return a Request object 流程中断,开始准备下载新的request对象
# - or raise IgnoreRequest 调用Request.errback,如果代码未处理这个异常,这个异常就被忽略,并且不会被记录下来
return response
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain,进入到process_response的流程中
# - return a Request object: stops process_exception() chain, 准备下载request对象
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
Spider Middleware
爬虫中间件位于engine
和spider
中间,用于处理spider
生成的request、item
对象,以及engine
返回的response
对象。
激活中间件
使用时,需要在settings.py
文件中声明.
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.CustomSpiderMiddleware': 543,
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': None,
}
声明的中间件会与
scrapy
默认配置的中间件合并,并按照值从小到大进行处理。禁用默认中间件需要将值设为None
即可。
内置爬虫中间件
- DepthMiddleware 记录爬虫的深度,在
settings.py
中通过DEPTH_LIMIT
,DEPTH_STATS_VERBOSE
,DEPTH_STATS_VERBOSE
- HttpErrorMiddleware 过滤掉出错的HTTP响应,降低资源消耗
HTTPERROR_ALLOWED_CODES
、HTTPERROR_ALLOW_ALL
- OffsiteMiddleware 过滤掉爬取域名以外的链接
- UrlLengthMiddleware Url长度过滤
- RefererMiddleware
内置服务
日志管理
scrapy自身提供的scrapy.log
已经废弃了,现在则是使用logging
库,每一个spider
实例中都可以通过logger
属性进行日志输出,默认的日志名称为爬虫的名字。
统计器
scrapy为每一个爬虫都提供了一个统计计数器stats
,这个计数器会在爬虫开启时启动,在爬虫结束后关闭。爬虫类需要通过self.crawler.stats
才能访问到该属性
def parse(self, response):
"""
stats对象是scrapy提供的统计对象,常用来计数
"""
# count赋值
self.crawler.stats.set_value('count', 1)
# count++
self.crawler.stats.inc_value('count')
# 当5>count时,值变为5
self.crawler.stats.max_value('count', 5)
# 当3<count时,值变为3
self.crawler.stats.min_value('count', 3)
# 获取count值
self.crawler.stats.get_value('count')
# 获取所有统计值
self.crawler.stats.get_stats()
使用技巧
通过Spiders Contracts进行单元测试
针对爬虫的测试比较麻烦,大部分情况下只能写单元测试。Scrapy 通过合同(contract)的方式来提供了测试 spider 的集成方法。通俗讲就是在方法内部通过@contract
的方式硬编码参数。参考极客网站
url
对应的是scrapy.contracts.default.UrlContract
协议,设置了用于检查 spider 的其他 constract 状态的样例 url。returns
对应的是scrapy.contracts.default.CallbackKeywordArgumentsContract
协议,设置 spider 返回的 items 和 requests 的上界和下界scrapes
对应的是class scrapy.contracts.default.ScrapesContract
协议,用于检查回调函数返回的所有 item 是否有特定的 fields
def parse(self, response):
""" This function parses a sample response. Some contracts are mingled
with this docstring.
@url http://www.amazon.com/s?field-keywords=selfish+gene
@returns items 1 16
@returns requests 0 0
@scrapes Title Author Year Price
"""
突破反爬虫基本技巧
爬虫和反爬虫之间的斗争一直没有停过,整理一些基础的反爬虫技巧,有待研究
-
动态修改
user agent form
简单点多准备一些常用浏览器的User-Agent
,在发起请求时,随机设置User-Agent
复杂点,使用
fake-useragent
库,配合自定义的下载中间件,记得在settings.py
文件中激活
class MyUserAgentDownloadMiddleware(object):
def __init__(self, crawler):
super(MyUserAgentDownloadMiddleware, self).__init__()
self.ua = UserAgent()
self.ua_type = crawler.settings.get("RANDOM_USER_AGENT", "random")
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
return cls(crawler)
def process_request(self, request, spider):
def get_ua():
return getattr(self.ua, self.ua_type)
request.headers.set_default("User-Agent", get_ua())