爬虫框架-scrapy的使用

Scrapy

  • Scrapy是纯python实现的一个为了爬取网站数据、提取结构性数据而编写的应用框架。
  • Scrapy使用了Twisted异步网络框架来处理网络通讯,可以加快我们的下载速度,并且包含了各种中间件接口,可以灵活的完成各种需求

1、安装

sudo pip3 install scrapy

2、认识scrapy框架

  • 2.1 scrapy架构图

  • Scrapy Engine(引擎): 负责SpiderItemPipelineDownloaderScheduler中间的通讯,信号、数据传递等。

  • Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队列,当引擎需要时,交还给引擎

  • Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理

  • Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器)

  • Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.

  • Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件

  • Spider Middlewares(Spider中间件):可以理解为是一个可以自定扩展和操作引擎Spider中间通信的功能组件(比如进入Spider的Responses和从Spider出去的Requests)

  • 2.2 Scrapy运行的大体流程:

1.引擎从spider拿到第一个需要处理的URL,并将request请求交给调度器。

2.调度器拿到request请求后,按照一定的方式进行整理排列,入队列,并将处理好的request请求返回给引擎。

3.引擎通知下载器,按照下载中间件的设置去下载这个request请求。

4.下载器下载request请求,并将获取到的response按照下载中间件进行处理,然后后交还给引擎,由引擎交给spider来处理。对于下载失败的request,引擎会通知调度器进行记录,待会重新下载。

5.spider拿到response,并调用回调函数(默认调用parse函数)去进行处理,并将提取到的Item数据和需要跟进的URL交给引擎。

6.引擎将item数据交给管道进行处理,将需要跟进的URL交给调度器,然后开始循环,直到调度器中不存在任何request,整个程序才会终止。

  • 2.3 制作scrapy爬虫步骤:

1.创建项目:通过(scrapy startproject 项目名)来创建一个项目

2.明确目标:编写items.py文件,定义提取的Item

3.制作爬虫:编写spiders/xx.py文件,爬取网站并提取Item

4.存储内容:编写pipelines.py文件,设计管道来存储提取到的Item(即数据)

 3、入门教程

  • 3.1 创建项目

    • 在开始爬虫之前,第一步需要创建一个项目。先进入打算存储代码的目录,运行以下命令:
      scrapy startproject myProject
    • 其中myProject为项目名,运行上述命令后,在当前目录下会创建一个myProject目录,该目录包含以下内容:
      .
      ├── myProject
      │   ├── __init__.py
      │   ├── items.py
      │   ├── middlewares.py
      │   ├── pipelines.py
      │   ├── settings.py
      │   └── spiders
      │       └── __init__.py
      └── scrapy.cfg

scrapy.cfg:项目的配置文件

myProject/items.py:项目中的目标文件

myProject/middlewares.py:项目中的中间件文件

myProject/pipelines.py:项目中的管道文件

myProject/settings.py:项目中的设置文件

