Scrapy框架之CrawlSpider

hresh 342 0

Scrapy框架之CrawlSpider

CrawlSpiders是Spider的派生类,Spider类的设计原则是只爬取start_url列表中的网页,而CrawlSpider类定义了一些规则(rule)来提供跟进link的方便的机制,从爬取的网页中获取link并继续爬取的工作更适合。适合爬取知乎或简书全站的数据,对于爬虫开发人员来说是个很强大的利器。

本章主要讲述一下我在学习 CrawlSpider 类时的历程。在学习使用 CrawlSpider 来爬取了中华网科技类的新闻数据后,我觉得该类对于深层爬取网页内容来说实在是太方便了。因此我仔细研究了一下 CrawlSpider 类与 Spider 类爬取数据的不同之处,这里我们就 CrawlSpider 类的源码进行分析。

简要说明

CrawlSpider 是爬取那些具有一定规则网站的常用的爬虫,它基于 Spider 并有一些独特属性:

  • rules: 是Rule对象的集合,用于匹配目标网站并排除干扰
  • parse_start_url: 用于爬取起始响应,必须要返回 Item,Request 中的一个。

    因为 rules 是 Rule 对象的集合,所以这里也要介绍一下 Rule。

class scrapy.spiders.Rule(
        link_extractor, 
        callback = None, 
        cb_kwargs = None, 
        follow = None, 
        process_links = None, 
        process_request = None
)

主要参数:
- link_extractor:是一个 Link Extractor 对象,用于定义需要提取的链接。
- callback: 从 link_extractor 中每获取到链接时,参数所指定的值作为回调函数,该回调函数接受一个 response 作为其第一个参数。
- follow:是一个布尔(boolean)值,指定了根据该规则从 response 提取的链接是否需要跟进。 如果 callback 为 None,follow 默认设置为 True ,否则默认为 False。
- process_links:指定该 spider 中哪个的函数将会被调用,从 link_extractor 中获取到链接列表时将会调用该函数。该方法主要用来过滤。默认为 None。
- process_request:指定该 spider 中哪个的函数将会被调用, 该规则提取到每个 request 时都会调用该函数。 (用来过滤request),默认为 identity。

注意:当编写爬虫规则时,避免使用 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

发表评论 取消回复
表情 图片 链接 代码

分享