简介
前面的几篇博文都是使用默认basic模版生成的spider,即scrapy.Spider。scrapy所有的spider都继承自scrapy.Spider,它默认使用start_requests()方法请求start_urls()中的url,并且默认使用pase()方法处理返回的response。我们需要注意的是scrapy开始运行后start_requests()方法只会被调用一次。
以上都是scrapy的基础知识,为什么再次强调呢?
因为我们本次会使用crawl模版生成crawlspider,在crawlspider中通过start_requests实现模拟登陆豆瓣,使用crawlspider的属性实现抓取豆瓣电影top250,和普通spider进行对比学习。
CrawlSpider
CrawlSpider是爬取一般网站常用的spider,适合于从爬取的网页中获取link并继续爬取的场景。除了从Spider继承过来的性外,其提供了一个新的属性rules,它是一个Rule对象列表,每个Rule对象定义了种义link的提取规则,如果多个Rule匹配一个连接,那么根据定义的顺序使用第一个。
例如:我们爬取豆瓣电影的top250,普通的spider是从指定页面开始抓取每个页面的item,然后再跳转到下一页,如此反复执行,直至next_page为空。而crawlspider是从两个维度横向和纵向进行,访问列表页和详细页,大体流程为:通过rule的正则表达式匹配列表页横向的next_page,提取link,来访问每个列表页,直至next_page为空;再通过rule的正则表达式纵向匹配列表页的每个item,提取link,访问详细页,然后通过指定的parse_XXXX方法提取需要的item。
CrawlSpider基于spider,除了具有spider所有属性的外,还具有以下自身的特性:
rules: 是Rule对象的集合,用于匹配目标网站并排除干扰
parse_start_url: 用来初始化start_url中response,必须要返回Item,Request等。
Rule
scrapy.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)
rules是Rule对象的集合,有几个参数:link_extractor、callback=None、cb_kwargs=None、follow=None、process_links=None、process_request=None。
link_extractor:
scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href', ), canonicalize=False, unique=True, process_value=None, strip=True)
allow:满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。
deny:与这个正则表达式(或正则表达式列表)不匹配的URL一定不提取。
allow_domains:会被提取的链接的domains。
deny_domains:一定不会被提取链接的domains。
restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接。还有一个类似的restrict_css。
callback
当link_extractor获取到链接时会调用该参数所指定的回调函数. 该回调函数接受一个response作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)。
callback参数注意:当编写爬虫规则时,请避免使用parse作为回调函数。因为CrawlSpider使用默认通过callback调用parse方法来实现其逻辑,如果您覆盖了parse方法,crawlspider将会运行失败。
为什么覆盖了parse方法,crawlspider将会运行失败?
因为scrapy首先由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执行不同的操作。
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(需要自定义)处理响应。
follow
指定了根据该规则从response提取的链接是否需要继续跟进。当callback为None, 默认值为True。
process_links
主要用来过滤由link_extractor获取到的链接。
process_request
主要用来过滤在rule中提取到的request。
CrawlSpider工作分析
以上通过对各参数了解后,我们分析下CrawlSpider如何工作。
1.CrawlSpider获取rules后如何工作?
CrawlSpider类会在init方法中调用_compile_rules方法,然后在其中浅拷贝rules中的各个Rule获取要用于回调(callback),要进行处理的链接(process_links)和要进行的处理请求(process_request)
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)
2.rules中的Rule如何工作?
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
从rules和Rule的源码,个人理解的访问流程为:
1.scrapy由start_requests对start_urls中的每一个url发起请求(make_requests_from_url),这个请求会被parse接收;_parse_response根据有无callback,follow和self.follow_links执行不同的操作。此时的callback,follow,self.follow_link的参数就是从rules中获得
2.rules是浅拷贝Rule,因此先执行Rule
Rule通过link_extractor提取匹配到的url,并获得process_links、process_request;最后判断是否follow,当callback为false,则follow为true
3.rules浅拷贝后,主要进行递归工作。当callback为false,也就是follow为true时,调用默认的pase方法继续进行匹配;当callback为true,也就是follow为fasle,则会调用我们指定的parse_XXXX方法进行处理数据。
同时这也是我们不能重写默认parse的原因。
CrawlSpider实现
1.crawlspider直接爬取豆瓣top250
#生成crawl模版的爬虫
scrapy genspider -t crawl movieTop250_crawlspider douban.com
#定义item
vim scrapy/douban/douban/items.py
import scrapy
from scrapy.loader.processors import Join, MapCompose, TakeFirst
class Movietop250(scrapy.Item):
rank = scrapy.Field()
title = scrapy.Field(output_processor=Join())
descripition = scrapy.Field()
link = scrapy.Field()
star = scrapy.Field(output_processor=Join(','))
quote = scrapy.Field()
#定义爬虫
vim movieTop250_crawlspider.py
# -*- coding: utf-8 -*-
#使用crawlspider抓取豆瓣电影top250
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from douban.items import Movietop250
from scrapy.loader import ItemLoader
class Movietop250CrawlspiderSpider(CrawlSpider):
name = 'movieTop250_crawlspider'
allowed_domains = ['douban.com']
start_urls = ['http://movie.douban.com/top250']
rules = (
#列表页匹配前第三页
Rule(LinkExtractor(allow='start=[0-5][0-5]&filter=')),
#详细页提取item
Rule(LinkExtractor(allow='/subject/\d+/'), callback='parse_item'),
)
def parse_item(self, response):
loader = ItemLoader(item=Movietop250(), selector=response.xpath('//div[@id="content"]'))
#loader.add_xpath('rank', 'h1/span[@property="v:itemreviewed"]/text()')
#yield loader.load_item()
loader = ItemLoader(item=Movietop250(), selector=response)
movie = loader.nested_xpath('//div[@id="content"]')
movie.add_xpath('rank', 'div[@class="top250"]/span[@class="top250-no"]/text()')
movie.add_xpath('title', 'h1/span[@property="v:itemreviewed"]/text()')
yield loader.load_item()
提取内容下:
#开始爬取
scrapy crawl movieTop250_crawlspider -o movieTop250_crawlspider.json
#查看数据
vim movieTop250_crawlspider
[
{"rank": ["No.1"], "title": "肖申克的救赎 The Shawshank Redemption"},
{"rank": ["No.25"], "title": "星际穿越 Interstellar"},
{"rank": ["No.24"], "title": "无间道 無間道"},
{"rank": ["No.23"], "title": "当幸福来敲门 The Pursuit of Happyness"},
{"rank": ["No.22"], "title": "天堂电影院 Nuovo Cinema Paradiso"},
{"rank": ["No.21"], "title": "触不可及 Intouchables"},
{"rank": ["No.19"], "title": "乱世佳人 Gone with the Wind"},
{"rank": ["No.18"], "title": "楚门的世界 The Truman Show"},
{"rank": ["No.17"], "title": "龙猫 となりのトトロ"},
{"rank": ["No.16"], "title": "教父 The Godfather"},
{"rank": ["No.15"], "title": "大话西游之大圣娶亲 西遊記大結局之仙履奇緣"},
{"rank": ["No.14"], "title": "放牛班的春天 Les choristes"},
]
.......
crawlspider直接爬取很简单,根据rules中的第一条Rule先提取每个列表页匹配的正则,此时调用默认的parse;然后第二条Rule会提取详细页的item,此时调用我们重写parse_item获得我们需要的字段。
2.crawlspider模拟登陆爬取豆瓣电影top250
crawlspider模拟登陆需要通过设置 meta={“cookiejar”:1}保持会话,默认crawlspider没有这样的设置,但是不要忘了crawlspider基于spider,因此也具有spider的特性。
关键点在于spider和crawlspider怎么衔接?
还记得我们我们为什么反复强调start_requests():scrapy开始爬取并且用于创建Request的方法。在爬虫开始运行后,scrapy会调用start_requests(),其内部会调用make_requests_from_url()方法从start_urls列表中对每一个链接生成Request。我们只需要把要开始爬取的链接放入start_urls中即可,也可以不定义start_urls,重新实现start_requests,生成自定义的Request对象,如FormRequest,然后获取到Response后传递给自定义的回调函数处理。start_requests在整个爬虫运行过程中只会执行一次。
因此 ,我们在通过start_requests()实现登陆后,然后调用make_requests_from_url()方法从start_urls中开始request后,就会进入crawlspider的rules,开始进行匹配抓取了。此时实现了spider和crawlspider的衔接。
# -*- coding: utf-8 -*-
#crawspider登陆豆瓣,爬取豆瓣电影top250
import scrapy
import urllib
from PIL import Image
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy.loader import ItemLoader
from douban.items import Movietop250
class Movietop250LoginCrawlspiderSpider(CrawlSpider):
name = 'movieTop250_login_crawlspider'
allowed_domains = ['douban.com']
start_urls = ['https://movie.douban.com/top250']
headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0"}
rules = (
Rule(LinkExtractor(allow='start=[0-5][0-5]&filter=')),
Rule(LinkExtractor(allow='/subject/\d+/'), callback='parse_item'),
)
def start_requests(self):
'''
重写start_requests,请求登录页面
'''
return [scrapy.FormRequest("https://accounts.douban.com/login", headers=self.headers, meta={"cookiejar":1}, callback=self.parse_before_login)]
def parse_before_login(self, response):
'''
登录表单填充,查看验证码
'''
print("登录前表单填充")
captcha_id = response.xpath('//input[@name="captcha-id"]/@value').extract_first()
captcha_image_url = response.xpath('//img[@id="captcha_image"]/@src').extract_first()
if captcha_image_url is None:
print("登录时无验证码")
formdata = {
"source": "index_nav",
"form_email": "[email protected]",
"form_password": "******",
}
else:
print("登录时有验证码")
save_image_path = "/home/yanggd/python/scrapy/douban/douban/spiders/captcha.jpeg"
#将图片验证码下载到本地
urllib.urlretrieve(captcha_image_url, save_image_path)
#打开图片,以便我们识别图中验证码
try:
im = Image.open('captcha.jpeg')
im.show()
except:
pass
#手动输入验证码
captcha_solution = raw_input('根据打开的图片输入验证码:')
formdata = {
"source": "None",
"redir": "https://www.douban.com",
"form_email": "[email protected]",
"form_password": "******",
"captcha-solution": captcha_solution,
"captcha-id": captcha_id,
"login": "登录",
}
print("登录中")
#提交表单
return scrapy.FormRequest.from_response(response, meta={"cookiejar":response.meta["cookiejar"]}, headers=self.headers, formdata=formdata, callback=self.parse_after_login)
def parse_after_login(self, response):
'''
验证登录是否成功,通过make_requests_from_url对接crawlspider
'''
account = response.xpath('//a[@class="bn-more"]/span/text()').extract_first()
if account is None:
print("登录失败")
else:
print(u"登录成功,当前账户为 %s" %account)
#在此通过make_requests_from_url进入rules
for url in self.start_urls :
yield self.make_requests_from_url(url)
def parse_item(self, response):
loader = ItemLoader(item=Movietop250(), selector=response.xpath('//div[@id="content"]'))
#loader.add_xpath('rank', 'h1/span[@property="v:itemreviewed"]/text()')
#yield loader.load_item()
loader = ItemLoader(item=Movietop250(), selector=response)
movie = loader.nested_xpath('//div[@id="content"]')
movie.add_xpath('rank', 'div[@class="top250"]/span[@class="top250-no"]/text()')
movie.add_xpath('title', 'h1/span[@property="v:itemreviewed"]/text()')
yield loader.load_item()
以上代码处理流程是这样的:
1.默认从start_requests()开始,虽然我们定义了start_urls,但是这是给crawlspider抓取的起始页;start_requests()使用的是自己设置的豆瓣登陆;
2.调用parse_before_login()方法填充并提交表单;
3.调用parse_after_logoin()方法验证登陆,再通过调用make_requests_from_url()方法开始从start_urls中的豆瓣电影top250页面访问;
4.进入start_urls中的页面后,crawlspider就开始使用rules按我们设置的规则进行递归抓取;
5.最后在详情页调用parse_item抓取我们想要的字段;
注意:此爬虫我们设置了只爬取前3页,爬取太多可能被ban;
#开始爬取
scrapy crawl movieTop250_login_crawlspider -o movieTop250_login_crawlspider.json
#查看数据
vim movieTop250_login_crawlspider
[
{"rank": ["No.1"], "title": "肖申克的救赎 The Shawshank Redemption"},
{"rank": ["No.25"], "title": "星际穿越 Interstellar"},
{"rank": ["No.24"], "title": "无间道 無間道"},
{"rank": ["No.23"], "title": "当幸福来敲门 The Pursuit of Happyness"},
{"rank": ["No.22"], "title": "天堂电影院 Nuovo Cinema Paradiso"},
{"rank": ["No.21"], "title": "触不可及 Intouchables"},
{"rank": ["No.19"], "title": "乱世佳人 Gone with the Wind"},
{"rank": ["No.18"], "title": "楚门的世界 The Truman Show"},
{"rank": ["No.16"], "title": "教父 The Godfather"},
{"rank": ["No.15"], "title": "大话西游之大圣娶亲 西遊記大結局之仙履奇緣"},
{"rank": ["No.14"], "title": "放牛班的春天 Les choristes"},
{"rank": ["No.13"], "title": "忠犬八公的故事 Hachi: A Dog's Tale"},
{"rank": ["No.17"], "title": "龙猫 となりのトトロ"},
]
......
scrapy shell调试
scrapy shell "http:/movie.douban.com/top250"
from scrapy.linkextractors import LinkExtractor
item = LinkExtractor(allow=(allow='/subject/\d+/')).extract_links(response)