myProject/spiders:放置spider代码的目录

  • 3.2 明确目标(定义Item)

    • 我们打算抓取网站http://www.itcast.cn/channel/teacher.shtml#ajavaee里所有老师的姓名、职称、入职时间和个人简介:
      • 首先打开myProject/items.py文件
      • Item是保存爬取到的数据的容器,其使用方法和python字典类似
      • 创建一个scrapy.Item 类, 并且定义类型为 scrapy.Field的类属性来定义一个Item(类似于ORM的映射关系)
      • 创建一个MyprojectItem 类,和构建item模型(model)
        import scrapy
        
        class MyprojectItem(scrapy.Item):
            name = scrapy.Field()
            title = scrapy.Field()
            hiredate = scrapy.Field()
            profile = scrapy.Field()
        
  • 3.3 制作爬虫

    • 在项目根目录下输入以下命令,可以在myProject/spiders目录下创建一个名为itcast的爬虫(itcast.py),并且指定爬虫作用域的范围itcast.cn:
      scrapy genspider itcast itcast.cn 
    • 打开itcast.py,默认添上了以下内容:
      import scrapy
      
      class ItcastSpider(scrapy.Spider):
          name = 'itcast'
          allowed_domains = ['itcast.cn']
          start_urls = ['http://itcast.cn/']
      
          def parse(self, response):
              pass
    • 要建立一个Spider, 你必须用scrapy.Spider类创建一个子类,并确定了3个强制的属性和1个方法。

      • name这个爬虫的识别名称,必须是唯一的

      • allow_domains爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。

      • start_urls爬取的URL列表。因此,第一个被获取到的页面将是其中之一。 后续的URL则从初始URL返回的数据中提取。

      • parse(self, response)Request对象默认的回调解析方法。每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,该方法负责解析返回的数据(response.body),提取数据(生成item)以及生成需要进一步处理的URL的Request对象

    • 修改start_urls为第一个需要爬取的URL:
      start_urls = ['http://www.itcast.cn/channel/teacher.shtml#ajavaee']
    • 修改parse方法提取Item:
      def parse(self, response):
          for teacher in response.xpath("//ul[@class='clears']/li/div[@class='main_mask']"):
              #将提取到到的数据封装到一个MyprojectItem对象中
              item = MyprojectItem()
              #利用xpath返回该表达式所对应的所有节点的selector list列表
              #调用extract方法序列化每个节点为Unicode字符串并返回list
              name = teacher.xpath('h2/text()').extract()[0]
              title = teacher.xpath('h2/span/text()').extract()[0]
              hiredate = teacher.xpath('h3/text()').extract()[0].split('')[-1]
              profile = teacher.xpath('p/text()').extract()[0]
              item['name'] = name
              item['title'] = title
              item['hiredate'] = hiredate
              item['profile'] = profile
              # 使用yield将获取的数据交给pipelines,如果使用return,则数据不会经过pipelines
              yield item
  • 3.4 存储内容

    • Feed输出

      • 如果仅仅想要保存item,可以不需要实现任何的pipeline,而是使用自带的Feed输出(Feed export)。主要有以下4种方式,通过-o指定输出文件格式:
        # json格式,默认为Unicode编码
        scrapy crawl itcast -o itcast.json
        # json lines格式,默认为Unicode编码
        scrapy crawl itcast -o itcast.jl 或 scrapy crawl itcast -o itcast.jsonlines
        #csv 逗号表达式,可用Excel打开
        scrapy crawl itcast -o itcast.csv
        # xml格式
        scrapy crawl itcast -o itcast.xml

        执行这些命令后,将会对爬取的数据进行序列化,并生成文件。

    • 编写Item Pipeline(通用):

      • 每个Item Pipeline都是实现了简单方法的Python类,他们接收到Item并通过它执行一些行为,同时也决定此Item是丢弃还是被后续pipeline继续处理。
      • 每个item pipeline组件必须实现process_item(self,item,spider)方法:
        • 这个方法必须返回一个Item (或任何继承类)对象, 或是抛出 DropItem异常。
        • 参数是被爬取的item和爬取该item的spider
        • spider程序每yield一个item,该方法就会被调用一次
      • 同时还可以实现以下方法:
        • from_crawler(cls,crawler):类方法,返回当前管道类实例化后的对象,如果重写该方法,则必须实现__init__方法(该方法一般不需要重写
        • __init__():初始化,可以在不实现from_crawler方法的前提下实现
        • open_spider(self,spider):开启spider的时候调用,只执行1次
        • close_spider(self,spider):关闭spider的时候调用,只执行1次
          class FirstPipeline:
              def __init__(self):
                  print('管道: __init__')
          
              def process_item(self, item, spider):
                  print('管道:process_item')
                  return item
          
              def open_spider(self, spider):
                  print('管道:open_spider')
                  # spider.settings == spider.crawler.settings == crawler.settings
                  print('管道(通过spider对象获取全局配置RETRY_TIMES):', spider.settings.get('RETRY_TIMES')) # 通过spider对象获取全局配置中的值,没有则返回None
          
              @classmethod
              def from_crawler(cls, crawler):
                  print('管道:from_crawler')
                  print('管道(通过crawler对象获取全局配置RETRY_TIMES):', crawler.settings.get('RETRY_TIMES')) # 通过spider对象获取全局配置中的值,没有则返回None
                  return cls() # 必须返回当前管道类实例化后的对象
          
              def close_spider(self, spider):
                  print('管道:close_spider')

          运行项目后执行结果:

          管道:from_crawler
          管道(通过crawler对象获取全局配置RETRY_TIMES): 2
          管道: __init__
          管道:open_spider
          管道(通过spider对象获取全局配置RETRY_TIMES): 2
          管道:process_item
          管道:close_spider

          可以看出执行顺序,其中from_crawler方法可省略,如果要实现,则必须同时实现__init__方法

    • item写入json文件:

      import json
      from itemadapter import ItemAdapter
      
      class MyprojectPipeline:
          def open_spider(self,spider):
              '''可选实现,开启spider时调用该方法'''
              self.f = open('itcast.json','w')
      
          def process_item(self, item, spider):
              '''必须实现,被抛弃的item将不会被后续的pipeline组件所处理'''
              self.f.write(json.dumps(dict(item),ensure_ascii=False)+'\n')
              return item
      
          def close_spider(self,spider):
              '''可选实现,关闭spider时调用该方法'''
              self.f.close()
    • 启用Item Pipeline组件

      ITEM_PIPELINES = {
         'myProject.pipelines.MyprojectPipeline': 300,
      }

      在settings.py文件里添加以上配置(可以取消原有的注释),后面的数字确定了item通过pipeline的顺序,通常定义在0-1000范围内,数值越低,组件的优先级越高

    • 启动爬虫

      scrapy crawl itcast

      查看当前目录下是否生成了itcast.json文件

 4、Scrapy Shell

Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。

  • 启动scrapy shell

    scrapy shell <url>
    命令行启动,url是要爬取的网页的地址
  • 常见可用对象response

    • response.status:状态码
    • response.url:当前页面url
    • response.body:响应体(bytes类型)
    • response.text:响应文本(str类型)
    • response.request:对应的请求对象
    • response.ip_address:获取服务器IP信息,response.ip_address.compressed会返回具体的ip地址
    • response.urljoin():可以将相对url构造成一个绝对url
      • # response.url:'https://quotes.toscrape.com'
        # response.urljoin('/page/2/') => 'https://quotes.toscrape.com/page/2'
    • response.json():如果响应体的是json,则直接转换成python的dict类型
    • response.headers:响应头
    • response.selector:返回Selector对象,之后就可以调用xpath和css等方法,也可以简写成response.xpath()和response.css()
  • selector选择器

    • Selector有四个基本的方法,最常用的还是xpath:

      • xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector列表,针对列表中的每个selector,依旧可以继续进行xpath、css等操作,如果只想获取第一个元素序列化后的结果,可继续调用extract_first()方法,没有元素则返回None
      • css(): 传入CSS表达式,返回该表达式所对应的所有节点的selectort列表,语法同 BeautifulSoup4
        • response.css('.author::text').extract_first() # 提取文本内容
          response.css('.author::attr("itemprop")').extract_first() # 提取属性值
      • extract(): 序列化该节点为Unicode字符串并返回list
      • re()和re_first(): 根据传入的正则表达式对数据进行提取,re()返回Unicode字符串列表(非节点列表),re_first()类似于extract_first(),返回第一个字符串
        • 注意,response对象没有re()方法和re_first()方法
          response.xpath('//a').re('Name:(.*?)') # ['张三', '李四', '王五']
          response.xpath('//a').re_first('Name:(.*?)') # '张三'

5、Spider

Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。

  • scrapy.Spider是最基本的类,所有编写的爬虫必须继承这个类。
    import scrapy
    
    class XxSpider(scrapy.Spider):
        pass
  • 主要用到的函数及调用顺序为:
    • __init__():初始化爬虫名字和start_urls列表
    • start__requests(self):调用make_requests_from_url()生成Requests对象交给Scrapy下载并返回response
    • parse(self,response):解析response,并返回Item或Requests(需指定回调函数)。Item传给Item pipline持久化 , 而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse()),一直进行循环,直到处理完所有的数据为止。
  • 其他方法
    • log(self, message, level=log.DEBUG)
      • message:字符串类型,写入的log信息
      • level:log等级,有CRITICAL、 ERROR、WARNING、INFO、DEBUG这5种,默认等级为DEBUG
    • close(spider, reason):静态方法
      • 执行顺序:在管道执行close_spider()方法之后执行
      • 可以进行一些收尾工作,比如浏览器关闭,数据库关闭等
        import scrapy
        from selenium import webdriver
        
        class WangyiSpider(scrapy.Spider):
            name = 'wangyi'
            allowed_domains = ['163.com']
            start_urls = ['https://news.163.com/']
            bro = webdriver.Chrome() #浏览器对象
        
            def parse(self, response):
                '''解析首页'''
                pass
        
            @staticmethod
            def close(spider, reason):
                '''关闭浏览器'''
                spider.bro.quit()

