菜鸟写Python-Scrapy:Spider源码分析扩展-CrawlSpider使用分析(详解)

本系列文章在上章节: 菜鸟写Python-Scrapy:Spider源码分析,分析了Scrapy的Spider爬虫源码,并且也提到了spider分为四种模版,常见是的basic和crawl两种,那么本文将详细阐述一下CrawlSpider基本情况。

一、前言

我们发现很多网站中下属url都是有一定规则的(如django在item中定义的urls规则就是正则表达的),那么我们如果能利用这些规则,似乎就可以省去我们仔细去解析网页面中url的苦力,有没有方法呢?答案是肯定的,要满足这个要求就可以使用spider提供的第二个模块crawlspider,它非常适合爬取整个网站数据。

二、CrawlSpider分析

CrawlSpider是爬取一般网站常用的spider。它定义了一些规则(rule)来提供跟进link的方便的机制。它同样要继承Spider,然后在基础上提供了一些Rule规则来指定爬取规则。

生成CrawlSpider项目的cmd命令:scrapy genspider -t crawl lagou www.lagou.com  (scrapy genspider -t 模版 项目名 域名)

相对之前的改变在于通过-t指定了spider的模版,如果不指定则默认为basic模版,这个就是我们之前使用的。

crawlspider要做的是全站点urls的匹配,当找到fllow我们要页面时才做页面解析,因此要实现这一步,肯定要告诉我们的spider哪一种的url规则才是我们要爬取的,所以crawlspider相比basicspider先提供了一个Rule规则类,来筛选符合要求我们的urls。

CrawlSpider基于Spider自己独有的特性:

  • Rules: 是Rule对象的集合,用于匹配目标网站并排除干扰,简单来说就是定义爬取urls的规则
  • parse_start_url: 用于爬取起始响应,必须要返回ItemRequest中的一个。

Rule参数:link_extractor、callback=None、cb_kwargs=None、follow=None、process_links=None、process_request=None

如果这一步理解拗口,建议要挑到第三节看怎么使用crawlspider,然后又感觉再回来看rules的定义。

参数理解:

1.link_extractor:该值(方法)是一个Link Extractor实例,主要定义的就是链接的解析规则。规则如下:

  • allow:值是正则表达式,满足括号中“正则表达式(列表,dict列表)”的urls会被提取;如果为空,则全部匹配。
  • deny:不提取与这个正则表达式(列表,dict()列表)匹配的URL。
  • allow_domains:会被提取的链接的domains。
  • deny_domains:一定不会被提取链接的domains。
  • restrict_xpaths/restrict_css:使用xpath/css表达式,来指定爬取哪一块的urls和allow共同过滤爬取链接。

2.callback:回调方法,用来解析response爬取页面数据的。但是回调方法函数名不能用parse,因为parse在crawlspider定义了并且有自己不一样的作用,它已经不像spider类中的那样没有具体操作。

3.cb_kwargs:给callback方法传递参数。

4.follow:布尔对象,用来表示是当前页面(response)中的url是否继续采集。如果callback是None,那么它就默认为True,否则为False。

5.process_links:该方法在crawlspider中的_requests_to_follow方法中被调用,它接收一个元素为Link的列表作为参数,返回值也是一个元素为Link的列表。可以用该方法对采集的Link对象进行修改,比如修改Link.url。这里的如果你的目标url是相对的链接,那么scrapy会将其扩展成绝对的。

6.process_request:处理request的。(其实后面两个参数,我也还没有实际用上,先当理解吧,觉得拗口就先跳过吧)

三、CrawlSpider使用

我们这一步先尝试根据官方给出的模版先写一个crawlspider,因为我发现我先去看了源码,发现源码理解可比使用要难的多,但是crawlspider的基本使用确实简单易解,一看就会使用。

(这里已爬取拉勾招聘职位数据为案例)

1.生成一个crawlspider:scrapy genspider -t crawl lagou www.lagou.com

2.编写Rule规则,筛选要爬取的url,以lagou为例:

2.1 抓取urls的规则--目的是找到招聘职位的详情页面,如https://www.lagou.com/jobs/3959551.html,这是一个详细python招聘职位的描述,然后爬取这个岗位的详细数据
2.2 那怎么才能从lagou这个站中那么多urls找到类似上述的url呢?所以要给定抓取urls规则或者范围
2.3 分析lagou发现从以下两个地方可以进入招聘职位页面:
# 1、招聘分类:这里面有各类招聘的所有职位; https://www.lagou.com/zhaopin/***** 招聘页面urls共性
# 2、招聘需要公司发布,我们从公司页面也可以抓取到所有的职位; https://www.lagou.com/gongsi/** 公司页面共性
# 3、从上面两个大urls可以进入到一个具体岗位的url,如https://www.lagou.com/jobs/3959551.html,而.*/jobs/.*html是共性
所以我们可以写一个Rules,来界定我们抓取的规则:1圈定范围 2从范围中找到要解析的页面
rules = (
    # 根据上面分析,可以写一些规则也圈定抓取的urls
    # 爬取urls中有 zhaopin/的urls,并却对这类页面中的所有的url进行跟进follow
    Rule(LinkExtractor(allow=('.*/zhaopin/.*',)),follow=True),
    # 也可以再加规则,如果发现公司页面也进行follow然后找到公司中的发布的职位,也可以爬取到
    Rule(LinkExtractor(allow=(r'.*/gongsi/.*')),follow=True),
    # 对页面进行根据时,如果发现/jobs/.*.html的url则说明找到了要爬取的页面,那么调用parse_item进行解析
    Rule(LinkExtractor(allow=r'/jobs/.*.html'), callback='parse_item', follow=True),
)

