目录
写在前面
在Scrapy基础——Spider中,我简要地说了一下Spider类。Spider基本上能做很多事情了,但是如果你想爬取知乎或者是简书全站的话,你可能需要一个更强大的武器。
CrawlSpider基于Spider,但是可以说是为全站爬取而生。
简要说明
CrawlSpider是爬取那些具有一定规则网站的常用的爬虫,它基于Spider并有一些独特属性
- rules: 是Rule对象的集合,用于匹配目标网站并排除干扰
- parse_start_url: 用于爬取起始响应,必须要返回Item,Request中的一个。
因为rules是Rule对象的集合,所以这里也要介绍一下Rule。它有几个参数:link_extractor、callback=None、cb_kwargs=None、follow=None、process_links=None、process_request=None
其中的link_extractor既可以自己定义,也可以使用已有LinkExtractor类,主要参数为:
- allow:满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。
- deny:与这个正则表达式(或正则表达式列表)不匹配的URL一定不提取。
- allow_domains:会被提取的链接的domains。
- deny_domains:一定不会被提取链接的domains。
- restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接。还有一个类似的restrict_css
下面是官方提供的例子,我将从源代码的角度开始解读一些常见问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import scrapy from scrapy.spiders import CrawlSpider, Rule from scrapy.linkextractors import LinkExtractor class MySpider(CrawlSpider): name = 'example.com' allowed_domains = ['example.com'] start_urls = ['http://www.example.com'] rules = ( # Extract links matching 'category.php' (but not matching 'subsection.php') # and follow links from them (since no callback means follow=True by default). Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))), # Extract links matching 'item.php' and parse them with the spider's method parse_item Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'), ) def parse_item(self, response): self.logger.info('Hi, this is an item page! %s', response.url) item = scrapy.Item() item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)') item['name'] = response.xpath('//td[@id="item_name"]/text()').extract() item['description'] = response.xpath('//td[@id="item_description"]/text()').extract() return item |
CrawlSpider介绍及主要函数讲解
CrawlSpider是爬取一般网站常用的spider。它定义了一些规则(rule)来提供跟进link的方便的机制。也许这个spider并不是完全适合特定网站或项目,但它对很多情况都使用。
因此我们可以在它的基础上,根据需求修改部分方法。当然我们也可以实现自己的spider。除了从Spider继承过来的(必须提供的)属性外,它还提供了一个新的属性:
1)rules
一个包含一个(或多个)Rule对象的集合(list)。 每个Rule对爬取网站的动作定义了特定表现。如果多个Rule匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。
使用方式案例如下:
1 2 3 4 5 6 7 |
rules = ( # 提取匹配 'category.php' (但不匹配 'subsection.php') 的链接并跟进链接(没有callback意味着follow默认为True) Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))), # 提取匹配 'item.php' 的链接并使用spider的parse_item方法进行分析 Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item', follow=True), ) |
2)parse_start_url(response)
当start_url的请求返回时,该方法被调用。 该方法分析最初的返回值并必须返回一个Item对象或者一个Request对象或者一个可迭代的包含二者对象。
该spider方法需要用户自己重写。
1 2 |
def parse_start_url(self, response): return [] |
3)parse(),一定不要重写这个方法
通过上面的介绍,我们知道Spider中的parse()方法是需要我们重写的,如下:
1 2 |
def parse(self, response): raise NotImplementedError |
但是,CrawlSpider中的parse()方法在源码中已经实现了一些功能,如下:
1 2 |
def parse(self, response): return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True) |
所以我们在使用CrawlSpider时,一定一定不要去重写parse()函数(重点)。可以使用Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item', follow=True)
中的callback去指定需要跳转的parse。
例如我们在讲解简书全站爬取的时候使用方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class JianshuCrawl(CrawlSpider): name = "jianshu_spider_crawl" # 可选,加上会有一个爬取的范围 allowed_domains = ["jianshu.com"] start_urls = ['https://www.jianshu.com/'] # response中提取链接的匹配规则,得出符合条件的链接 pattern = '.*jianshu.com/u/*.' pagelink = LinkExtractor(allow=pattern) # 可以写多个rule规则 rules = [ # 只要符合匹配规则,在rule中都会发送请求,同时调用回调函数处理响应。 # rule就是批量处理请求。 Rule(pagelink, callback='parse_item', follow=True), ] # 不能写parse方法,因为源码中已经有了,会覆盖导致程序不能跑 def parse_item(self, response): for each in response.xpath("//div[@class='main-top']"): ...... |
callback='parse_item'
(这里的parse_item是字符串)指定了跳转的函数def parse_item(self, response)
。
问题:CrawlSpider如何工作的?
首先由
start_requests
对start_urls
中的每一个url发起请求(make_requests_from_url
),这个请求会被parse接收。在Spider里面的parse需要我们定义,但CrawlSpider定义parse
去解析响应(self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)
)_parse_response根据有无
callback
,follow
和self.follow_links
执行不同的操作
1 2 3 4 5 6 7 8 9 10 11 |
def _parse_response(self, response, callback, cb_kwargs, follow=True): ##如果传入了callback,使用这个callback解析页面并获取解析得到的reques或item 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 ## 其次判断有无follow,用_requests_to_follow解析响应是否有符合要求的link。 if follow and self._follow_links: for request_or_item in self._requests_to_follow(response): yield request_or_item |
其中_requests_to_follow
又会获取link_extractor
(这个是我们传入的LinkExtractor)解析页面得到的link(link_extractor.extract_links(response))
,对url进行加工(process_links,需要自定义),对符合的link发起Request。使用.process_request
(需要自定义)处理响应。
问题:CrawlSpider如何获取rules?
CrawlSpider类会在__init__
方法中调用_compile_rules
方法,然后在其中浅拷贝rules
中的各个Rule
获取要用于回调(callback),要进行处理的链接(process_links)和要进行的处理请求(process_request)
1 2 3 4 5 6 7 8 9 10 11 12 |
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) |
那么Rule
是怎么样定义的呢?
1 2 3 4 5 6 7 8 9 10 11 12 |
class Rule(object): def __init__(self, link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=identity): self.link_extractor = link_extractor self.callback = callback self.cb_kwargs = cb_kwargs or {} self.process_links = process_links self.process_request = process_request if follow is None: self.follow = False if callback else True else: self.follow = follow |
因此LinkExtractor会传给link_extractor。
有callback的是由指定的函数处理,没有callback的是由哪个函数处理的?
由上面的讲解可以发现_parse_response
会处理有callback
的(响应)respons。
cb_res = callback(response, **cb_kwargs) or ()
而_requests_to_follow
会将self._response_downloaded
传给callback
用于对页面中匹配的url发起请求(request)。
r = Request(url=link.url, callback=self._response_downloaded)
如何在CrawlSpider进行模拟登陆
因为CrawlSpider和Spider一样,都要使用start_requests发起请求,用从Andrew_liu大神借鉴的代码说明如何模拟登陆:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
##替换原来的start_requests,callback为 def start_requests(self): return [Request("http://www.zhihu.com/#signin", meta = {'cookiejar' : 1}, callback = self.post_login)] def post_login(self, response): print 'Preparing login' #下面这句话用于抓取请求网页后返回网页中的_xsrf字段的文字, 用于成功提交表单 xsrf = Selector(response).xpath('//input[@name="_xsrf"]/@value').extract()[0] print xsrf #FormRequeset.from_response是Scrapy提供的一个函数, 用于post表单 #登陆成功后, 会调用after_login回调函数 return [FormRequest.from_response(response, #"http://www.zhihu.com/login", meta = {'cookiejar' : response.meta['cookiejar']}, headers = self.headers, formdata = { '_xsrf': xsrf, 'email': '1527927373@qq.com', 'password': '321324jia' }, callback = self.after_login, dont_filter = True )] #make_requests_from_url会调用parse,就可以与CrawlSpider的parse进行衔接了 def after_login(self, response) : for url in self.start_urls : yield self.make_requests_from_url(url) |
数据流程图
源码分析
最后贴上Scrapy.spiders.CrawlSpider的源代码,以便检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
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) |