6、CrwalSpider

  • 快速创建CrawlSpider模板:

    scrapy genspider -t crawl 爬虫名 爬虫域
  • scrapy.spiders.CrwalSpider是编写的爬虫所必须继承的类
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    
    class XxxSpider(CrawlSpider):
        name = "xxx"
        allowed_domains = ["xxx.com"]
        start_urls = ["https://xxx.com"]
    
        rules = (Rule(LinkExtractor(allow=r"Items/"), callback="parse_item", follow=True),)
    
        def parse_item(self, response):
            item = {}
            #item["domain_id"] = response.xpath('//input[@id="sid"]/@value').get()
            #item["name"] = response.xpath('//div[@id="name"]').get()
            #item["description"] = response.xpath('//div[@id="description"]').get()
            yield item

    CrawlSpider类继承于Spider类,它定义了一些规则(rule)来提供跟进link的方便的机制,从爬取的网页中获取link并继续爬取的工作更适合。

  • LinkExtractor

    • class scrapy.spiders.LinkExtractor
    • 每个LinkExtractor对象有唯一的公共方法是 extract_links(),它接收一个Response对象,并返回一个 scrapy.link.Link 对象。根据不同的response调用多次来提取链接
    • 主要参数:

      • allow:满足括号中“正则表达式”的值会被提取,如果为None(不指定),则全部匹配。

      • deny:与这个正则表达式(或正则表达式列表)匹配的URL一定不提取。

      • allow_domains:符合此域名的链接才会被提取,相当于域名白名单。

      • deny_domains:一定不会被提取链接的域名,相当于域名黑名单。

      • restrict_xpaths:如果定义了该参数,则会从当前页面中Xpath匹配的区域提取连接,其值是Xpath表达式或Xpath表达式列表,和allow共同作用过滤链接,不指定allow参数,则默认提取当前区域下的所有链接。

      • restrict_css:用法同restrcit_xpaths,从匹配的指定区域提取链接
      • tags:指定提取链接的节点,默认是('a', 'area'),如果还需要提取图片链接,则可以修改为('a', 'area', 'img')
      • attrs:指定提取链接的属性,和tags配合起来使用,默认是('href',),提取图片,可以修改为('href', 'src')
      • process_value:一个callable方法,可以通过该方法完成提取内容到最终链接的转换,比如href属性中是一段Js处理逻辑('javascript:goToPage("../other/page.html")'),很明显不是有效的链接,可以通过该方法进行处理,然后返回正确的链接地址
      • unique:默认True,对提取的链接进行去重
      • strip:默认True,对提取的链接去除首尾空格
  • rules

    • class scrapy.spiders.Rule
    • 在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了特定操作。如果多个rule匹配了相同的链接,第一个会被使用。
    • Rule对象主要参数:
      • link_extractor:是一个Link Extractor对象,用于定义页面中哪些链接需要后续爬取,提取出的链接会自动生成Request
      • callback:从link_extractor中每获取到链接时,该回调函数接受一个response作为其第一个参数。注意:字符串类型,避免使用'parse'
      • follow:布尔类型,指定了根据该Rule(规则)从response提取的链接是否需要再次跟进(后续的response是否继续根据规则进行链接提取)。 如果callback为None,follow 默认设置为True ,否则为False。
      • process_links:指定函数(既可以是callbale方法,也可以是方法名字符串),从link_extractor中获取到链接列表时将会调用该函数,主要用来过滤,必须返回links。
          rules = [
                # 本案例为特殊情况,需要调用deal_links方法处理每个页面里的链接
                Rule(pagelink, process_links = "deal_links", follow = True),
                Rule(contentlink, callback = 'parse_item')
            ]
        
        # 需要重新处理每个页面里的链接,将链接里的‘Type&type=4?page=xxx’替换为‘Type?type=4&page=xxx’(或者是Type&page=xxx?type=4’替换为‘Type?page=xxx&type=4’),否则无法发送这个链接
            def deal_links(self, links):
                for link in links:
                    link.url = link.url.replace("?","&").replace("Type&", "Type?")
                    print link.url
                return links
      • process_request:指定函数(既可以是callbale方法,也可以是方法名字符串), 该规则提取到每个request时都会调用该函数,用来过滤request,必须返回Request对象或None。
  • CrawSpider爬虫示例

    • 以阳光热线问政平台http://wz.sun0769.com/political/index/politicsNewest?id=1为例,爬取投诉帖子的编号、帖子的标题,帖子的处理状态和帖子里的内容。
      import scrapy
      from scrapy.linkextractors import LinkExtractor
      from scrapy.spiders import CrawlSpider, Rule
      from myProject.items import MyprojectItem
      
      class SunSpider(CrawlSpider):
          name = 'sun'
          allowed_domains = ['wz.sun0769.com']
          start_urls = ['http://wz.sun0769.com/political/index/politicsNewest?id=1&page=1']
      
          rules = (
              Rule(LinkExtractor(allow=r'id=\d+&page=\d+')),# 翻页:每一页的匹配规则,callback为None,默认跟进
              Rule(LinkExtractor(allow=r'politics/index\?id=\d+'), callback='parse_item'),# 每个帖子的匹配规则,设置了callback,默认不跟进
          )
      
          def parse_item(self, response):
              item = MyprojectItem()
              title = response.xpath('//div[@class="mr-three"]/p[@class="focus-details"]/text()').extract()[0] #帖子标题
              status = response.xpath('//div[@class="focus-date clear focus-date-list"]/span[3]/text()').extract()[0].split()[1] #处理状态
              number = response.xpath('//div[@class="focus-date clear focus-date-list"]/span[4]/text()').extract()[0].split('')[-1] #帖子编号
              content = response.xpath('//div[@class="details-box"]/pre/text()').extract()[0] #帖子内容
              item['title'] = title
              item['status'] = status
              item['number'] = number
              item['content'] = content
      yield
      item