3.编写一个用于解析页面的parse_item,这个方法的作用跟以前的parse一样,但区别在于,在crawlspider中这个处理方法,不能取名为parse。

(你可以取任何名字,就是一定不能取名为这个,原因是在crawlspider类中该parse方法有它自己的作用,如何命名为parse则为重新了类方法,那么会改变crawlspider正常执行,后面源码中会介绍到。)

    # 符合规则的进行页面爬取解析response
    def parse_item(self, response):
        item = {}
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return item

这个方法就是用来分析页面,和拿到页面数据了,具体的实现跟以前spider通过正则表达提取一样,然后把数据给item并传给pipeline处理。这里就不在详细说了。唯独还要提醒一点的就是命名:慎用(别用)parse做为回调方法,因为这边的parse已经不像spider类中的那样没有具体操作。

看到了这里,是不是发现crawlspider其实代码实现很简单,而且很方便抓取整个网站某一类的urls。

lagou完整代码如下:

class LagouSpider(CrawlSpider):
    name = 'lagou'
    allowed_domains = ['www.lagou.com']
    start_urls = ['http://www.lagou.com/']

    # 抓取urls的规则--目的是找到招聘职位的详情页面,然后爬取职位的详细数据
    # 如https://www.lagou.com/jobs/3959551.html,这是一个详细python招聘职位的描述
    # 那怎么才能从lagou这个站中那么多urls找到类似上述的url呢?所以要给定抓取urls规则或者范围
    # 分析lagou发现从以下两个地方可以进入招聘职位页面:
    # 1、招聘分类:这里面有各类招聘的所有职位; https://www.lagou.com/zhaopin/***** 招聘页面urls共性
    # 2、招聘需要公司发布,我们从公司页面也可以抓取到所有的职位; https://www.lagou.com/gongsi/** 公司页面共性
    rules = (
        # 根据上面分析,可以写一些规则也圈定抓取的urls
        # 爬取urls中有 zhaopin/的urls,并却对这类页面中的所有的url进行跟进follow
        Rule(LinkExtractor(allow=('.*/zhaopin/.*',)),follow=True),
        # 也可以再加规则,如果发现公司页面也进行follow然后找到公司中的发布的职位,也可以爬取到
        Rule(LinkExtractor(allow=(r'.*/gongsi/.*')),follow=True),
        # 对页面进行根据时,如果发现/jobs/.*.html的url则说明找到了要爬取的页面,那么调用parse_item进行解析
        Rule(LinkExtractor(allow=r'/jobs/.*.html'), callback='parse_item', follow=True),
    )

    # 符合规则的进行页面爬取解析response
    def parse_item(self, response):
        item = {}
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return item

四、CrawlSpider源码分析

class CrawlSpider(Spider):

    rules = ()

    def __init__(self, *a, **kw):
        super(CrawlSpider, self).__init__(*a, **kw)
        self._compile_rules()

    #1、首先调用parse()方法来处理start_urls中返回的response对象。
    #2、parse()将这些response对象传递给了_parse_response()函数处理,并设置回调函数为parse_start_url()。
    #3、设置了跟进标志位True,即follow=True。
    #4、返回response。
    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

    def _build_request(self, rule, link):
         #构造Request对象,并将Rule规则中定义的回调函数作为这个Request对象的回调函数。这个‘_build_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:
                links = rule.process_links(links)
            #将链接加入seen集合,为每个链接生成Request对象,并设置回调函数为_repsonse_downloaded()。
            for link in links:
                seen.add(link)
                #构造Request对象,并将Rule规则中定义的回调函数作为这个Request对象的回调函数。这个‘_build_request’函数在上面定义。
                r = self._build_request(n, link)
                #对每个Request调用process_request()函数。该函数默认为indentify,即不做任何处理,直接返回该Request。
                yield rule.process_request(r)

    #处理通过rule提取出的连接,并返回item以及request。
    def _response_downloaded(self, response):
        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):
        #1、首先判断是否设置了回调函数。(该回调函数可能是rule中的解析函数,也可能是 parse_start_url函数)  
        #2、如果设置了回调函数(parse_start_url()),那么首先用parse_start_url()处理response对象,  
        #3、然后再交给process_results处理。返回cb_res的一个列表。  
        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

        #如果需要跟进,那么使用定义的Rule规则提取并返回这些Request对象。
        if follow and self._follow_links:
            #返回每个Request对象。
            for request_or_item in self._requests_to_follow(response):
                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)

作者:小怪聊职场
链接:https://www.jianshu.com/p/d492adf17312
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

我们对上述代码做了分析,发现执行流程其实是:

猜你喜欢

转载自blog.csdn.net/godot06/article/details/81672900