Python自动化开发学习-Scrapy

发布时间:2019-09-12 08:00:01编辑:auto阅读(1896)

    讲师博客:https://www.cnblogs.com/wupeiqi/p/6229292.html
    中文资料(有示例参考):http://www.scrapyd.cn/doc/

    项目准备

    Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。使用之前有一个类似django的创建项目以及目录结构的过程。

    Scrapy 安装

    使用pip安装(windows会有问题):

    pip3 install scrapy

    装不上主要是因为依赖的模块Twisted安装不上,所以得先安装Twisted,并且不能用pip直接下载安装。先去下载Twisted的whl安装文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    然后使用pip本地安装:

    pip install E:\Downloads\Twisted-18.9.0-cp36-cp36m-win_amd64.whl
    pip install -i https://mirrors.aliyun.com/pypi/simple/ scrapy
    pip install -i https://mirrors.aliyun.com/pypi/simple/ pywin32

    Scrapy 组件

    Scrapy主要包括了以下组件:

    • 引擎(Scrapy):
      用来处理整个系统的数据流处理, 触发事务(框架核心)
    • 调度器(Scheduler):
      用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
    • 下载器(Downloader):
      用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
    • 爬虫(Spiders):
      爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
    • 项目管道(Pipeline):
      负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
    • 下载器中间件(Downloader Middlewares):
      位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
    • 爬虫中间件(Spider Middlewares):
      介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
    • 调度中间件(Scheduler Middewares):
      介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

    Python自动化开发学习-Scrapy

    工作流程:
    绿线是数据流向,引擎是整个程序的入口。首先从初始 URL 开始(这步大概是引擎把初始URL加到调度器),Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,Spider 分析出来的结果有两种:一种是需要进一步抓取的链接,例如“下一页”的链接,这些东西会被传回 Scheduler ;另一种是需要保存的数据,它们则被送到 Item Pipeline 那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。
    另外,引擎和其他3个组件直接有通道。在数据流动的通道里还可以安装各种中间件,进行必要的处理。

    Scrapy 项目结构

    启动项目
    打开终端进入想要存储 Scrapy 项目的目录,然后运行 scrapy startproject (project name)。创建一个项目:

    > scrapy startproject PeppaScrapy

    执行完成后,会生成如下的文件结构:

    ProjectName/
    ├── ProjectName
    │   ├── __init__.py
    │   ├── items.py
    │   ├── middlewares.py
    │   ├── pipelines.py
    │   ├── settings.py
    │   └── spiders
    │       └── __init__.py
    └── scrapy.cfg

    文件说明

    • scrapy.cfg : 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
    • items.py : 设置数据存储模板,用于结构化数据,如:Django的Model
    • pipelines : 数据处理行为,如:一般结构化的数据持久化
    • settings.py : 配置文件,如:递归的层数、并发数,延迟下载等
    • spiders : 爬虫目录,如:创建文件,编写爬虫规则

    关于配置文件,需要的时候可以先去下面的地址查,版本不是最新的,不过是中文。
    https://www.jianshu.com/p/df9c0d1e9087

    创建爬虫应用
    先切换到项目目录,在执行grnspider命令 scrapy genspider [-t template] (name) (domain) 。比如:

    > cd PeppaScrapy
    > scrapy genspider spider_lab lab.scrapyd.cn

    效果就是在spiders目录下,创建了一个spider_lab.py的文件。这里没有用-t参数指定模板,就是用默认模板创建的。其实不用命令也行了,自己建空文件,然后自己写也是一样的。
    可以使用-l参数,查看有哪些模板:

    > scrapy genspider -l
    Available templates:
      basic
      crawl
      csvfeed
      xmlfeed

    然后再用-d参数,加上上面查到的模板名,查看模板的内容:

    > scrapy genspider -d basic
    # -*- coding: utf-8 -*-
    import scrapy
    
    class $classname(scrapy.Spider):
        name = '$name'
        allowed_domains = ['$domain']
        start_urls = ['http://$domain/']
    
        def parse(self, response):
            pass

    爬取页面

    把之前的创建的应用的文件修改一下,简单完善一下parse方法:

    import scrapy
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'spider_lab'
        allowed_domains = ['lab.scrapyd.cn']
        start_urls = ['http://lab.scrapyd.cn/']
    
        def parse(self, response):
            print(response.url)
            print(response.body.decode())

    查看应用列表:

    > scrapy list
    spider_lab

    运行单独爬虫应用,这里加上了--nolog参数,避免打印日志的干扰:

    > scrapy crawl spider_lab --nolog

    在python里启动爬虫

    每次都去命令行打一遍命令也很麻烦,也是可以直接写python代码,执行python来启动的。把下面的代码加到引用文件的最后:

    if __name__ == '__main__':
        from scrapy import cmdline
        log_level = '--nolog'
        name = SpiderLabSpider.name
        cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())

    其实就是提供了在python里调用命令行执行命令的方法。之后,还可以写一个main.py放到项目根目录下,写上启动整个项目的命令。

    Windows 编码问题

    有可能会遇到编码问题,不过我的windows没问题,如果遇到了,试一下下面的方法:

    import io
    import sys
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')

    踩坑(爬虫Robots协议)

    Robots协议就是每个网站对于来到的爬虫所提出的要求。并非强制要求遵守的协议,只是一种建议。
    默认scrapy遵守robot协议。我在爬 http://dig.chouti.com/ 的时候遇到了这个问题。把 --nolog 参数去掉,查看错误日志,有如下的信息:

    [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dig.chouti.com/robots.txt> (referer: None)
    [scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET http://dig.chouti.com/>

    先去下载robots.txt文件,然后根据文件的建议,就禁止继续爬取了。可以直接浏览器输入连接查看文件内容:

    User-agent: *
    Allow: /link/
    Disallow: /?
    Disallow: /*?
    Disallow: /user
    Disallow: /link/*/comments
    Disallow: /admin/login
    
    # Sitemap files
    Sitemap: https://dig.chouti.com/sitemap.xml

    你要守规矩的的话,就只能爬 https://dig.chouti.com/link/xxxxxxxx 这样的url,一个帖子一个帖子爬下来。
    如果可以选择不遵守协议,那么就在爬的时候把这个设置设为False。全局的设置在settings.py文件里:

    # Obey robots.txt rules
    ROBOTSTXT_OBEY = True

    也可以只对一个应用修改设置:

    import scrapy
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'chouti'
        allowed_domains = ['chouti.com']
        start_urls = ['http://dig.chouti.com/']
        custom_settings = {'ROBOTSTXT_OBEY': False}
    
        def parse(self, response):
            print(response.url)
            print(response.encoding)
            print(response.text)
    
    if __name__ == '__main__':
        from scrapy import cmdline
        log_level = '--nolog'
        name = SpiderLabSpider.name
        cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())

    自定义请求头

    上面踩坑的过程中,一度以为是请求头有问题,已定义请求头的方法也是设置settings.py文件,里面有一个剩下的默认配置:

    # Override the default request headers:
    #DEFAULT_REQUEST_HEADERS = {
    #   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    #   'Accept-Language': 'en',
    #}

    默认都注释掉了,你可以在这里为全局加上自定义的请求头,当然也可以只为单独的应用配置:

    import scrapy
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'test'
        allowed_domains = ['chouti.cn']
        start_urls = ['http://dig.chouti.com/']
        # 这个网站会屏蔽User-Agent里包含python的请求
        custom_settings = {'ROBOTSTXT_OBEY': False,
                           'DEFAULT_REQUEST_HEADERS': {'User-Agent': 'python'},
                           }
    
        def parse(self, response):
            print(response.request.headers)  # 这个是请求头
            print(response.headers)  # 这个是响应头
    
    if __name__ == '__main__':
        from scrapy import cmdline
        log_level = ''
        name = SpiderLabSpider.name
        cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())

    xpath 选择器

    使用xpaht选择器可以提取数据,另外还有CSS选择器也可以用。
    XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。对 XPath 的理解是很多高级 XML 应用的基础。

    scrapy 里的 xpath

    解析页面内容会用到Selector这个类,下面只贴出parse回调函数里的代码:

    from scrapy.selector import Selector
    
        def parse(self, response):
            title1 = response.xpath('//title')
            print('title1', title1)
            title2 = Selector(response).xpath('//title')
            print('title2', title2)

    上面的两种用法是一样的,通过response对象也可以直接调用xpath方法。这里说明了xpath方法是Selector这个类提供的。另外用方法二还有一个好处,就是因为之后需要调用Selector类里的方法,这样显示的声明Selector类之后,编辑器可以找到类似的方法,给出各种提示。直接用response调用,就没有这种便利了。
    另外还有一个XmlXPathSelector类,作用和Selector类差不多,可能是就版本使用的类。

    表达式

    常用的表达式:

    • node_name : 选取从节点的所有子节点。就是标签名,比如上面的title
    • // : 匹配当前节点下的所有节点,不考虑位置。就是选择下面的子子孙孙
    • / : 匹配当前节点下的子节点,只往下找一层,就是找儿子。类似文件路径
    • . : 选择当前节点。类似文件路径
    • .. : 选择当前节点的父节点。类似文件路径
    • @ : 选取属性

    提取属性
    提取属性的话,也是先定位到标签的范围,然后最后@属性名称,拿到所有对应的属性。另外@*可以拿到所有的属性。要当某个标签下的属性,就在标签名之后/@就好了:

    Selector(response).xpath('//@href')  # 提取所有的href属性
    Selector(response).xpath('//ol[@class="page-navigator"]//@href')  # ol.page-navigator下的所有的href属性
    Selector(response).xpath('//head/meta/@*').extract()  # head>meta 标签了所有的属性
    Selector(response).xpath('//*[@id="body"]/div/@class')  # id为body的标签的下一级标签里的class属性

    查找标签,限定属性
    使用这样的表达式:标签[@属性名='属性值'] ,另外还能用not(),注意要用小括号把取反的内容包起来:

    Selector(response).xpath('//div[@id="body"]//span[@class="text"]')  # 只要 span.text 的span标签
    Selector(response).xpath('//div[@id="body"]//span[not(@class="text")]')  # 没有text这个class的span标签
    Selector(response).xpath('//meta[@name]')  # 有name属性的meta
    Selector(response).xpath('//meta[not(@name)]')  # 没有name属性meta

    提取值
    xpath方法返回的是个对象,这个对象还可以无限次的再调用xpath方法。拿到最终的对象之后,我们需要获取值,这里有 extract() 和 extract_first() 这两个方法。因为查找的结果可能是多个值,extract方法返回列表,而extract_first方法直接返回值,但是是列表是第一个元素的值。

    提取文字
    表达式:/text() 可以把文字提取出来:

        def parse(self, response):
            tags = Selector(response).xpath('//ul[@class="tags-list"]//a/text()').extract()
            print(tags)  # 这样打印效果不是很好
            for tag in tags:
                print(tag.strip())

    还有个方法,可以提取整段文字拼到一起。表达式:string() :

    Selector(response).xpath('string(//ul[@class="tags-list"]//a)').extract()  # 这样没拿全
    Selector(response).xpath('string(//ul[@class="tags-list"])').extract()  # 这样才拿全了

    上面第一次没拿全,某个a标签下的文字就是一段。string()表达式看来值接收一个值,如果传的是个列表,可能就只操作第一个元素。
    在我们商品详情、小说内容的时候可能会比较好用。

    匹配class的问题
    xpath中没有提供对class的原生查找方法。因为class里是可以包含多个值的。比如下面的这个标签:

    <div class="test main">Test</div>

    下面的表达式是无法匹配到的:

    response.xpath('//div[@class="test"]')

    要匹配到,你得写死:

    response.xpath('//div[@class="test main"]')

    但是这样显然是不能接受的,如果还有其他test但是没出main的标签就匹配不上了。
    contains 函数 (XPath),检查第一个参数字符串是否包含第二个参数字符串。用这个函数就能做好了

    response.xpath('//div[contains(@class, "test")]')

    这样又有新问题了,如果有别的class名字比如:test1、mytest,这种也都会被上面的方法匹配上。
    concat 函数 (XPath),返回参数的串联。就是字符串拼接,contains的两个参数的两边都加上空格,就能解决上面的问题。之所以要引入concat函数时因为,后面的字符串可以手动在两边加上空格,但是@class是变量,这个也不能用加号,就要用这个函数做拼接:

    response.xpath('//div[contains(concat(" ", @class, " "), " test ")]')

    normalize-space 函数 (XPath),返回去掉了前导、尾随和重复的空白的参数字符串。上面已经没问题了。不过还不够完美。在拼接@class之前,先把两边可能会出现的其他空白字符给去掉,可能会有某些操作需要改变一下class,但是又不要对这个class有任何实际的影响。总之这个是最终的解决方案:

    response.xpath('//div[contains(concat(" ", normalize-space(@class), " "), " test ")]')

    这里已经引出了好几个函数了,还有更多别的函数,需要的时候再查吧。

    正则匹配
    xpath也是可以用正则匹配的,用法很简单 re:test(x, y) 。第一个参数用@属性比较多,否则就是正则匹配标签了,就和纯的正则匹配似乎没什么差别了。

    Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')

    xpath 与 css定位方式的比较

    https://www.cnblogs.com/tina-cherish/p/7127812.html
    xpath很强大,但是不支持原生的class,不过上面已经给了比较严谨的解决方案了。
    css有部分功能无法实现。比如不能向上找,只能匹配当前层级,要通过判断子元素来确定当前元素是否匹配就不行。这种情况使用xpath的话,中括号里可以在嵌套中括号的。
    不过css感觉更直观,也已经没什么学习成本了。

    实战

    登录抽屉并点赞。边一步一步实现,边补充用到的知识点。

    获取首页的内容

    import scrapy
    from scrapy.selector import Selector
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'chouti'
        allowed_domains = ['chouti.com']
        start_urls = ['http://dig.chouti.com/']
        custom_settings = {'ROBOTSTXT_OBEY': False}
    
        def parse(self, response):
            items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
            for item in items:
                news = item.xpath(
                    './div[@class="news-content"]'
                    '//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
                    '/text()'
                ).extract()[-1]
                print(news.strip())
    
    if __name__ == '__main__':
        from scrapy import cmdline
        log_level = '--nolog'
        name = SpiderLabSpider.name
        cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())

    这里爬取的只是首页的内容

    爬取所有页的内容

    现在要获取所有分页的url,然后继续爬取。下面就是在parse回调函数后面增加了一点代码是做好了。不过现在的代码还不完善,会无休止的爬取下去,先不要运行,之后还要再改:

    import urllib.parse
    
        def parse(self, response):
            items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
            for item in items:
                news = item.xpath(
                    './div[@class="news-content"]'
                    '//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
                    '/text()'
                ).extract()[-1]
                print(news.strip())
            # 不找下一页,而是找全部的页,这样会有去重的问题,就是要这个效果
            pages = Selector(response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
            print(pages)
            url_parse = urllib.parse.urlparse(response.url)
            for page in pages:
                url = "%s://%s%s" % (url_parse.scheme, url_parse.hostname, page)
                yield scrapy.Request(url=url)

    这里做的事情就是当从前也分析了分页的信息,把分页信息生成新的url,然后再给调度器继续爬取。
    这里用的 scrapy.Request() ,实际上是应该要通过 from scrapy.http import Request 导入再用的。不过这里并不需要导入,并且只能能在scrapy下调用。因为在 scrapy/__init__.py 里有导入这个模块了。并且这里已经不是系统第一次调用这个类了,程序启动的时候,其实就是跑了下面的代码把 start_urls 的地址开始爬取网页了:

    for url in self.start_urls:
        yield Request(url, dont_filter=True)

    这段代码就是在当前类的父类 scrapy.Spider 里的 start_requests 方法里面。

    爬取深度

    爬取深度,允许抓取任何网站的最大深度。如果为零,则不施加限制。
    这个是可以在配置文件里设置的。默认的配置里没有写这条,并且默认值是0,就是爬取深度没有限制。所以就会永不停止的爬取下去。实际上不会无休止,似乎默认就有去重的功能,爬过的页面不会重复爬取。所以不设置爬取深度,就能把所有的页面都爬下来了
    这里要讲的是爬取深度的设置,所以和其他设置一样,可以全局的在settings.py里设置。也可以现在类的公用属性 custom_settings 这个字典里:

        custom_settings = {
            'ROBOTSTXT_OBEY': False,
            'DEPTH_LIMIT': 1,
        }

    这个深度可以在返回的response参数里找到,在meta这个字典里:response.meta['depth']

    去重规则

    默认有下面2条配置:

    DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
    DUPEFILTER_DEBUG = False

    去重的功能默认就是在 'scrapy.dupefilters.RFPDupeFilter' 这个类里做的。这个类有个父类 BaseDupeFilter 帮我们定义好了接口,我们可以写一个自己的类自定义去重规则,继承 BaseDupeFilter 实现里面的方法:

    from scrapy.dupefilters import BaseDupeFilter
    
    class MyFilter(BaseDupeFilter):
    
        def __init__(self):
            # 去重可以用上集合
            # 在request_seen方法里判断这个set,操作这个set
            self.visited_url = set()
    
        @classmethod
        def from_settings(cls, settings):
            """初始化时调用的方法
            返回一个实例,作用就是可以调用配置的信息生成实例
            实例化时使用:obj = MyFilter.from_settings()
            所以不要这样实例化:obj = MyFilter()
            什么都不写,上面两重方法生成的实例是一样的
            """
            return cls()
    
        def request_seen(self, request):
            """过滤规则
            检测当前请求是否需要过滤(去重)
            返回True表示需要过滤,返回False表示不用过滤
            """
            return False
    
        def open(self):  # can return deferred
            """开始爬虫时,调用一次
            比如要记录到文件的,在这里检查和重建记录文件
            """
            pass
    
        def close(self, reason):  # can return a deferred
            """结束爬虫时,调用一次
            这里可以把之前的记录文件close掉
            """
            pass
    
        def log(self, request, spider):  # log that a request has been filtered
            """日志消息的记录或打印可以写在这里"""
            pass
    

    现在知道了,默认就是有去重规则的。所以上面爬取所有页面的代码并并不会无休止的执行下去,而是可以把所有页面都爬完的。

    启动和回调函数

    程序启动后,首先会调用父类 scrapy.Spider 里的 start_requests 方法。我们也可以不设置 start_urls 属性,然后自己重构 start_requests 方法。启动的效果是一样的:

        # start_urls = ['http://lab.scrapyd.cn/']
    
        def start_requests(self):
            urls = ['http://lab.scrapyd.cn/']
            for url in urls:
                yield scrapy.Request(url=url, dont_filter=True)

    另外就是这个 scrapy.Request 类,回调函数 parse 方法最后也是调用这个方法类。这里还有一个重要的参数 callback 。默认不设置时 callback=parse ,所以可以手动设置callback参数,使用别的回调函数。或者准备多个回调函数,每次调度的时候设置不同额callback。比如第一次用默认的,之后在 parse 方法里再调用的时候,设置 callback=func 使用另外的回调函数。

    Cookie

    默认就是开启Cookie的,所以其实我们并不需要操作什么。
    配置的 COOKIES_ENABLED 选项一旦关闭,则不会有Cookie了,别处再怎么设置也没用。
    可以用meta参数,为请求单独设置cookie:

    yield scrapy.Request(url, self.login, meta={'cookiejar': True})

    不过如果要为请求单独设置的话,就得为每个请求都显示的声明。否则不写,就是认为是不要cookie。meta可以有如下设置:

    meta={'cookiejar': True}  # 使用Cookie
    meta={'cookiejar': False}  # 不使用Cookie,也就写在第一个请求里。之后的请求不设置就是不使用Cookie
    meta={'cookiejar': response.meta['cookiejar']}  # 使用上一次的cookie,上一次必须是True或者这个,否则会有问题

    手动设置cookie值
    Request 实例化的时候有 cookies 参数,直接传字典进去就可以了。

    获取cookie的值
    并没有cookie这个专门的属性。本质上cookie就是headers里的一个键值对,用下面的方法去headers里获取:

    response.request.headers.getlist('Cookie')  # 请求的Cookie
    response.headers.getlist('Set-Cookie')  # 响应的Cookie

    登录抽屉并点赞

    最后就是综合应用了。登录需要Cookies的操作。不过其实什么都不做就可以了,默认方法就能把Cookies操作好。
    然后就是从打开页面、完成登录、到最后点赞,需要发多次的请求,然后每次请求返回后所需要做的操作也是不一样的,这里就需要准备多个回调函数,并且再发起请求的时候指定回调函数。代码如下:

    import scrapy
    from scrapy.selector import Selector
    from utils.base64p import b64decode_str  # 自己写的从文件读密码的方法,不是重点
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'chouti_favor'
        custom_settings = {
            'ROBOTSTXT_OBEY': False,
        }
    
        def start_requests(self):
            url = 'http://dig.chouti.com/'
            yield scrapy.Request(url, self.login)
    
        def login(self, response):
            # 避免把密码公开出来,去文件里拿,并且做了转码,这不是这里的重点
            with open('../../utils/password') as f:
                auth = f.read()
                auth = auth.split('\n')
            post_dict = {
                'phone': '86%s' % auth[0],  # 从请求正文里发现,会在手机号前加上86
                'password': b64decode_str(auth[1]),  # 直接填明文的用户名和密码也行的
            }
            yield scrapy.FormRequest(
                url='http://dig.chouti.com/login',
                formdata=post_dict,
                callback=self.check_login,
            )
    
        def check_login(self, response):
            print(response.request.headers.getlist('Cookie'))
            print(response.headers.getlist('Set-Cookie'))
            print(response.text)
            yield scrapy.Request(
                url='http://dig.chouti.com/',
                dont_filter=True,  # 这页之前爬过了,如果不关掉过滤,就不会再爬了
            )
    
        def parse(self, response):
            items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
            do_favor = True
            for item in items:
                news = item.xpath(
                    './div[@class="news-content"]'
                    '//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
                    '/text()'
                ).extract()[-1]
                print(news.strip())
                # 点赞,做个判断,只赞第一条
                if do_favor:
                    do_favor = False
                    linkid = item.xpath('./div[@class="news-content"]/div[@share-linkid]/@share-linkid').extract_first()
                    yield scrapy.Request(
                        url='https://dig.chouti.com/link/vote?linksId=%s' % linkid,
                        method='POST',
                        callback=self.favor,
                    )
    
        def favor(self, response):
            print("点赞", response.text)
    
    if __name__ == '__main__':
        from scrapy import cmdline
        log_level = '--nolog'
        name = SpiderLabSpider.name
        cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())

    注意:首页的地址 http://dig.chouti.com 一共访问了两次。第二次如果不把 dont_filter 设为True,关闭过滤,就不会再去爬了。当然也可以第一次爬完之后,就保存在变量里,等登录后再从这个返回开始之后的处理。
    上面的POST请求,用到了 FormRequest 这个类。这个类继承的是 Request 。里面主要就是把字典拼接成请求体,设置一下请求头的 Content-Type ,默认再帮我们把 method 设为 POST 。也是可以继续用 Request 的,就是把上面的3个步骤自己做了。主要是请求体,大概是按下面这样拼接一下传给body参数:

    body='phone=86151xxxxxxxx&password=123456&oneMonth=1',

    格式化处理

    之前只是简单的处理,所以在parse方法中直接处理。对于想要获取更多的数据处理,则可以利用Scrapy的items将数据格式化,然后统一交由pipelines来处理。
    回顾一下 Scrapy 组件和工作流程,项目管道(Pipeline) 组件负责这个工作。

    items

    先要编辑一下 items.py 里的类,默认会帮我们生成一个类,并有简单的注释。必须要处理2个数据 title 和 href ,则改写 items.py 如下:

    import scrapy
    
    class PeppascrapyItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        title = scrapy.Field()
        href = scrapy.Field()

    然后去修改之前的 parse 方法,导入上面的类,把要处理的数据传递进去生成实例,然后 yield :

    from PeppaScrapy.items import PeppascrapyItem
    
    class SpiderLabSpider(scrapy.Spider):
        name = 'spider_lab'
        allowed_domains = ['lab.scrapyd.cn']
        start_urls = ['http://lab.scrapyd.cn/']
    
        def parse(self, response):
            print(response.url)
            items = Selector(response).xpath(
                '//div[@id="body"]//div[@id="main"]/div[@class="quote post"]')
            for item in items:
                title = item.xpath('./span[@class="text"]/text()').extract_first()
                href = item.xpath('./span/a/@href').extract_first()
                yield PeppascrapyItem(title=title, href=href)

    上面这段代码,只需要注意最后3行。把要保存的数据用items.py里的类实例化后,yield返回。
    回顾下流程,之前yield返回给 scrapy.Request ,就是把数据返回给调度器继续继续爬取
    这里yield返回给 scrapy.Item ,就是 Item Pipeline 里的 Item 进入数据的处理。
    在 Item 里只是把数据传递出来,数据的处理则在 Pipeline 里。
    如果有多处数据要返回,则可以自定义多个 scrapy.Item 类,来做数据的格式化处理。

    Pipline

    还有一个 pipelines.py 文件,默认里面只有一个 return ,但是传入2个参数 item 和 spider,先打印看看:

    class PeppascrapyPipeline(object):
        def process_item(self, item, spider):
            print(item)
            print(spider)
            return item

    只是编写处理方法还不够,这个方法需要注册。在settings.py文件里,默认写好了注册的方法,只需要把注释去掉。ITEM_PIPELINES 的 key 就是要注册的方法,而 value 则是优先级。理论上字典没有顺序,优先级小的方法先执行:

    ITEM_PIPELINES = {
       'PeppaScrapy.pipelines.PeppascrapyPipeline': 300,
    }

    最后返回的item是个字典,我们报错的变量名是key,值就是value。而spider则是这个爬虫 scrapy.Spider 对象。
    执行多个操作
    这里一个类就是执行一个操作,如果对返回的数据要有多次操作,也可以多定义几个类,然后注册上即可。
    每次操作的item,就是上一次操作最后 return item 传递下来的。第一次操作的item则是从 scrapy.Item 传过来的。所以也可以对item进行处理,然后之后的操作就是在上一次操作对item的修改之上进行的。所以也可以想return什么就return什么,就是给下一个操作处理的数据。
    绑定特定的爬虫
    Pipline并没有和特定的爬虫进行绑定,也就是所有的爬虫都会依次执行所有的Pipline。对于特定爬虫要做得特定的操作,可以在process_item方法里通过参数spider的spider.name进行判断。

    DropItem

    接着讲上面的执行多个操作。如果在某个地方要终止之后所有的操作,则可以用 DropItem 。用法如下:

    from scrapy.exceptions import DropItem
    
    class PeppascrapyPipeline(object):
        def process_item(self, item, spider):
            print(item)
            raise DropItem()

    这样对这组数据的操作就终止了。一般应该把这句放在某个条件的分支里。

    初始化操作

    Pipeline 这个类里,还可以定义更多方法。除了上面的处理方法,还有另外3个方法,其中一个是类方法。所有的方法名都不能修改,具体如下:

    class PeppascrapyPipeline(object):
    
        def __init__(self, value):
            self.value = value
    
        def process_item(self, item, spider):
            """操作并进行持久化"""
            print(item)
            # 表示将item丢弃,不会被后续pipeline处理
            raise DropItem()
            # print(spider)
            # return item 给后续的pipeline继续处理
            # return item
    
        @classmethod
        def from_crawler(cls, crawler):
            """初始化时候,用于创建pipeline对象"""
            val = crawler.settings.get('BOT_NAME')
            # getint 方法可以直接获取 int 参数
            # val = crawler.settings.getint('DEPTH_LIMIT')
            return cls(val)
    
        def open_spider(self,spider):
            """爬虫开始执行时,调用"""
            print('START')
    
        def close_spider(self,spider):
            """爬虫关闭时,被调用"""
            print('OVER')

    类方法 from_crawler 是用于创建pipeline对象的。主要是接收了crawler参数,可以获取到settings里的参数然后传给构造方法。比如这里获取了settings.py里的值传给了对象。
    另外2个方法 open_spider 和 close_spider ,是在爬虫开始和关闭时执行的。即使爬虫有多次返回,处理方法要调用多次,但是这2个方法都只会调用一次。这2个方法是在爬虫 scrapy.Spider 开始和关闭的时候各执行一次的。而不是第一次返回数据处理和最后一次数据处理完毕。
    打开文件的操作
    以写入文件为例,写入一段数据需要3步:打开文件,写入,关闭文件。如果把这3不都写在 process_item 方法里,则会有多次的打开和关闭操作。正确的做法是,打开文件在 open_spider 方法里执行,写入还是在 process_item 方法里每次返回都可以写入,最后在 close_spider 方法里关闭文件。

    中间件

    默认有一个 middlewares.py 文件,里面默认创建了2个类,分别是爬虫中间件和下载中间件

    爬虫中间件

    class PeppascrapySpiderMiddleware(object):
        # Not all methods need to be defined. If a method is not defined,
        # scrapy acts as if the spider middleware does not modify the
        # passed objects.
    
        @classmethod
        def from_crawler(cls, crawler):
            # This method is used by Scrapy to create your spiders.
            s = cls()
            crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
            return s
    
        def process_spider_input(self, response, spider):
            """下载完成,执行,然后交给parse处理"""
            # Called for each response that goes through the spider
            # middleware and into the spider.
    
            # Should return None or raise an exception.
            return None
    
        def process_spider_output(self, response, result, spider):
            """spider处理完成,返回时调用
            返回Request或者Item(字典也行,Item本身也是个字典)
            Request就是给调度器继续处理
            Item就是给项目管道保存
            """
            # Called with the results returned from the Spider, after
            # it has processed the response.
    
            # Must return an iterable of Request, dict or Item objects.
            for i in result:
                yield i
    
        def process_spider_exception(self, response, exception, spider):
            """异常调用"""
            # Called when a spider or process_spider_input() method
            # (from other spider middleware) raises an exception.
    
            # Should return either None or an iterable of Response, dict
            # or Item objects.
            pass
    
        def process_start_requests(self, start_requests, spider):
            """爬虫启动时调用"""
            # Called with the start requests of the spider, and works
            # similarly to the process_spider_output() method, except
            # that it doesn’t have a response associated.
    
            # Must return only requests (not items).
            for r in start_requests:
                yield r
    
        def spider_opened(self, spider):
            spider.logger.info('Spider opened: %s' % spider.name)

    爬虫中间件这里要注意下 process_spider_output() 返回的内容之后是要交给调度器继续爬取的,或者是交给项目管道做保存操作。所以返回的可以是 Request 或者是 Item 。

    下载器中间件

    class PeppascrapyDownloaderMiddleware(object):
        # Not all methods need to be defined. If a method is not defined,
        # scrapy acts as if the downloader middleware does not modify the
        # passed objects.
    
        @classmethod
        def from_crawler(cls, crawler):
            # This method is used by Scrapy to create your spiders.
            s = cls()
            crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
            return s
    
        def process_request(self, request, spider):
            """请求需要被下载时,经过所有下载器中间件的process_request调用"""
            # Called for each request that goes through the downloader
            # middleware.
    
            # Must either:
            # - return None: continue processing this request
            # - or return a Response object
            # - or return a Request object
            # - or raise IgnoreRequest: process_exception() methods of
            #   installed downloader middleware will be called
            return None
    
        def process_response(self, request, response, spider):
            """spider处理完成,返回时调用"""
            # Called with the response returned from the downloader.
    
            # Must either;
            # - return a Response object
            # - return a Request object
            # - or raise IgnoreRequest
            return response
    
        def process_exception(self, request, exception, spider):
            """异常处理
            当下载处理器(download handler)
            或 process_request() (下载中间件)抛出异常时执行
            """
            # Called when a download handler or a process_request()
            # (from other downloader middleware) raises an exception.
    
            # Must either:
            # - return None: continue processing this exception
            # - return a Response object: stops process_exception() chain
            # - return a Request object: stops process_exception() chain
            pass
    
        def spider_opened(self, spider):
            spider.logger.info('Spider opened: %s' % spider.name)

    process_request方法
    对不同的返回值,回有不同的效果:
    一般返回None,继续后面的中间件或者下载。这里可以修改一下请求头信息。比如,在请求头里添加代理的设置,然后再让后续的操作来执行。
    返回Response,下载器就是要去下载生成Response。这里直接返回Response就相当于已经下载完成了。所以之后不再是执行下载了,而是返回给中间件里的process_response方法,执行下载完成后的操作。比如,可以不用默认的下载器来下载。到这里自己用Request模块写段代码去下载,然后创建一个scrap.http.Eesponse对象,把内容填进去返回。
    返回Request,调度器就是生成一个个的Request,然后调度执行。如果这里返回了Request,就会停止这次的执行,把Request放回调度器,等待下一次被调度执行。在process_response方法里返回Request也是一样的效果,只是这里是在下载前要重新调度,那个是在下载后。

    自定义操作

    自定制命令

    自定制命令
    一、在spiders同级创建任意目录,如:commands
    二、在目录里创建 crawlall.py 文件,名字任意取,这个文件名将来就是执行这段代码的命令
    下面是一个启动spiders里所有爬虫的代码:

    from scrapy.commands import ScrapyCommand
    from scrapy.utils.project import get_project_settings
    
    class Command(ScrapyCommand):
        requires_project = True
    
        def syntax(self):
            return '[options]'
    
        def short_desc(self):
            return 'Runs all of the spiders'
    
        def run(self, args, opts):
            spider_list = self.crawler_process.spiders.list()
            for name in spider_list:
                self.crawler_process.crawl(name, **opts.__dict__)
            self.crawler_process.start()

    三、在 settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称' ,比如:

    COMMANDS_MODULE = "PeppaScrapy.commands"

    四、执行命令: scrapy crawlall

    自定义扩展

    利用信号在指定位置注册制定操作。
    自定义的型号要写在写一类,然后在settings里注册。默认的配置文件里是有EXTENSIONS的,注释掉了,这里就放开注释然后改一下:

    # Enable or disable extensions
    # See https://doc.scrapy.org/en/latest/topics/extensions.html
    EXTENSIONS = {
       # 'scrapy.extensions.telnet.TelnetConsole': None,
       'PeppaScrapy.extensions.MyExtension': 100
    }

    根据上面的操作,就是创建 extensions.py 文件,然后写一个 MyExtension 的类:

    # PeppaScrapy/extensions.py 文件
    from scrapy import signals
    
    class MyExtension(object):
        def __init__(self, value):
            self.value = value
    
        @classmethod
        def from_crawler(cls, crawler):
            val = crawler.settings.get('BOT_NAME')
            ext = cls(val)
            # 注册你的方法和信息
            crawler.signals.connect(ext.spider_start, signal=signals.spider_opened)
            crawler.signals.connect(ext.spider_stop, signal=signals.spider_closed)
            return ext
    
        # 写你要执行的方法
        def spider_start(self, spider):
            print('open')
    
        def spider_stop(self, spider):
            print('close')
    

    所有的信号
    上面的例子里用到了 spider_opened 和 spider_closed 这2个信号。
    在 scrapy/signals.py 里可以查到所有的信号:

    engine_started = object()
    engine_stopped = object()
    spider_opened = object()
    spider_idle = object()
    spider_closed = object()
    spider_error = object()
    request_scheduled = object()
    request_dropped = object()
    response_received = object()
    response_downloaded = object()
    item_scraped = object()
    item_dropped = object()

    配置文件详细

    # -*- coding: utf-8 -*-
    
    # Scrapy settings for step8_king project
    #
    # For simplicity, this file contains only settings considered important or
    # commonly used. You can find more settings consulting the documentation:
    #
    #     http://doc.scrapy.org/en/latest/topics/settings.html
    #     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
    #     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
    
    # 1. 爬虫名称
    BOT_NAME = 'step8_king'
    
    # 2. 爬虫应用路径
    SPIDER_MODULES = ['step8_king.spiders']
    NEWSPIDER_MODULE = 'step8_king.spiders'
    
    # Crawl responsibly by identifying yourself (and your website) on the user-agent
    # 3. 客户端 user-agent请求头
    # USER_AGENT = 'step8_king (+http://www.yourdomain.com)'
    
    # Obey robots.txt rules
    # 4. 禁止爬虫配置
    # ROBOTSTXT_OBEY = False
    
    # Configure maximum concurrent requests performed by Scrapy (default: 16)
    # 5. 并发请求数
    # CONCURRENT_REQUESTS = 4
    
    # Configure a delay for requests for the same website (default: 0)
    # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
    # See also autothrottle settings and docs
    # 6. 延迟下载秒数
    # DOWNLOAD_DELAY = 2
    
    # The download delay setting will honor only one of:
    # 7. 单域名访问并发数,并且延迟下次秒数也应用在每个域名
    # CONCURRENT_REQUESTS_PER_DOMAIN = 2
    # 单IP访问并发数,如果有值则忽略:CONCURRENT_REQUESTS_PER_DOMAIN,并且延迟下次秒数也应用在每个IP
    # CONCURRENT_REQUESTS_PER_IP = 3
    
    # Disable cookies (enabled by default)
    # 8. 是否支持cookie,cookiejar进行操作cookie
    # COOKIES_ENABLED = True
    # COOKIES_DEBUG = True
    
    # Disable Telnet Console (enabled by default)
    # 9. Telnet用于查看当前爬虫的信息,操作爬虫等...
    #    使用telnet ip port ,然后通过命令操作
    # TELNETCONSOLE_ENABLED = True
    # TELNETCONSOLE_HOST = '127.0.0.1'
    # TELNETCONSOLE_PORT = [6023,]
    
    # 10. 默认请求头
    # Override the default request headers:
    # DEFAULT_REQUEST_HEADERS = {
    #     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    #     'Accept-Language': 'en',
    # }
    
    # Configure item pipelines
    # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
    # 11. 定义pipeline处理请求
    # ITEM_PIPELINES = {
    #    'step8_king.pipelines.JsonPipeline': 700,
    #    'step8_king.pipelines.FilePipeline': 500,
    # }
    
    # 12. 自定义扩展,基于信号进行调用
    # Enable or disable extensions
    # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
    # EXTENSIONS = {
    #     # 'step8_king.extensions.MyExtension': 500,
    # }
    
    # 13. 爬虫允许的最大深度,可以通过meta查看当前深度;0表示无深度
    # DEPTH_LIMIT = 3
    
    # 14. 爬取时,0表示深度优先Lifo(默认);1表示广度优先FiFo
    
    # 后进先出,深度优先
    # DEPTH_PRIORITY = 0
    # SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
    # SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
    # 先进先出,广度优先
    
    # DEPTH_PRIORITY = 1
    # SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
    # SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'
    
    # 15. 调度器队列
    # SCHEDULER = 'scrapy.core.scheduler.Scheduler'
    # from scrapy.core.scheduler import Scheduler
    
    # 16. 访问URL去重
    # DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'
    
    # Enable and configure the AutoThrottle extension (disabled by default)
    # See http://doc.scrapy.org/en/latest/topics/autothrottle.html
    
    """
    17. 自动限速算法
        from scrapy.contrib.throttle import AutoThrottle
        自动限速设置
        1. 获取最小延迟 DOWNLOAD_DELAY
        2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
        3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
        4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间
        5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY
        target_delay = latency / self.target_concurrency
        new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延迟时间
        new_delay = max(target_delay, new_delay)
        new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
        slot.delay = new_delay
    """
    
    # 开始自动限速
    # AUTOTHROTTLE_ENABLED = True
    # The initial download delay
    # 初始下载延迟
    # AUTOTHROTTLE_START_DELAY = 5
    # The maximum download delay to be set in case of high latencies
    # 最大下载延迟
    # AUTOTHROTTLE_MAX_DELAY = 10
    # The average number of requests Scrapy should be sending in parallel to each remote server
    # 平均每秒并发数
    # AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
    
    # Enable showing throttling stats for every response received:
    # 是否显示
    # AUTOTHROTTLE_DEBUG = True
    
    # Enable and configure HTTP caching (disabled by default)
    # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
    
    """
    18. 启用缓存
        目的用于将已经发送的请求或相应缓存下来,以便以后使用
    
        from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
        from scrapy.extensions.httpcache import DummyPolicy
        from scrapy.extensions.httpcache import FilesystemCacheStorage
    """
    # 是否启用缓存策略
    # HTTPCACHE_ENABLED = True
    
    # 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
    # HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
    # 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
    # HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
    
    # 缓存超时时间
    # HTTPCACHE_EXPIRATION_SECS = 0
    
    # 缓存保存路径
    # HTTPCACHE_DIR = 'httpcache'
    
    # 缓存忽略的Http状态码
    # HTTPCACHE_IGNORE_HTTP_CODES = []
    
    # 缓存存储的插件
    # HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
    
    """
    19. 代理,需要在环境变量中设置
        from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware
    
        方式一:使用默认
            os.environ
            {
                http_proxy:http://root:woshiniba@192.168.11.11:9999/
                https_proxy:http://192.168.11.11:9999/
            }
        方式二:使用自定义下载中间件
    
        def to_bytes(text, encoding=None, errors='strict'):
            if isinstance(text, bytes):
                return text
            if not isinstance(text, six.string_types):
                raise TypeError('to_bytes must receive a unicode, str or bytes '
                                'object, got %s' % type(text).__name__)
            if encoding is None:
                encoding = 'utf-8'
            return text.encode(encoding, errors)
    
        class ProxyMiddleware(object):
            def process_request(self, request, spider):
                PROXIES = [
                    {'ip_port': '111.11.228.75:80', 'user_pass': ''},
                    {'ip_port': '120.198.243.22:80', 'user_pass': ''},
                    {'ip_port': '111.8.60.9:8123', 'user_pass': ''},
                    {'ip_port': '101.71.27.120:80', 'user_pass': ''},
                    {'ip_port': '122.96.59.104:80', 'user_pass': ''},
                    {'ip_port': '122.224.249.122:8088', 'user_pass': ''},
                ]
                proxy = random.choice(PROXIES)
                if proxy['user_pass'] is not None:
                    request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
                    encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))
                    request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
                    print "**************ProxyMiddleware have pass************" + proxy['ip_port']
                else:
                    print "**************ProxyMiddleware no pass************" + proxy['ip_port']
                    request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
    
        DOWNLOADER_MIDDLEWARES = {
           'step8_king.middlewares.ProxyMiddleware': 500,
        }
    
    """
    
    """
    20. Https访问
        Https访问时有两种情况:
        1. 要爬取网站使用的可信任证书(默认支持)
            DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
            DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"
    
        2. 要爬取网站使用的自定义证书
            DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
            DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory"
    
            # https.py
            from scrapy.core.downloader.contextfactory import ScrapyClientContextFactory
            from twisted.internet.ssl import (optionsForClientTLS, CertificateOptions, PrivateCertificate)
    
            class MySSLFactory(ScrapyClientContextFactory):
                def getCertificateOptions(self):
                    from OpenSSL import crypto
                    v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
                    v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
                    return CertificateOptions(
                        privateKey=v1,  # pKey对象
                        certificate=v2,  # X509对象
                        verify=False,
                        method=getattr(self, 'method', getattr(self, '_ssl_method', None))
                    )
        其他:
            相关类
                scrapy.core.downloader.handlers.http.HttpDownloadHandler
                scrapy.core.downloader.webclient.ScrapyHTTPClientFactory
                scrapy.core.downloader.contextfactory.ScrapyClientContextFactory
            相关配置
                DOWNLOADER_HTTPCLIENTFACTORY
                DOWNLOADER_CLIENTCONTEXTFACTORY
    
    """
    
    """
    21. 爬虫中间件
        class SpiderMiddleware(object):
    
            def process_spider_input(self,response, spider):
                '''
                下载完成,执行,然后交给parse处理
                :param response: 
                :param spider: 
                :return: 
                '''
                pass
    
            def process_spider_output(self,response, result, spider):
                '''
                spider处理完成,返回时调用
                :param response:
                :param result:
                :param spider:
                :return: 必须返回包含 Request 或 Item 对象的可迭代对象(iterable)
                '''
                return result
    
            def process_spider_exception(self,response, exception, spider):
                '''
                异常调用
                :param response:
                :param exception:
                :param spider:
                :return: None,继续交给后续中间件处理异常;含 Response 或 Item 的可迭代对象(iterable),交给调度器或pipeline
                '''
                return None
    
            def process_start_requests(self,start_requests, spider):
                '''
                爬虫启动时调用
                :param start_requests:
                :param spider:
                :return: 包含 Request 对象的可迭代对象
                '''
                return start_requests
    
        内置爬虫中间件:
            'scrapy.contrib.spidermiddleware.httperror.HttpErrorMiddleware': 50,
            'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': 500,
            'scrapy.contrib.spidermiddleware.referer.RefererMiddleware': 700,
            'scrapy.contrib.spidermiddleware.urllength.UrlLengthMiddleware': 800,
            'scrapy.contrib.spidermiddleware.depth.DepthMiddleware': 900,
    
    """
    # from scrapy.contrib.spidermiddleware.referer import RefererMiddleware
    # Enable or disable spider middlewares
    # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
    SPIDER_MIDDLEWARES = {
        # 'step8_king.middlewares.SpiderMiddleware': 543,
    }
    
    """
    22. 下载中间件
        class DownMiddleware1(object):
            def process_request(self, request, spider):
                '''
                请求需要被下载时,经过所有下载器中间件的process_request调用
                :param request:
                :param spider:
                :return:
                    None,继续后续中间件去下载;
                    Response对象,停止process_request的执行,开始执行process_response
                    Request对象,停止中间件的执行,将Request重新调度器
                    raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
                '''
                pass
    
            def process_response(self, request, response, spider):
                '''
                spider处理完成,返回时调用
                :param response:
                :param result:
                :param spider:
                :return:
                    Response 对象:转交给其他中间件process_response
                    Request 对象:停止中间件,request会被重新调度下载
                    raise IgnoreRequest 异常:调用Request.errback
                '''
                print('response1')
                return response
    
            def process_exception(self, request, exception, spider):
                '''
                当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
                :param response:
                :param exception:
                :param spider:
                :return:
                    None:继续交给后续中间件处理异常;
                    Response对象:停止后续process_exception方法
                    Request对象:停止中间件,request将会被重新调用下载
                '''
                return None
    
        默认下载中间件
        {
            'scrapy.contrib.downloadermiddleware.robotstxt.RobotsTxtMiddleware': 100,
            'scrapy.contrib.downloadermiddleware.httpauth.HttpAuthMiddleware': 300,
            'scrapy.contrib.downloadermiddleware.downloadtimeout.DownloadTimeoutMiddleware': 350,
            'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': 400,
            'scrapy.contrib.downloadermiddleware.retry.RetryMiddleware': 500,
            'scrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware': 550,
            'scrapy.contrib.downloadermiddleware.redirect.MetaRefreshMiddleware': 580,
            'scrapy.contrib.downloadermiddleware.httpcompression.HttpCompressionMiddleware': 590,
            'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600,
            'scrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware': 700,
            'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 750,
            'scrapy.contrib.downloadermiddleware.chunked.ChunkedTransferMiddleware': 830,
            'scrapy.contrib.downloadermiddleware.stats.DownloaderStats': 850,
            'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900,
        }
    
    """
    # from scrapy.contrib.downloadermiddleware.httpauth import HttpAuthMiddleware
    # Enable or disable downloader middlewares
    # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
    # DOWNLOADER_MIDDLEWARES = {
    #    'step8_king.middlewares.DownMiddleware1': 100,
    #    'step8_king.middlewares.DownMiddleware2': 500,
    # }

关键字