7、logging功能

Scrapy提供了log功能,通过在setting.py中进行设置,可以被用来配置logging

  • 设置

    • LOG_ENABLED:默认 True,启用logging
    • LOG_ENCODING:默认'utf-8',logging使用的编码
    • LOG_FILE:默认None,在当前目录里创建logging输出文件的文件名
    • LOG_LEVEL:默认'DEBUG',有'CRITICAL'(严重错误)、'ERROR'(一般错误)、'WARNING'(警告信息)、'INFO'(一般信息)、'DEBUG'(调试信息)这5种等级
    • LOG_STDOUT:默认False,如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
  • 示例:

    #在settings.py中任意位置添上以下两句,终端上会清爽很多
    LOG_FILE = "xxx.log"
    LOG_LEVEL = "INFO"

8、Request对象

  • GET请求

    • 可以使用yield scrapy.Request(url,callback)方法来发送请求
    • Request对象初始化方法传入参数如下:
      class Request(object_ref):
      
          def __init__(self, url, callback=None, method='GET', headers=None, body=None,
                       cookies=None, meta=None, encoding='utf-8', priority=0,
                       dont_filter=False, errback=None, flags=None, cb_kwargs=None):
              pass
    • 主要参数:
      • url:需要请求并进行下一步处理的url
      • callback:指定该请求返回的Response,由哪个函数来处理
      • method:默认'GET',一般不需要指定,可以是‘POST’,'PUT'等
      • headrs:请求时包含的头文件,一般不需要
      • meta:额外传递的参数,比较常用,在不同的request之间传递数据用的,dict类型,回调函数中可通过response.meta来获取,此外还预留了一些特殊处理,比如:
        • request.meta['proxy']:可以设置请求代理
        • request.meta['max_retry_times']:可以设置请求时的最大重试次数
      • encoding:使用默认的‘utf-8’就行
      • dont_filter:表明该请求不由调度器过滤,可以发送重复请求,默认为False
      • errback:指定错误处理函数
      • priority:Request优先级,默认0,数值越大越先被调度执行
      • cb_kwargs:回调方法的额外参数,可以作为字典传递
  • POST请求

    • Json数据

      • scrapy.http.JsonRequest
        • data参数:传递json数据
        • Content-Type:application/json
    • 表单数据

      • scrapy.FormRequest或者scrapy.http.FormRequest
        • Content-Type:application/x-www-form-urlencoded
        • formdata参数:传递表单数据
          import scrapy
          from scrapy.http import FormRequest, JsonRequest
          
          class TestSpider(scrapy.Spider):
              name = "test"
              start_url = "https://www.httpbin.org/post"
          
              def start_requests(self):
                  data = {'a':'1', 'b':'2'}
                  # 发送表单数据
                  yield FormRequest(url=self.start_url, formdata=data, callback=self.parse_cb, dont_filter=True)
                  
                  # 发送json数据
                  yield JsonRequest(url=self.start_url, data=data, callback=self.parse_cb, dont_filter=True)
          
              def parse_cb(self, response):
                  print(response.text)
      • 表单请求一些常见处理:
        • 可以使用yield scrapy.FormRequest(url, formdata, callback)方法进行发送
        • 如果希望程序执行一开始就发送POST请求,可以重写Spider类的start_requests(self)方法,并且不再调用start_urls里的url,而是直接使用需要post的url,然后调用yield scrapy.FormRequest(url, formdata, callback)方法。
        • 如果想要预填充或重写像用户名、用户密码这些表单字段, 可以在parse()方法中先通过response.xpath('xxx')来获取某些字段值,然后添到formdata中,再使用yield scrapy.FormRequest.from_response(response, formdata, callback) 方法实现。
        • 3种模拟登陆策略:
          #模拟登陆时,必须保证settings.py里的 COOKIES_ENABLED (Cookies中间件) 处于开启状态
          
          # 策略一:直接POST数据(比如需要登陆的账户信息)
          def start_requests(self):
              url = 'xxx'
              yield scrapy.FormRequest(
                      url = url,
                      formdata = {"xxl" : "xxx1", "xx2" : "xxx2"},
                      callback = self.parse_page)
          def parse_page(self,response):
              pass
          
          #策略二:标准的模拟登陆步骤
          #1.首先发送登录页面的get请求,获取到页面里的登录必须的参数
          #2.然后和账户密码一起post到服务器
          def parse(self,response):
              # 提取登陆需要的参数(以_xsrf为例)
              _xsrf = response.xpath("xxx").extract()[0]
              # 发送请求参数,并调用指定回调函数处理
              yield scrapy.FormRequest.from_response(
                      response,
                      formdata = {"xxl" : "xxx1", "xx2" : "xxx2","_xsrf":_xsrf}},
                      callback = self.parse_page
                      )
          
          def parse_page(self,response):
              pass
          
          #策略三:直接使用保存登陆状态的Cookie模拟登陆
          #如果实在没办法了,可以用这种方法模拟登录,首先通过浏览器登陆后,复制出cookies,比较麻烦,但是成功率100%,
          
          #在类属性中填上登陆成功后的cookies内容
          cookies = {...}
          # 可以重写Spider类的start_requests方法,附带Cookie值,发送POST请求
          def start_requests(self):
              for url in self.start_urls:
                  yield scrapy.FormRequest(url, cookies = self.cookies, callback = self.parse_page)
          
          def parse_page(self,response):
              pass

