CrawlSpiders是Spider的派生类,Spider类的设计原则是只爬取start_url列表中的网页,而CrawlSpider类定义了一些规则(rule)来提供跟进link的方便的机制,从爬取的网页中获取link并继续爬取的工作更适合。适合爬取知乎或简书全站的数据,对于爬虫开发人员来说是个很强大的利器。
本章主要讲述一下我在学习 CrawlSpider 类时的历程。在学习使用 CrawlSpider 来爬取了中华网科技类的新闻数据后,我觉得该类对于深层爬取网页内容来说实在是太方便了。因此我仔细研究了一下 CrawlSpider 类与 Spider 类爬取数据的不同之处,这里我们就 CrawlSpider 类的源码进行分析。
简要说明
CrawlSpider 是爬取那些具有一定规则网站的常用的爬虫,它基于 Spider 并有一些独特属性:
因为 rules 是 Rule 对象的集合,所以这里也要介绍一下 Rule。
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)
主要参数:
注意:当编写爬虫规则时,避免使用 parse 作为回调函数。由于 CrawlSpider 使用 parse 方法来实现其逻辑,如果覆盖了 parse 方法,crawl spider 将会运行失败。
CrawSpider 源码详细解析
class CrawlSpider(Spider):
#首先由start_requests对start_urls中的每一个url发起请求(make_requests_from_url),这个请求会被parse接收。
rules = ()
def __init__(self, *a, **kw):
super(CrawlSpider, self).__init__(*a, **kw)
self._compile_rules()
#首先调用parse()来处理start_urls中返回的response对象
#parse()则将这些response对象传递给了_parse_response()函数处理,并设置回调函数为parse_start_url()
#设置了跟进标志位True
def parse(self, response):
return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)
#处理start_url中返回的response,可以重写
def parse_start_url(self, response):
return []
def process_results(self, response, results):
return results
#为每个链接生成Request对象,并设置回调函数为_repsonse_downloaded()
def _build_request(self, rule, link):
#将Rule规则中定义的回调函数作为这个Request对象的回调函数
r = Request(url=link.url, callback=self._response_downloaded)
r.meta.update(rule=rule, link_text=link.text)
return r
#从response中抽取符合任一用户定义'规则'的链接,并构造成Resquest对象返回
def _requests_to_follow(self, response):
if not isinstance(response, HtmlResponse):
return
seen = set()
for n, rule in enumerate(self._rules):
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]#保证抽取到的符合规则的链接不会有重复值
if links and rule.process_links:#这一步,由于rule.process_links不定义默认为None,所以不会进入
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link)
##对每个Request调用process_request()函数。该函数默认为indentify,即不做任何处理
yield rule.process_request(r)
##处理通过rule提取出的连接,并返回item
def _response_downloaded(self, response):
#response.meta['rule']里的rule值即为_requests_to_follow中传递给_build_request的n
rule = self._rules[response.meta['rule']]
return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)
#解析response对象,会用callback解析处理他,并返回request或Item对象
def _parse_response(self, response, callback, cb_kwargs, follow=True):
#使用_parse_response来解析response分为两种情况:
#1.当经过parse()函数传值到_parse_response时,此时的callback即为self.parse_start_url(),值为[],follow默认为True。
#所以此时会跳过第一个判断语句,进入到下一个判断条件里,self._follow_links默认也为True。
#2.当经过_response_downloaded()传值到_parse_response时,a.如果rule中有定义callback,则此时的callback即为rule中自定义的callback
(在 本程序中为parse_item),follow值为False,所以此时会进入第一个判断语句,不会进入第二个判断语句,最后会解析成Item;
#b.如果rule中没有定义callback,则此时的callback为None,follow默认为True,所以此时会进入第二个判断语句,最后会解析成request。
if callback:
cb_res = callback(response, **cb_kwargs) or ()
cb_res = self.process_results(response, cb_res)
for requests_or_item in iterate_spider_output(cb_res):
yield requests_or_item
if follow and self._follow_links:
for request_or_item in self._requests_to_follow(response):
#返回每个Request对象
yield request_or_item
def _compile_rules(self):
def get_method(method):
if callable(method):
return method
elif isinstance(method, six.string_types):
return getattr(self, method, None)
self._rules = [copy.copy(r) for r in self.rules]
for rule in self._rules:
rule.callback = get_method(rule.callback)
rule.process_links = get_method(rule.process_links)
rule.process_request = get_method(rule.process_request)
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = super(CrawlSpider, cls).from_crawler(crawler, *args, **kwargs)
spider._follow_links = crawler.settings.getbool(
'CRAWLSPIDER_FOLLOW_LINKS', True)
return spider
def set_crawler(self, crawler):
super(CrawlSpider, self).set_crawler(crawler)
self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
关于源码的分析在注释里已经写的很详细了,大家可以尝试跟着代码学习网站的爬取过程,如有疑惑,欢迎在评论区留言,大家一起学习进步。
实战分析
我们以中华网科技类新闻为例,来了解 CrawlSpider 和 Item Loader 的用法。
通过下面的命令可以快速创建 CrawlSpider 模板 的代码:
scrapy genspider -t crawl china tech.china.com
items.py
class NewsItem(scrapy.Item):
base_page_url = scrapy.Field()#当前列表页url
news_title = scrapy.Field()#新闻标题
news_url = scrapy.Field()#新闻详情地址
news_text = scrapy.Field()#新闻内容
news_publish_time = scrapy.Field()#新闻发布时间
news_source = scrapy.Field()#新闻来源
news_website = scrapy.Field()#新闻站点
china.py
class ChinaSpider(CrawlSpider):
name = 'china'
allowed_domains = ['tech.china.com']
start_urls = ['https://tech.china.com/articles/']
rules = (
Rule(
LinkExtractor(allow=r'article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'),
callback='parse_item'),
Rule(LinkExtractor(restrict_xpaths='//div[@class="pages"]/a[contains(text(),"下一页")]'))
# 当callback为空的时候,follow默认为True,follow为True是时,代表继续跟进匹配分析
)
def _build_request(self, rule, link, base_url):
r = Request(url=link.url, callback=self._response_downloaded, meta={'base_url': base_url})
r.meta.update(rule=rule, link_text=link.text)
return r
def _requests_to_follow(self, response):
# 重写该方法,只是稍作修改
base_url = response.url
# print('base_url------------{}'.format(base_url))
if not isinstance(response, HtmlResponse):
return
seen = set()
for n, rule in enumerate(self._rules):
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]
if links and rule.process_links:
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link, base_url)
yield rule.process_request(r)
def parse_item(self, response):
base_url = response.meta['base_url']
loader = ChinaLoader(item=NewsItem(), response=response)
loader.add_value('base_page_url', base_url)
loader.add_xpath('news_title', '//h1[@id="chan_newsTitle"]/text()')
loader.add_value('news_url', response.url)
loader.add_xpath('news_text', '//div[@id="chan_newsDetail"]//text()')
loader.add_xpath('news_publish_time', '//div[@id="chan_newsInfo"]/text()', re='(\d+-\d+-\d+\s\d+:\d+:\d+)')
loader.add_xpath('news_source', '//div[@id="chan_newsInfo"]/text()', re='来源:(.*)')
loader.add_value('news_website', '中华网')
yield loader.load_item()
结果如下:
在 china.py 方法中,我们对 CrawlSpider 类中的 _requests_to_follow 方法和 _build_request 进行了简单的重写,主要是为了提取每个列表页的 url 地址。虽然提取新闻列表页 url 没有什么太大的意义,但是去了解如何获取这一内容有助于我们更好的学习 CrawlSpider。
总结:多看,多想,多动手。
详情代码请点击这里:https://github.com/Acorn2/scrapyuniversal