scrapy爬虫之crwalspider模拟登陆

简介

前面的几篇博文都是使用默认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。

主要用来过滤由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": "yanggd1987@163.com",
                "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": "yanggd1987@163.com",
                                "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)
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页