9、Downloader Middlewares(下载中间件)

  • 下载中间件是处于引擎(crawler.engine)和下载器(crawler.engine.download())之间的一层组件,可以有多个下载中间件被加载运行。
    • 当引擎传递请求给下载器的过程中,下载中间件可以对请求进行处理 (例如增加http header信息,增加proxy信息等);
    • 在下载器完成http请求,传递响应给引擎的过程中, 下载中间件可以对响应进行处理(例如进行gzip的解压等)
  • 要激活下载器中间件组件,将其加入到settings.py中的DOWNLOADER_MIDDLEWARES 设置中。 该设置是一个字典(dict),键为中间件类的路径,值为其中间件的顺序(order)。例如:
    DOWNLOADER_MIDDLEWARES = {
       'myProject.middlewares.MyprojectDownloaderMiddleware': 543,
    }
  • 中间件组件是一个定义了以下一个或多个方法的Python类:
    • process_request(self, request, spider):当每个request通过下载中间件时,该方法被调用。优先级数字越小,越先调用
    • process_response(self, request, response, spider):当下载器完成http请求,传递响应给引擎的时候调用。与请求处理相反,数字越小,越后调用
  • 示例:(使用随机User-Agent和代理IP)
    • middlewares.py文件
      import random
      import json
      import redis
      
      from scrapy import signals
      from itemadapter import is_item, ItemAdapter
      from myProject.settings import USER_AGENTS
      
      class MyprojectDownloaderMiddleware:
          def __init__(self):
              self.r = redis.StrictRedis(host='localhost') #创建redis连接客户端,用于取里面存储的动态获取的代理ip
      
          def process_request(self, request, spider):
              user_agent = random.choice(USER_AGENTS) #取随机user-Agent
              proxy_list = json.loads(self.r.get('proxy_list'))
              proxy = random.choice(proxy_list) #取随机ip
              request.headers["User-Agent"] = user_agent #设置user-agent
              request.meta['proxy'] ='http://'+proxy['ip']+':'+str(proxy['port']) #使用代理ip
    • 修改settings.py文件配置
      #禁用cookies
      COOKIES_ENABLED = False
      
      #设置下载延迟
      DOWNLOAD_DELAY = 3
      
      #添加自己写的下载中间件类
      DOWNLOADER_MIDDLEWARES = {
         'myProject.middlewares.MyprojectDownloaderMiddleware': 543,
      }
      
      #添加USER-AGENTS
      USER_AGENTS = [
          "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
          "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
          "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
          "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
          "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
          "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
          "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
          "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
          ]

10、请求重试

  • settings.py文件中设置

    # RETRY_ENABLED = True  默认True,开启重试
    RETRY_TIMES = 5 # 设置重试次数,默认2次(最多请求2+1=3次),设置5,则最多请求6次
    RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429, 403] # 设置需要进行重试的状态码,默认:[500, 502, 503, 504, 522, 524, 408, 429],不含403
  •  中间件中示例

    class FirstDownloaderMiddleware:
        def process_request(self, request, spider):
            print(f'中间件:{request.url}')
    
        def process_response(self, request, response, spider):
            # 设置重试的条件
            if response.status == 403:
                retry_times = request.meta.get('cur_retry_times',0) + 1
                if retry_times <= 5:
                    request.meta['cur_retry_times'] = retry_times
                    request.dont_filter = True
                    return request
                else:
                    print(f'Give up retrying <{response.url}> (failed {retry_times} times)')
            return response

    结果示例:

    中间件:http://127.0.0.1:5000/hello
    中间件:http://127.0.0.1:5000/hello
    中间件:http://127.0.0.1:5000/hello
    中间件:http://127.0.0.1:5000/hello
    中间件:http://127.0.0.1:5000/hello
    中间件:http://127.0.0.1:5000/hello
    Gave up retrying <http://127.0.0.1:5000/hello> (failed 6 times)
  • Spider中示例

    class TestSpider(scrapy.Spider):
        name = "test"
        start_urls = ["http://127.0.0.1:5000/hello"]
    
        def parse(self, response):
            if response.status == 403:
                retry_times = response.meta.get('cus_retry_times', 0) + 1
                if retry_times <= 5:
                    r = response.request
                    r.meta['cus_retry_times'] = retry_times
                    r.dont_filter = True
                    yield r
                else:
                    print(f' Give up retrying <{response.url}> (failed {retry_times} times)')
            else:
                print('成功响应')

11、大文件下载

  • ImagesPipeline

    • 专门用来下载图片、音乐、视频等二进制文件(继承自FilesPipeline)
    • 在settings.py中添加文件存储路径
      IMAGES_STORE = './images' # 文件夹不存在,则自动创建
    • 自定义管道类,继承自ImagesPipeline,但是要实现一下3个方法
      • get_media_requests:发送请求

      • file_path:返回文件名(在IMAGES_STORE指定的目录下,可以是带目录路径的文件名,如果文件夹不存在则会自动创建)

      • item_completed:单个item完成下载时的处理方法,对于下载失败的可以抛出DropItem异常,从而忽略该item,停止后续管道处理(注意管道调用顺序)

    • 示例
      from scrapy.pipelines.images import ImagesPipeline
      from scrapy.exceptions import DropItem
      from scrapy import Request
      
      class FirstPipeline(ImagesPipeline):
          def get_media_requests(self, item, info):
              '''发送请求'''
              url = item['src']
              yield Request(url=url,meta={'item':item})
      
          def file_path(self, request, response=None, info=None, *, item=None):
              '''返回文件路径:在IMAGES_STORE指定的目录下,如果路径不存在则自动创建'''
              item = request.meta['item']
              type = item['type']
              name = item['name']
              file_name = f'{type}/{name}.jpg'
              return file_name
      
          def item_completed(self, results, item, info):
              '''单个item完成下载时的处理方法'''
              # 下载失败的Item,可以不进行后续管道处理
              if not results[0][0]:
                  raise DropItem(f'Image下载失败')
              #成功示例: [(True,{'url': 'https://img3.doubanio.com/view/photo/l/public/p579722647.webp', 'path': '电影/三傻大闹宝莱坞.jpg','checksum': '8fc3b6b27a3c17cd991340e7a9921e13', 'status': 'downloaded'})]
              #失败示例: [(False,< twisted.python.failure.Failure scrapy.pipelines.files.FileException: cannot identify image file < _io.BytesIO object at 0x00000235EAFC4D10 >>)]
              return item

12、项目根目录下新建main.py,用于调试

  • 方案1
    import subprocess
    
    # 可以开启多个爬虫
    subprocess.run('scrapy crawl test1')
    subprocess.run('scrapy crawl test2')
  • 方案2
    from scrapy import cmdline
    
    cmdline.execute('scrapy crawl test'.split())

 

posted @ 2021-04-28 03:37  eliwang  阅读(1681)  评论(0编辑  收藏  举报