爬虫框架 scrapy

简介

什么是框架?

所谓的框,其实说白了就是一个【项目的半成品】,该项目的半成品需要被集成了各种功能且具有较强的通用性。

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍。所谓的框架就是一个已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)的具有很强通用性的项目模板。对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可。

初期如何学习框架?

只需要学习框架集成好的各种功能的用法即可!前期切勿钻研框架的源码!

安装

Linux/mac系统:
      pip install scrapy(任意目录下)

Windows系统:

      a. pip install wheel(任意目录下)

      b. 下载twisted文件,下载网址如下: http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

      c. 终端进入下载目录,执行 pip install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
      注意:如果该步骤安装出错,则换一个版本的whl文件即可

      d. pip install pywin32(任意目录下)

      e. pip install scrapy(任意目录下)
      
如果安装好后,在终端中录入scrapy指令按下回车,如果没有提示找不到该指令,则表示安装成功

基本使用

  • 创建项目

    • scrapy startproject 项目名称

    • 项目的目录结构:

      • firstBlood   # 项目所在文件夹, 建议用pycharm打开该文件夹
            ├── firstBlood  		# 项目跟目录
            │   ├── __init__.py
            │   ├── items.py  		# 封装数据的格式
            │   ├── middlewares.py  # 所有中间件
            │   ├── pipelines.py	# 所有的管道
            │   ├── settings.py		# 爬虫配置信息
            │   └── spiders			# 爬虫文件夹, 稍后里面会写入爬虫代码
            │       └── __init__.py
            └── scrapy.cfg			# scrapy项目配置信息,不要删它,别动它,善待它. 
        
        
  • 创建爬虫爬虫文件:

    • cd project_name(进入项目目录)
    • scrapy genspider 爬虫文件的名称(自定义一个名字即可) 起始url
      • (例如:scrapy genspider first www.xxx.com)
    • 创建成功后,会在爬虫文件夹下生成一个py的爬虫文件
  • 编写爬虫文件

    • 理解爬虫文件的不同组成部分

    • import scrapy
      
      class FirstSpider(scrapy.Spider):
          #爬虫名称:爬虫文件唯一标识:可以使用该变量的值来定位到唯一的一个爬虫文件
          name = 'first' #无需改动
          #允许的域名:scrapy只可以发起百度域名下的网络请求
          # allowed_domains = ['www.baidu.com']
          #起始的url列表:列表中存放的url可以被scrapy发起get请求
          start_urls = ['https://www.baidu.com/','https://www.sogou.com']
      
          #专门用作于数据解析
          #参数response:就是请求之后对应的响应对象
          #parse的调用次数,取决于start_urls列表元素的个数
          def parse(self, response):
              print('响应对象为:',response)
      
      
  • 配置文件修改:settings.py

    • 不遵从robots协议:ROBOTSTXT_OBEY = False
    • 指定输出日志的类型:LOG_LEVEL = 'ERROR'
    • 指定UA:USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36'
  • 运行项目

    • scrapy crawl 爬虫名称 :该种执行形式会显示执行的日志信息(推荐)
      scrapy crawl 爬虫名称 --nolog:该种执行形式不会显示执行的日志信息(一般不用)
      
    • from scrapy.cmdline import execute
      
      if __name__ == '__main__':
          execute("scrapy crawl 爬虫名称".split())
      

数据解析

  • 注意,如果终端还在第一个项目的文件夹中,则需要在终端中执行cd ../返回到上级目录,在去新建另一个项目。

  • 新建数据解析项目:

    • 创建工程:scrapy startproject 项目名称
    • cd 项目名称
    • 创建爬虫文件:scrapy genspider 爬虫文件名 www.xxx.com
      pos
  • 配置文件的修改:settings.py

    • 不遵从robots协议:ROBOTSTXT_OBEY = False
    • 指定输出日志的类型:LOG_LEVEL = 'ERROR'
    • 指定UA:USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36'
  • 编写爬虫文件:spiders/duanzi.py

    • import scrapy
      
      class DuanziSpider(scrapy.Spider):
          name = 'duanzi'
          # allowed_domains = ['www.xxx.com']
          #对首页进行网络请求
          #scrapy会对列表中的url发起get请求
          start_urls = ['https://ishuo.cn/duanzi']
      
          def parse(self, response):
              # response.text  # 页面源代码
              # response.xpath()  # 通过xpath方式提取
              # response.css()  # 通过css方式提取
              # response.json() # 提取json数据
              # response.url # url
              # response.urljoin('www.baidu.com')  # url + www.baidu.com
              #如何获取响应数据
              #调用xpath方法对响应数据进行xpath形式的数据解析
              li_list = response.xpath('//*[@id="list"]/ul/li')
              for li in li_list:
                  # content = li.xpath('./div[1]/text()')[0]
                  # title = li.xpath('./div[2]/a/text()')[0]
                  # #<Selector xpath='./div[2]/a/text()' data='一年奔波,尘缘遇了谁'>
                  # print(title)#selector的对象,且我们想要的字符串内容存在于该对象的data参数里
                  #解析方案1:
                  # title = li.xpath('./div[2]/a/text()')[0]
                  # content = li.xpath('./div[1]/text()')[0]
                  # #extract()可以将selector对象中data参数的值取出
                  # print(title.extract())
                  # print(content.extract())
                  #解析方案2:
                  #title和content为列表,列表只要一个列表元素
                  title = li.xpath('./div[2]/a/text()')
                  content = li.xpath('./div[1]/text()')
                  #extract_first()可以将列表中第0个列表元素表示的selector对象中data的参数值取出
                  print(title.extract_first())
                  print(content.extract_first())
      
      

持久化存储

两种方案:

  • 基于终端指令的持久化存储
  • 基于管道的持久化存储(推荐)

基于终端指令的持久化存储

  • 只可以将parse方法的返回值存储到指定后缀的文本文件中。

  • 编码流程:

    • 在爬虫文件中,将爬取到的数据全部封装到parse方法的返回值中

      • import scrapy
        
        class DemoSpider(scrapy.Spider):
            name = 'demo'
            # allowed_domains = ['www.xxx.com']
            start_urls = ['https://ishuo.cn/duanzi']
        
            def parse(self, response):
                # 如何获取响应数据
                # 调用xpath方法对响应数据进行xpath形式的数据解析
                li_list = response.xpath('//*[@id="list"]/ul/li')
                all_data = []#爬取到的数据全部都存储到了该列表中
                for li in li_list:
                    title = li.xpath('./div[2]/a/text()').extract_first()
                    content = li.xpath('./div[1]/text()').extract_first()
                    #将段子标题和内容封装成parse方法的返回
                    dic = {
                        'title':title,
                        'content':content
                    }
                    all_data.append(dic)
        
                return all_data
        
        
    • 将parse方法的返回值存储到指定后缀的文本文件中:

      • scrapy crawl 爬虫文件名称 -o duanzi.csv
  • 总结:

    • 优点:简单,便捷
    • 缺点:局限性强
      • 只可以将数据存储到文本文件无法写入数据库
      • 存储数据文件后缀是指定好的,通常使用.csv
      • 需要将存储的数据封装到parse方法的返回值中

基于管道实现持久化存储

优点:极大程度的提升数据存储的效率

缺点:编码流程较多

编码流程

1.在爬虫文件中进行数据解析

def parse(self, response):
  # 如何获取响应数据
  # 调用xpath方法对响应数据进行xpath形式的数据解析
  li_list = response.xpath('//*[@id="list"]/ul/li')
  all_data = []  # 爬取到的数据全部都存储到了该列表中
  for li in li_list:
    title = li.xpath('./div[2]/a/text()').extract_first()
    content = li.xpath('./div[1]/text()').extract_first()

2.将解析到的数据封装到Item类型的对象中

  • 2.1 在items.py文件中定义相关的字段

    • class SavedataproItem(scrapy.Item):
          # define the fields for your item here like:
          # name = scrapy.Field()
          #爬取的字段有哪些,这里就需要定义哪些变量存储爬取到的字段
          title = scrapy.Field()
          content = scrapy.Field()
      
  • 2.2 在爬虫文件中引入Item类,实例化item对象,将解析到的数据存储到item对象中

    •     def parse(self, response):
          		from items import SavedataproItem #导入item类
              # 如何获取响应数据
              # 调用xpath方法对响应数据进行xpath形式的数据解析
              li_list = response.xpath('//*[@id="list"]/ul/li')
              all_data = []  # 爬取到的数据全部都存储到了该列表中
              for li in li_list:
                  title = li.xpath('./div[2]/a/text()').extract_first()
                  content = li.xpath('./div[1]/text()').extract_first()
                  #实例化一个item类型的对象
                  item = SavedataproItem()
                  #通过中括号的方式访问item对象中的两个成员,且将解析到的两个字段赋值给item对象的两个成员即可
                  item['title'] = title
                  item['content'] = content
      

3.将item对象提交给管道

  • #将存储好数据的item对象提交给管道   (spider返回的内容只能是字典, requestes对象, item数据或者None. 其他内容一律报错)
    yield item
    

4.在管道中接收item类型对象(pipelines.py就是管道文件)

  • 管道只可以接收item类型的对象,不可以接收其他类型对象

  • class SavedataproPipeline:
        #process_item用来接收爬虫文件传递过来的item对象
        #item参数,就是管道接收到的item类型对象
        def process_item(self, item, spider):
            print(item)
            return item
    

5.在管道中对接收到的数据进行任意形式的持久化存储操作

  • 可以存储到文件中也可以存储到数据库中

  • # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
    
    
    # useful for handling different item types with a single interface
    from itemadapter import ItemAdapter
    
    
    class SavedataproPipeline:
        #重写父类的方法
        fp = None
        def open_spider(self,spider):
            print('我是open_spider方法,我在项目开始运行环节,只会被执行一次!')
            self.fp = open('duanzi.txt','w',encoding='utf-8')
        #process_item用来接收爬虫文件传递过来的item对象
        #item参数,就是管道接收到的item类型对象
        #process_item方法调用的次数取决于爬虫文件给其提交item的次数
        def process_item(self, item, spider):
            #item类型的对象其实就是一个字典
            # print(item)
            #将item字典中的标题和内容获取
            title = item['title']
            content = item['content']
            self.fp.write(title+':'+content+'\n')
            print(title,':爬取保存成功!')
            return item
    
        def close_spider(self,spider):
            print('在爬虫结束的时候会被执行一次!')
            self.fp.close()
    

6.在配置文件中开启管道机制

  • 注意:默认情况下,管道机制是没有被开启的,需要在配置文件中手动开启
  • 在setting.py中把ITEM_PIPELINES解除注释就表示开启了管道机制

管道深入操作

  • 如何将数据存储到数据库

    • 注意:一个管道类负责将数据存储到一个具体的载体中。如果想要将爬取到的数据存储到多个不同的载体/数据库中,则需要定义多个管道类。
  • 思考:

    • 在有多个管道类的前提下,爬虫文件提交的item会同时给没一个管道类还是单独的管道类?
      • 爬虫文件只会将item提交给优先级最高的那一个管道类。优先级最高的管道类的process_item中需要写return item操作,该操作就是表示将item对象传递给下一个管道类,下一个管道类获取了item对象,才可以将数据存储成功!
  • 管道类:

  • # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
    
    
    # useful for handling different item types with a single interface
    from itemadapter import ItemAdapter
    import pymysql
    import redis
    import pymongo
    #负责将数据存储到mysql中
    class MysqlPipeline:
        conn = None #mysql的链接对象
        cursor = None
        def open_spider(self,spider):
            self.conn = pymysql.Connect(
                host = '127.0.0.1',
                port = 3306,
                user = 'root',
                password = 'boboadmin',
                db = 'spider3qi',
                charset = 'utf8'
            )
            self.cursor = self.conn.cursor()
        #爬虫文件每向管道提交一个item,则process_item方法就会被调用一次
        def process_item(self, item, spider):
            title = item['title']
            sql = 'insert into xiaoshuo (title) values ("%s")'%title
            self.cursor.execute(sql)
            self.conn.commit()
            print('成功写入一条数据!')
            return item
        def close_spider(self,spider):
            self.cursor.close()
            self.conn.close()
    
    #将数据持久化存储到redis中
    class RedisPipeLine:
        conn = None
        def open_spider(self,spider):
            #在链接前务必手动启动redis的服务
            self.conn = redis.Redis(
                host='127.0.0.1',
                port=6379
            )
        def process_item(self,item,spider):
            #注意:如果想要将一个python字典直接写入到redis中,则redis模块的版本务必是2.10.6
            #如果redis模块的版本不是2.10.6则重新安装:pip install redis==2.10.6
            self.conn.lpush('xiaoshuo',item)
            print('数据存储redis成功!')
            return item
    
    class MongoPipeline:
        conn = None #链接对象
        db_sanqi = None #数据仓库
        def open_spider(self,spider):
            self.conn = pymongo.MongoClient(
                host='127.0.0.1',
                port=27017
            )
            self.db_sanqi = self.conn['sanqi']
        def process_item(self,item,spider):
            self.db_sanqi['xiaoshuo'].insert_one({'title':item['title']})
            print('插入成功!')
            return item
    
  • 配置文件:

  • ITEM_PIPELINES = {
       #数字表示管道类被执行的优先级,数字越小表示优先级越高
       'xiaoshuoPro.pipelines.MysqlPipeline': 300,
       'xiaoshuoPro.pipelines.RedisPipeLine': 301,
       'xiaoshuoPro.pipelines.MongoPipeline': 302,
    }
    

scrapy爬取多媒体资源数据

  • 使用一个专有的管道类ImagesPipeline

  • 具体的编码流程:

    • 1.在爬虫文件中进行图片/视频的链接提取

    • 2.将提取到的链接封装到items对象中,提交给管道

    • 3.在管道文件中自定义一个父类为ImagesPipeline的管道类,且重写三个方法即可:

      • def get_media_requests(self, item, info):接收爬虫文件提交过来的item对象,然后对图片地址发起网路请求,返回图片的二进制数据
        
        def file_path(self, request, response=None, info=None, *, item=None):指定保存图片的名称
        def item_completed(self, results, item, info):返回item对象给下一个管道类
        
    • 4.在配置文件中开启指定的管道,且通过IMAGES_STORE = 'girlsLib'操作指定图片存储的文件夹。

    # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
    
    
    # useful for handling different item types with a single interface
    import scrapy
    from itemadapter import ItemAdapter
    
    from scrapy.pipelines.images import ImagesPipeline
    
    #自定义的管道类一定要继承与ImagesPipeline
    class mediaPileline(ImagesPipeline):
        #重写三个父类的方法来完成图片二进制数据的请求和持久化存储
        #可以根据图片地址,对其进行请求,获取图片数据
        #参数item:就是接收到的item对象
        def get_media_requests(self, item, info):
            img_src = item['src']
            yield scrapy.Request(img_src)
        #指定图片的名称(只需要返回图片存储的名称即可)
        def file_path(self, request, response=None, info=None, *, item=None):
            imgName = request.url.split('/')[-1]
            print(imgName,'下载保存成功!')
            return imgName
        #如果没有下一个管道类,该方法可以不写
        def item_completed(self, results, item, info):
            return item #可以将当前的管道类接收到item对象传递给下一个管道类2.
    

scrapy深度爬取

  • 如何爬取多页的数据(全站数据爬取)

    • 手动请求发送:

      • #callback用来指定解析方法
        yield scrapy.Request(url=new_url,callback=self.parse)
        
  • 如何爬取深度存储的数据

    • 什么是深度,说白了就是爬取的数据没有存在于同一张页面中。

    • 必须使用请求传参的机制才可以完整的实现。

      • 请求传参:

        • yield scrapy.Request(meta={},url=detail_url,callback=self.parse_detail)
          
          可以将meta字典传递给callback这个回调函数
          
import scrapy
from ..items import DeepproItem

class DeepSpider(scrapy.Spider):
    name = 'deep'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://wz.sun0769.com/political/index/politicsNewest']
    #解析首页数据
    def parse(self, response):
        li_list = response.xpath('/html/body/div[2]/div[3]/ul[2]/li')
        for li in li_list:
            title = li.xpath('./span[3]/a/text()').extract_first()
            detail_url = 'https://wz.sun0769.com'+li.xpath('./span[3]/a/@href').extract_first()
            # print(title)
            item = DeepproItem()
            item['title'] = title
            #对详情页的url发起请求
            #参数meta可以将自身这个字典传递给callback指定的回调函数
            yield scrapy.Request(meta={'item':item},url=detail_url,callback=self.parse_detail)
    #解析详情页数据
    def parse_detail(self,response):
        meta = response.meta #接收请求传参过来的meta字典
        item = meta['item']
        content = response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]//text()').extract()
        content = ''.join(content)
        # print(content)
        item['content'] = content

        yield item

ImagePipeLines的请求传参

  • 环境安装:pip install Pillow

  • USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36'
    
  • 需求:将图片的名称和详情页中图片的数据进行爬取,持久化存储。

    • 分析:

      • 深度爬取:请求传参
      • 多页的数据爬取:手动请求的发送
    • 爬虫文件:

    • import scrapy
      
      from ..items import DeepimgproItem
      class ImgSpider(scrapy.Spider):
          name = 'img'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://pic.netbian.com/4kmeinv/']
          #通用的url模板
          url_model = 'https://pic.netbian.com/4kmeinv/index_%d.html'
          page_num = 2
          def parse(self, response):
              #解析出了图片的名称和详情页的url
              li_list = response.xpath('//*[@id="main"]/div[3]/ul/li')
              for li in li_list:
                  title = li.xpath('./a/b/text()').extract_first() + '.jpg'
                  detail_url = 'https://pic.netbian.com'+li.xpath('./a/@href').extract_first()
                  item = DeepimgproItem()
                  item['title'] = title
                  #需要对详情页的url发起请求,在详情页中获取图片的下载链接
                  yield scrapy.Request(url=detail_url,callback=self.detail_parse,meta={'item':item})
              if self.page_num <= 2:
                  new_url = format(self.url_model%self.page_num)
                  self.page_num += 1
                  yield scrapy.Request(url=new_url,callback=self.parse)
          #解析详情页的数据
          def detail_parse(self,response):
              meta = response.meta
              item = meta['item']
              img_src = 'https://pic.netbian.com'+response.xpath('//*[@id="img"]/img/@src').extract_first()
              item['img_src'] = img_src
      
              yield item
      
    • 管道:

    • # Define your item pipelines here
      #
      # Don't forget to add your pipeline to the ITEM_PIPELINES setting
      # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
      
      
      # useful for handling different item types with a single interface
      import scrapy
      from itemadapter import ItemAdapter
      
      from scrapy.pipelines.images import ImagesPipeline
      class DeepimgproPipeline(ImagesPipeline):
          # def process_item(self, item, spider):
          #     return item
          def get_media_requests(self, item, info):
              img_src = item['img_src']
              #请求传参,将item中的图片名称传递给file_path方法
              #meta会将自身传递给file_path
              print(item['title'],'保存下载成功!')
              yield scrapy.Request(url=img_src,meta={'title':item['title']})
          def file_path(self, request, response=None, info=None, *, item=None):
              #返回图片的名称
              #接收请求传参过来的数据
              title = request.meta['title']
              return title
          def item_completed(self, results, item, info):
              return item
      
      

如何提高scrapy的爬取效率

增加并发:
    默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。

降低日志级别:
    在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为WORNING或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘ERROR’

禁止cookie:
    如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False

禁止重试:
    对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False

减少下载超时:
    如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s

post请求发送

  • 问题:在之前代码中,我们从来没有手动的对start_urls列表中存储的起始url进行过请求的发送,但是起始url的确是进行了请求的发送,那这是如何实现的呢?

    • 解答:其实是因为爬虫文件中的爬虫类继承到了Spider父类中的start_requests(self)这个方法,该方法就可以对start_urls列表中的url发起请求:

    • def start_requests(self):
            for u in self.start_urls:
               yield scrapy.Request(url=u,callback=self.parse)
      
    • 【注意】该方法默认的实现,是对起始的url发起get请求,如果想发起post请求,则需要子类重写该方法。

      • yield scrapy.Request():发起get请求
      • yield scrapy.FormRequest():发起post请求
    • import scrapy
      class FanyiSpider(scrapy.Spider):
          name = 'fanyi'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://fanyi.baidu.com/sug']
          #父类中的方法:该方法是用来给起始的url列表中的每一个url发请求
          def start_requests(self):
              data = {
                  'kw':'dog'
              }
              for url in self.start_urls:
                  #formdata是用来指定请求参数
                  yield scrapy.FormRequest(url=url,callback=self.parse,formdata=data)
          def parse(self, response):
              result = response.json()
              print(result)
      

Scrapy处理cookie

​ 在requests中我们讲解处理cookie主要有两个方案. 第一个方案. 从浏览器里直接把cookie搞出来. 贴到heades里. 这种方案, 简单粗暴. 第二个方案是走正常的登录流程. 通过session来记录请求过程中的cookie. 那么到了scrapy中如何处理cookie? 其实也是这两个方案.

​ 首先, 我们依然是把目标定好, 还是我们的老朋友, https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919

​ 这个url必须要登录后才能访问(用户书架). 对于该网页而言, 就必须要用到cookie了. 首先, 创建项目, 建立爬虫. 把该填的地方填上.

import scrapy
from scrapy import Request, FormRequest


class LoginSpider(scrapy.Spider):
    name = 'login'
    allowed_domains = ['17k.com']
    start_urls = ['https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919']

    def parse(self, response):
        print(response.text)

​ 此时运行时, 显示的是该用户还未登录. 不论是哪个方案. 在请求到start_urls里面的url之前必须得获取到cookie. 但是默认情况下, scrapy会自动的帮我们完成其实request的创建. 此时, 我们需要自己去组装第一个请求. 这时就需要我们自己的爬虫中重写start_requests()方法. 该方法负责起始request的组装工作. 我们不妨先看看原来的start_requests()是如何工作的.

# 以下是scrapy源码

def start_requests(self):
    cls = self.__class__
    if not self.start_urls and hasattr(self, 'start_url'):
        raise AttributeError(
            "Crawling could not start: 'start_urls' not found "
            "or empty (but found 'start_url' attribute instead, "
            "did you miss an 's'?)")
    if method_is_overridden(cls, Spider, 'make_requests_from_url'):
        warnings.warn(
            "Spider.make_requests_from_url method is deprecated; it "
            "won't be called in future Scrapy releases. Please "
            "override Spider.start_requests method instead (see %s.%s)." % (
                cls.__module__, cls.__name__
            ),
        )
        for url in self.start_urls:
            yield self.make_requests_from_url(url)
    else:
        for url in self.start_urls:
            # 核心就这么一句话. 组建一个Request对象.我们也可以这么干. 
            yield Request(url, dont_filter=True)

自己写个start_requests()看看.

def start_requests(self):
    print("我是万恶之源")
    yield Request(
        url=LoginSpider.start_urls[0],
        callback=self.parse
    )

接下来, 我们去处理cookie

1. 方案一, 直接从浏览器复制cookie过来

def start_requests(self):
        # 直接从浏览器复制
        cookies = "GUID=bbb5f65a-2fa2-40a0-ac87-49840eae4ad1; c_channel=0; c_csc=web; Hm_lvt_9793f42b498361373512340937deb2a0=1627572532,1627711457,1627898858,1628144975; accessToken=avatarUrl%3Dhttps%253A%252F%252Fcdn.static.17k.com%252Fuser%252Favatar%252F16%252F16%252F64%252F75836416.jpg-88x88%253Fv%253D1610625030000%26id%3D75836416%26nickname%3D%25E5%25AD%25A4%25E9%25AD%2582%25E9%2587%258E%25E9%25AC%25BCsb%26e%3D1643697376%26s%3D73f8877e452e744c; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2275836416%22%2C%22%24device_id%22%3A%2217700ba9c71257-035a42ce449776-326d7006-2073600-17700ba9c728de%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_referrer_host%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%2C%22first_id%22%3A%22bbb5f65a-2fa2-40a0-ac87-49840eae4ad1%22%7D; Hm_lpvt_9793f42b498361373512340937deb2a0=1628145672"
        cookie_dic = {}
        for c in cookies.split("; "):
            k, v = c.split("=")
            cookie_dic[k] = v

        yield Request(
            url=LoginSpider.start_urls[0],
            cookies=cookie_dic,
            callback=self.parse
        )

这种方案和原来的requests几乎一模一样. 需要注意的是: cookie需要通过cookies参数进行传递!

2. 方案二, 完成登录过程.

    def start_requests(self):
        # 登录流程
        username = "18614075987"
        password = "q6035945"
        url = "https://passport.17k.com/ck/user/login"
		
        # 发送post请求
        # yield Request(
        #     url=url,
        #     method="post",
        #     body="loginName=18614075987&password=q6035945",
        #     callback=self.parse
        # )
        
        # 发送post请求
        yield FormRequest(
            url=url,
            formdata={
                "loginName": username,
                "password": password
            },
            callback=self.parse
        )
	
    def parse(self, response):
        # 得到响应结果. 直接请求到默认的start_urls
        yield Request(
            url=LoginSpider.start_urls[0],
            callback=self.parse_detail
        )

    def parse_detail(self, resp):
        print(resp.text)

​ 注意, 发送post请求有两个方案,

  1. Scrapy.Request(url=url, method='post', body=数据)

  2. Scarpy.FormRequest(url=url, formdata=数据) -> 推荐

    区别: 方式1的数据只能是字符串. 这个就很难受. 所以推荐用第二种.

3. 方案三, 在settings文件中给出cookie值.

​ 在settings中.有一个配置项: DEFAULT_REQUEST_HEADERS, 在里面可以给出默认的请求头信息. 但是要注意, 需要在settings中把COOKIES_ENABLED设置成False. 否则, 在下载器中间件中, 会被干掉.

COOKIES_ENABLED = False

DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  'Cookie': 'xxxxxx',
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
}

scrapy的核心组件

  • 从中可以大致了解scrapy框架的一个运行机制

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

整个工作流程,

1. 爬虫中起始的url构造成request对象, 并传递给调度器. 
2. `引擎`从`调度器`中获取到request对象. 然后交给`下载器`
3. 由`下载器`来获取到页面源代码, 并封装成response对象. 并回馈给`引擎`
4. `引擎`将获取到的response对象传递给`spider`, 由`spider`对数据进行解析(parse). 并回馈给`引擎`
5. `引擎`将数据传递给pipeline进行数据持久化保存或进一步的数据处理. 

中间件

  • scrapy的中间件有两个:

    • 爬虫中间件 (SpiderMiddleware)
    • 下载中间件 (DownloaderMiddleware)
    • 中间件的作用是什么?
      • 观测中间件在五大核心组件的什么位置,根据位置了解中间件的作用
        • 下载中间件位于引擎和下载器之间
        • 引擎会给下载器传递请求对象,下载器会给引擎返回响应对象。
        • 作用:可以拦截到scrapy框架中所有的请求和响应。
          • 拦截请求干什么?
            • 修改请求的ip,修改请求的头信息,设置请求的cookie
          • 拦截响应干什么?
            • 可以修改响应数据
  • 中间件重要方法:

  • # Define here the models for your spider middleware
    #
    # See documentation in:
    # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
    
    from scrapy import signals
    
    # useful for handling different item types with a single interface
    from itemadapter import is_item, ItemAdapter
    
    class MiddleproDownloaderMiddleware:
    
        #类方法:作用是返回一个下载器对象(忽略)
        @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
        #拦截处理所有的请求对象
        #参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
        #spider参数的作用可以实现爬虫类和中间类的数据交互
        def process_request(self, request, spider):
            return None
        #拦截处理所有的响应对象
        #参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
        def process_response(self, request, response, spider):
            return response
        #拦截和处理发生异常的请求对象
        #参数:reqeust就是拦截到的发生异常的请求对象
        def process_exception(self, request, exception, spider):
            pass
        #控制日志数据的(忽略)
        def spider_opened(self, spider):
            spider.logger.info('Spider opened: %s' % spider.name)
    
    

接下来, 我们来说说这几个方法的返回值问题(难点)

  1. process_request(request, spider): 在每个请求到达下载器之前调用

    一, return None 不拦截, 把请求继续向后传递给权重低的中间件或者下载器

    二, return request 请求被拦截, 并将一个新的请求返回. 后续中间件以及下载器收不到本次请求

    三, return response 请求被拦截, 下载器将获取不到请求, 但是引擎是可以接收到本次响应的内容, 也就是说在当前方法内就已经把响应内容获取到了.

  2. proccess_response(request, response, spider): 每个请求从下载器出来调用

    一, return response 通过引擎将响应内容继续传递给其他组件或传递给其他process_response()处理

    二, return request 响应被拦截. 将返回内容直接回馈给调度器(通过引擎), 后续process_response()接收不到响应内容.

开发代理中间件

  • request.meta['proxy'] = proxy

  • # Define here the models for your spider middleware
    #
    # See documentation in:
    # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
    
    from scrapy import signals
    
    # useful for handling different item types with a single interface
    from itemadapter import is_item, ItemAdapter
    from scrapy import Request
    
    class MiddleproDownloaderMiddleware:
        #类方法:作用是返回一个下载器对象(忽略)
        @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
        #拦截处理所有的请求对象
        #参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
        #spider参数的作用可以实现爬虫类和中间类的数据交互
        def process_request(self, request, spider):
            #是的所有的请求都是用代理,则代理操作可以写在该方法中
            request.meta['proxy'] = 'http://ip:port'
            #弊端:会使得整体的请求效率变低
            print(request.url+':请求对象拦截成功!')
            return None
        #拦截处理所有的响应对象
        #参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
        def process_response(self, request, response, spider):
            print(request.url+':响应对象拦截成功!')
            return response
        #拦截和处理发生异常的请求对象
        #参数:reqeust就是拦截到的发生异常的请求对象
        #方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
        def process_exception(self, request, exception, spider):
            print(request.url+':发生异常的请求对象被拦截到!')
            #修正操作
            #只有发生了异常的请求才使用代理机制,则可以写在该方法中
            request.meta['proxy'] = 'https://ip:port'
            return request #对请求对象进行重新发送
        #控制日志数据的(忽略)
        def spider_opened(self, spider):
            spider.logger.info('Spider opened: %s' % spider.name)
    
    

开发UA中间件

  • request.headers['User-Agent'] = ua

  •     def process_request(self, request, spider):
            request.headers['User-Agent'] = '从列表中随机选择的一个UA值'
            print(request.url+':请求对象拦截成功!')
            return None
    

开发Cookie中间件

  • request.cookies = cookies

  • def process_request(self, request, spider):
        request.headers['cookie'] = 'xxx'
        #request.cookies = 'xxx'
        print(request.url+':请求对象拦截成功!')
        return None
    

selenium+scrapy (1)

  • 需求:将网易新闻中的国内,国际,军事,航空四个板块下的新闻标题和内容进行数据爬取

    • 注意:哪些数据是动态加载的!
    • 技术:selenium,scrapy,中间件
  • 分析:

    • 抓取首页中四个板块下所有的新闻标题和新闻内容
      • 获取首页中四个板块对应的详情页链接
        • 首页是没有动态加载数据,可以直接爬取+解析
    • 对每一个板块的url发起请求,获取详情页中的新闻标题等内容
      • 通过分析发现每一个板块中的新闻数据全部是动态加载的数据,如何解决呢?
        • 通过selenium解决
  • scrapy+selenium的编码流程

    • 1.在爬虫文件中定义浏览器对象,将浏览器对象作为爬虫类的一个成员变量
    • 2.在中间件中通过spider获取爬虫文件中定义的浏览器对象,进行请求发送和获取响应数据
    • 3.在爬虫文件中重写一个closed方法,来关闭浏览器对象
  • 爬虫文件

    • import scrapy
      from ..items import WangyiproItem
      from selenium import webdriver
      class WangyiSpider(scrapy.Spider):
          name = 'wangyi'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://news.163.com/']
          #创建浏览器对象,把浏览器对象作为爬虫类的一个成员
          bro = webdriver.Chrome(executable_path='/Users/zhangxiaobo/Desktop/三期/chromedriver1')
      
          model_urls = [] #存储4个板块对应的url
          def parse(self, response):
              #从首页解析每一个板块对应详情页的url,将其存储到model_urls列表中
              model_index = [2,3,5,6]
              li_list = response.xpath('//*[@id="index2016_wrap"]/div[3]/div[2]/div[2]/div[2]/div/ul/li')
              for index in model_index:
                  model_url = li_list[index].xpath('./a/@href').extract_first()
                  self.model_urls.append(model_url)
              #应该对每一个板块的详情页发起请求(动态加载)
              for model_url in self.model_urls:
                  yield scrapy.Request(url=model_url,callback=self.parse_detail)
          #目的是为了解析出每一个板块中的新闻标题和新闻详情页的url
          def parse_detail(self,response):
              #response就是一个不符合需求要求的响应对象
                  #该response中没有存储动态加载的新闻数据,因此该响应对象被视为不符合要求的响应对象
                  #需要将不符合要求的响应对象变为符合要求的响应对象即可,如何做呢?
                  #方法:篡改不符合要求的响应对象的响应数据,将该响应对象的响应数据修改为包含了动态加载的新闻数据即可。
              div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
              for div in div_list:
                  try:
                      #解析新闻标题+新闻详情页的url
                      title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
                      new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
                      item = WangyiproItem()
                      item['title'] = title
                  except Exception as e:
                      print('遇到了广告,忽略此次行为即可!')
      
                  #对新闻的详情页发起请求
                  if new_detail_url != None:
                      yield scrapy.Request(url=new_detail_url,callback=self.new_content_parse,meta={'item':item})
      
          def new_content_parse(self,response):
              item = response.meta['item']
              #解析新闻的详情内容
              content = response.xpath('//*[@id="content"]/div[2]//text()').extract()
              content = ''.join(content).strip()
              item['content'] = content
      
              yield item
      
          #重写一个父类方法,close_spider,该方法只会在爬虫最后执行一次
          def closed(self,spider):
              #关闭浏览器
              print('关闭浏览器成功!')
              self.bro.quit()
      
      
  • 中间件文件:

    • # Define here the models for your spider middleware
      #
      # See documentation in:
      # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
      import requests
      from scrapy import signals
      
      # useful for handling different item types with a single interface
      from itemadapter import is_item, ItemAdapter
      from time import sleep
      from scrapy.http import HtmlResponse#scrapy封装的响应对象对应的类
      class WangyiproDownloaderMiddleware:
      
      
          def process_request(self, request, spider):
      
              return None
      
          def process_response(self, request, response, spider):
              #可以拦截到所有的响应对象
              #当前项目一共会产生多少个响应对象呢?
               #1 + 4 + n个响应对象,在这些响应对象中只有4这4个响应对象需要被修改
              #如何筛选出指定的4个板块对应的响应对象呢?
                  #1.可以先找出指定4个板块的请求对象,然后根据请求对象定位指定4个响应对象
                  #2.可以根据4个板块的url定位到四个板块的请求对象
              model_urls = spider.model_urls
              if request.url in model_urls:
                  bro = spider.bro #从爬虫类中获取创建好的浏览器对象
                  bro.get(request.url)
                  sleep(1)
                  # bro.execute_script('document.documentElement.scrollTo(0,9000)')
                  # sleep(1)
                  #获取动态加载的数据
                  page_text = bro.page_source
                  #说明该request就是指定响应对象的请求对象
                  #此处的response就是指定板块对应的响应对象
                  response = HtmlResponse(url=request.url,
                                          request=request,
                                          encoding='utf-8',
                                          body=page_text)
                                      #body就是响应对象的响应数据
                  return response
              else:
                  return response
      
          def process_exception(self, request, exception, spider):
             pass
      
      
  • 配置文件:

    • DOWNLOADER_MIDDLEWARES = {
          'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
      
      }
      
  • 拓展功能:将人工智能+数据爬取中

    • 实现将爬取到的新闻进行分类和关键字提取

      • 百度AI的使用:https://ai.baidu.com/

        • 使用流程:

          • 点击首页右上角的控制台,进行登录。

          • 登录后进入到了智能云的首页

            • 点击页面左上角的三条杠,选择你想要实现的功能,点击,进入到指定功能页面

              • 在功能页面,首先点击【创建应用】,进行应用的创建
              • 创建好之后,点击管理应用就可以看到:
                • AppID,apiKey,secret key这三个值,会在程序中用到
            • 在功能页面点击左侧的【技术文档】,选择SDK说明,选择对应的Python语言即可,先看快速开始内容,在选择你想要实现的具体功能的文档界面即可。

              • 环境安装:pip install baidu-aip
            • 提取文章关键字:

              • from aip import AipNlp
                
                """ 你的 APPID AK SK """
                APP_ID = 'xxx'
                API_KEY = 'xxx'
                SECRET_KEY = 'xxx'
                
                client = AipNlp(APP_ID, API_KEY, SECRET_KEY)
                
                title = "iphone手机出现“白苹果”原因及解决办法,用苹果手机的可以看下"
                
                content = "如果下面的方法还是没有解决你的问题建议来我们门店看下成都市锦江区红星路三段99号银石广场24层01室。"
                
                """ 调用文章标签 """
                result = client.keyword(title, content)
                for dic in result['items']:
                    if dic['score'] >= 0.8:
                        key = dic['tag']
                        print(key)
                
            • 文章分类:

              • from aip import AipNlp
                
                """ 你的 APPID AK SK """
                APP_ID = 'xxx'
                API_KEY = 'x'
                SECRET_KEY = 'xx'
                
                client = AipNlp(APP_ID, API_KEY, SECRET_KEY)
                
                title = "秦刚访问特斯拉美国工厂马斯克陪同 传递什么信号?"
                
                content = "今天(4日),驻美大使秦刚访问了特斯拉硅谷工厂,同特斯拉CEO马斯克针对各项尖端科技、人类未来等主题展开探讨,并体验了特斯拉的新款Model S及最新自动辅助驾驶系统。他在海外社交平台上表示:“性能强劲,但乘坐平顺舒适”。"
                
                """ 调用文章分类 """
                result = client.topic(title, content)
                class_new = result['item']['lv1_tag_list'][0]['tag']
                print(class_new)
                

selenium+scrapy(2)

另一中写法:首先, 我们需要使用selenium作为下载器进行下载. 那么我们的请求应该也是特殊订制的. 所以, 在我的设计里, 我可以重新设计一个请求. 就叫SeleniumRequest

from scrapy.http.request import Request

class SeleniumRequest(Request):
    pass

这里面不需要做任何操作. 整体还是用它父类的东西来进行操作.

接下来. 完善一下spider

import scrapy
from boss.request import SeleniumRequest

class BeijingSpider(scrapy.Spider):
    name = 'beijing'
    allowed_domains = ['zhipin.com']
    start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position=']

    def start_requests(self):
        yield SeleniumRequest(
            url=BeijingSpider.start_urls[0],
            callback=self.parse,
        )

    def parse(self, resp, **kwargs):
        li_list = resp.xpath('//*[@id="main"]/div/div[3]/ul/li')
        for li in li_list:
            href = li.xpath("./div[1]/div[1]/div[1]/div[1]/div[1]/span[1]/a[1]/@href").extract_first()
            name = li.xpath("./div[1]/div[1]/div[1]/div[1]/div[1]/span[1]/a[1]/text()").extract_first()

            print(name, href)
            print(resp.urljoin(href))
            yield SeleniumRequest(
                url=resp.urljoin(href),
                callback=self.parse_detail,
            )
        # 下一页.....

    def parse_detail(self, resp, **kwargs):
        print("招聘人", resp.xpath('//*[@id="main"]/div[3]/div/div[2]/div[1]/h2').extract())


中间件~

class BossDownloaderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        # 这里很关键哦. 
        # 在爬虫开始的时候. 执行spider_opened
        # 在爬虫结束的时候. 执行spider_closed
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
        return s

    def process_request(self, request, spider):
        if isinstance(request, SeleniumRequest):
            self.web.get(request.url)
            time.sleep(3)
            page_source = self.web.page_source
            return HtmlResponse(url=request.url, encoding='utf-8', request=request, body=page_source)

    def process_response(self, request, response, spider):
        return response

    def process_exception(self, request, exception, spider):
        pass

    def spider_opened(self, spider):
        self.web = Chrome()
        self.web.implicitly_wait(10)
        # 完成登录. 拿到cookie. 很容易...
        print("创建浏览器")

    def spider_closed(self, spider):
        self.web.close()
        print("关闭浏览器")

settings

DOWNLOADER_MIDDLEWARES = {
    # 怼在所有默认中间件前面. 只要是selenium后面所有的中间件都给我停
   'boss.middlewares.BossDownloaderMiddleware': 99,  
}

CrawlSpider

  • 实现网站的全站数据爬取

    • 就是将网站中所有页码对应的页面数据进行爬取。
  • crawlspider其实就是scrapy封装好的一个爬虫类,通过该类提供的相关的方法和属性就可以实现全新高效形式的全站数据爬取。

  • 使用流程:

    • 新建一个scrapy项目

    • cd 项目

    • 创建爬虫文件(*):

      • scrapy genspider-t crawl spiderName www.xxx.com

      • 爬虫文件中发生的变化有哪些?

        • 当前爬虫类的父类为CrawlSpider
        • 爬虫类中多了一个类变量叫做rules
          • LinkExtractor:链接提取器
            • 可以根据allow参数表示的正则在当前页面中提取符合正则要求的链接
          • Rule:规则解析器
            • 可以接收链接提取器提取到的链接,并且对每一个链接进行请求发送
            • 可以根据callback指定的回调函数对每一次请求到的数据进行数据解析
        • 思考:如何将一个网站中所有的链接都提取到呢?
          • 只需要在链接提取器的allow后面赋值一个空正则表达式即可
        • 目前在scrapy中有几种发送请求的方式?
          • start_urls列表可以发送请求
          • scrapy.Request()
          • scrapy.FormRequest()
          • Rule规则解析器
  • 注意:

    • 链接提取器和规则解析器是一一对应的(一对一的关系)
    • 建议在使用crawlSpider实现深度爬取的时候,需要配合手动请求发送的方式进行搭配!
  • USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'
    

一. 使用常规Spider

我们把目光对准汽车之家. 抓取二手车信息.

注意, 汽车之家的访问频率要控制一下. 要不然会跳验证的.

DOWNLOAD_DELAY = 3
class ErshouSpider(scrapy.Spider):
    name = 'ershou'
    allowed_domains = ['che168.com']
    start_urls = ['https://www.che168.com/china/a0_0msdgscncgpi1ltocsp1exx0/']

    def parse(self, resp, **kwargs):
        # print(resp.text)
        # 链接提取器
        le = LinkExtractor(restrict_xpaths=("//ul[@class='viewlist_ul']/li/a",), deny_domains=("topicm.che168.com",) )
        links = le.extract_links(resp)
        for link in links:
            yield scrapy.Request(
                url=link.url,
                callback=self.parse_detail
            )
        # 翻页功能
        le2 = LinkExtractor(restrict_xpaths=("//div[@id='listpagination']/a",))
        pages = le2.extract_links(resp)
        for page in pages:
            yield scrapy.Request(url=page.url, callback=self.parse)

    def parse_detail(self, resp, **kwargs):
        title = resp.xpath('/html/body/div[5]/div[2]/h3/text()').extract_first()
        print(title)

LinkExtractor: 链接提取器. 可以非常方便的帮助我们从一个响应页面中提取到url链接. 我们只需要提前定义好规则即可.

参数:

​ allow, 接收一堆正则表达式, 可以提取出符合该正则的链接
​ deny, 接收一堆正则表达式, 可以剔除符合该正则的链接
​ allow_domains: 接收一堆域名, 符合里面的域名的链接被提取
​ deny_domains: 接收一堆域名, 剔除不符合该域名的链接
​ restrict_xpaths: 接收一堆xpath, 可以提取符合要求xpath的链接
​ restrict_css: 接收一堆css选择器, 可以提取符合要求的css选择器的链接
​ tags: 接收一堆标签名, 从某个标签中提取链接, 默认a, area
​ attrs: 接收一堆属性名, 从某个属性中提取链接, 默认href

值得注意的, 在提取到的url中, 是有重复的内容的. 但是我们不用管. scrapy会自动帮我们过滤掉重复的url请求.

二. 使用CrawlSpider

在scrapy中提供了CrawlSpider来完成全站数据抓取.

  1. 创建项目

    scrapy startproject qichezhijia
    
  2. 进入项目

    cd qichezhijia
    
  3. 创建爬虫(CrawlSpider)

    scrapy genspider -t crawl ershouche che168.com
    

    和以往的爬虫不同. 该爬虫需要用到crawl的模板来创建爬虫.

  4. 修改spider中的rules和回调函数

    class ErshoucheSpider(CrawlSpider):
        name = 'ershouche'
        allowed_domains = ['che168.com', 'autohome.com.cn']
        start_urls = ['https://www.che168.com/beijing/a0_0msdgscncgpi1ltocsp1exx0/']
    
        le = LinkExtractor(restrict_xpaths=("//ul[@class='viewlist_ul']/li/a",), deny_domains=("topicm.che168.com",) )
        le1 = LinkExtractor(restrict_xpaths=("//div[@id='listpagination']/a",))
        rules = (
            Rule(le1, follow=True),  # 单纯为了做分页 ;follow:True, 当前被提取到的链接是否把所有规则重新走一遍
            Rule(le, callback='parse_item', follow=False), # 单纯提取数据
        )
    
        def parse_item(self, response):
            print(response.url)
    

    CrawlSpider的工作流程.

    前期和普通的spider是一致的. 在第一次请求回来之后. 会自动的将返回的response按照rules中订制的规则来提取链接. 并进一步执行callback中的回调. 如果follow是True, 则继续在响应的内容中继续使用该规则提取链接. 相当于在parse中的scrapy.request(xxx, callback=self.parse)

分布式

  • 分布式在日常开发中并不常用,只是一个噱头!

  • 概念:

    • 可以使用多台电脑搭建一个分布式机群,使得多台对电脑可以对同一个网站的数据进行联合且分布的数据爬取。
  • 声明:

    • 原生的scrapy框架并无法实现分布式操作!why?
      • 多台电脑之间无法共享同一个调度器
      • 多台电脑之间无法共享同一个管道
  • 如何是的scrapy可以实现分布式呢?

    • 借助于一个组件:scrapy-redis
    • scrapy-redis的作用是什么?
      • 可以给原生的scrapy框架提供可被共享的调度器和管道!
      • 环境安装:pip install scrapy-redis
        • 注意:scrapy-redis该组件只可以将爬取到的数据存储到redis数据库
  • 编码流程(重点):

    • 1.创建项目

    • 2.cd 项目

    • 3.创建基于crawlSpider的爬虫文件

      • 3.1 修改爬虫文件
        • 导包:from scrapy_redis.spiders import RedisCrawlSpider
        • 修改当前爬虫类的父类为 RedisCrawlSpider
        • 将start_urls替换成redis_key的操作
          • redis_key变量的赋值为字符串,该字符串表示调度器队列的名称
        • 进行常规的请求操作和数据解析
    • 4.settings配置文件的修改

      • 常规内容修改(robots和ua等),先不指定日志等级

      • 指定可以被共享的管道类

        • ITEM_PIPELINES = {
              'scrapy_redis.pipelines.RedisPipeline': 400
          }
          
      • 指定可以被共享的调度器

        • # 使用scrapy-redis组件的去重队列
          DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
          # 使用scrapy-redis组件自己的调度器
          SCHEDULER = "scrapy_redis.scheduler.Scheduler"
          # 是否允许暂停
          SCHEDULER_PERSIST = True
          
      • 指定数据库

        • REDIS_HOST = '127.0.0.1'
          REDIS_PORT = 6379
          
    • 5.修改redis数据库的配置文件(redis.windows.conf)

      • 在配置文件中改行代码是没有没注释的:

        • bind 127.0.0.1
          #将上述代码注释即可(解除本机绑定,实现外部设备访问本机数据库
          
          如果配置文件中还存在:protected-mode = true,将true修改为false,
          修改为false后表示redis数据库关闭了保护模式,表示其他设备可以远程访问且修改你数据库中的数据
          
    • 6.启动redis数据库的服务端和客户端

    • 7.运行项目,发现程序暂定一直在等待,等待爬取任务

    • 8.需要向可以被共享的调度器的队列(redis_key的值)中放入一个起始的url

增量式

  • 爬虫应用场景分类

    • 通用爬虫
    • 聚焦爬虫
    • 功能爬虫
    • 分布式爬虫
    • 增量式:
      • 用来监测网站数据更新的情况(爬取网站最新更新出来的数据)。
      • 只是一种程序设计的思路,使用什么技术都是可以实现的。
      • 核心:
        • 去重。
          • 使用一个记录表来实现数据的去重:
            • 记录表:存储爬取过的数据的记录
            • 如何构建和设计一个记录表:
              • 记录表需要具备的特性:
                • 去重
                • 需要持久保存的
              • 方案1:使用Python的set集合充当记录表?
                • 不可以的!因为set集合无法实现持久化存储
              • 方案2:使用redis的set集合充当记录表?
                • 可以的,因为redis的set既可以实现去重又可以进行数据的持久化存储。
  • 基于两个场景实现增量式爬虫:

    • 场景1:如果爬取的数据都是存储在当前网页中,没有深度的数据爬取的必要。
    • 场景2:爬取的数据存在于当前页和详情页中,具备深度爬取的必要。
  • 场景1的实现:

    • 数据指纹:

      • 数据的唯一标识。记录表中可以不直接存储数据本身,直接存储数据指纹更好一些。

      • #爬虫文件
        import scrapy
        import redis
        from ..items import Zlsdemo1ProItem
        class DuanziSpider(scrapy.Spider):
            name = 'duanzi'
            # allowed_domains = ['www.xxxx.com']
            start_urls = ['https://ishuo.cn/']
            #Redis的链接对象
            conn = redis.Redis(host='127.0.0.1',port=6379)
        
            def parse(self, response):
                li_list = response.xpath('//*[@id="list"]/ul/li')
                for li in li_list:
                    content = li.xpath('./div[1]/text()').extract_first()
                    title = li.xpath('./div[2]/a/text()').extract_first()
                    all_data = title+content
                    #生成该数据的数据指纹
                    import hashlib  # 导入一个生成数据指纹的模块
                    m = hashlib.md5()
                    m.update(all_data.encode('utf-8'))
                    data_id = m.hexdigest()
        
                    ex = self.conn.sadd('data_id',data_id)
                    if ex == 1:#sadd执行成功(数据指纹在set集合中不存在)
                        print('有最新数据的更新,正在爬取中......')
                        item = Zlsdemo1ProItem()
                        item['title'] = title
                        item['content'] = content
                        yield item
                    else:#sadd没有执行成功(数据指纹在set集合中存储)
                        print('暂无最新数据更新,请等待......')
        
  • 场景2的实现:

    • 使用详情页的url充当数据指纹即可。

    • import scrapy
      import redis
      from ..items import Zlsdemo2ProItem
      class JianliSpider(scrapy.Spider):
          name = 'jianli'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://sc.chinaz.com/jianli/free.html']
          conn = redis.Redis(host='127.0.0.1',port=6379)
          def parse(self, response):
              div_list = response.xpath('//*[@id="container"]/div')
              for div in div_list:
                  title = div.xpath('./p/a/text()').extract_first()
                  #充当数据指纹
                  detail_url = 'https:'+div.xpath('./p/a/@href').extract_first()
                  ex = self.conn.sadd('data_id',detail_url)
                  item = Zlsdemo2ProItem()
                  item['title'] = title
                  if ex == 1:
                      print('有最新数据的更新,正在采集......')
                      yield scrapy.Request(url=detail_url,callback=self.parse_detail,meta={'item':item})
                  else:
                      print('暂无数据更新!')
      
          def parse_detail(self,response):
              item = response.meta['item']
              download_url = response.xpath('//*[@id="down"]/div[2]/ul/li[1]/a/@href').extract_first()
              item['download_url'] = download_url
      
              yield item
      

下面,我们再以天涯为目标来尝试一下完成增量式爬虫.
增量爬虫的核心:去除重复,

spider:

import scrapy
from redis import Redis
from tianya.items import TianyaItem


class TySpider(scrapy.Spider):

    name = 'ty'
    allowed_domains = ['tianya.cn']
    start_urls = ['http://bbs.tianya.cn/list-worldlook-1.shtml']

    def __init__(self, name=None, **kwargs):
        self.red = Redis(password="123456", db=6, decode_responses=True)
        super().__init__(name, **kwargs)

    def parse(self, resp, **kwargs):
        tbodys = resp.css(".tab-bbs-list tbody")[1:]
        for tbody in tbodys:
            hrefs = tbody.xpath("./tr/td[1]/a/@href").extract()
            for h in hrefs:
                # 两个方案.
                url = resp.urljoin(h)
                # 判断是否在该set集合中有数据
                r = self.red.sismember("tianya:details", url)  
                #   1. url去重. 优点: 简单, 缺点: 如果有人回复了帖子.就无法提取到最新的数据了
                if not r:
                    yield scrapy.Request(url=resp.urljoin(h), callback=self.parse_details)
                else:
                    print(f"该url已经被抓取过{url}")

        next_href = resp.xpath("//div[@class='short-pages-2 clearfix']/div[@class='links']/a[last()]/@href").extract_first()
        yield scrapy.Request(url=resp.urljoin(next_href), callback=self.parse)

    def parse_details(self, resp, **kwargs):
        title = resp.xpath('//*[@id="post_head"]/h1/span[1]/span/text()').extract_first()
        content = resp.xpath('//*[@id="bd"]/div[4]/div[1]/div/div[2]/div[1]/text()').extract_first()
        item = TianyaItem()
        item['title'] = title
        item['content'] = content
        # 提取完数据. 该url进入redis
        self.red.sadd("tianya:details", resp.url)  
        return item

​ pipelines

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
from redis import Redis
import json

class TianyaPipeline:

    def process_item(self, item, spider):
        #   2. 数据内容去重. 优点: 保证数据的一致性. 缺点: 需要每次都把数据从网页中提取出来
        print(json.dumps(dict(item)))
        r = self.red.sadd("tianya:pipelines:items", json.dumps(dict(item)))
        if r:
            # 进入数据库
            print("存入数据库", item['title'])
        else:
            print("已经在数据里了", item['title'])
        return item

    def open_spider(self, spider):
        self.red = Redis(password="123456", db=6)

    def close_spider(self, spider):
        self.red.close()

上述方案是直接用redis进行的去重. 我们还可以选择使用数据库, mongodb进行过滤. 原理都一样, 不在赘述.

scrapy项目部署

scrapyd部署工具介绍

  • scrapyd是一个用于部署和运行scrapy爬虫的程序,它由 scrapy 官方提供的。它允许你通过JSON API来部署爬虫项目和控制爬虫运行

所谓json api本质就是post请求的webapi

  • 选择一台主机当做服务器,安装并启动 scrapyd 服务。再这之后,scrapyd 会以守护进程的方式存在系统中,监听爬虫地运行与请求,然后启动进程来执行爬虫程序。

环境安装

  • scrapyd服务:

pip install scrapyd

  • scrapyd客户端:

pip install scrapyd-client

​ 一定要安装较新的版本10以上的版本,如果是现在安装的一般都是新版本

启动scrapyd服务

  • 打开终端在scrapy项目路径下 启动scrapyd的命令: scrapyd

Snip20220308_13

  • scrapyd 也提供了 web 的接口。方便我们查看和管理爬虫程序。默认情况下 scrapyd 监听 6800 端口,运行 scrapyd 后。在本机上使用浏览器访问 http://localhost:6800/地址即可查看到当前可以运行的项目。

Snip20220308_14

  • 点击job可以查看任务监控界面

Snip20220308_16

scrapy项目部署

配置需要部署的项目

  • 编辑需要部署的项目的scrapy.cfg文件(需要将哪一个爬虫部署到scrapyd中,就配置该项目的该文件)

Snip20220308_17

[deploy:部署名(部署名可以自行定义)] 
url = http://localhost:6800/ 
project = 项目名(创建爬虫项目时使用的名称)

username = bobo # 如果不需要用户名可以不写
password = 123456 # 如果不需要密码可以不写

部署项目到scrapyd

  • 同样在scrapy项目路径下执行如下指令:

    scrapyd-deploy 部署名(配置文件中设置的名称) -p 项目名称
    
  • 部署成功之后就可以看到部署的项目

Snip20220308_18

  • 使用以下命令检查部署爬虫结果:

    • scrapyd-deploy -L 部署名
      

管理scrapy项目

指令管理

  • 安装curl命令行工具

    • window需要安装
    • linux和mac无需单独安装
  • window安装步骤:

    Snip20220308_19

  • 启动项目:

    curl http://localhost:6800/schedule.json -d project=项目名 -d spider=爬虫名
    
    • 返回结果:注意期中的jobid,在关闭项目时候会用到

      • {"status": "ok", "jobid": "94bd8ce041fd11e6af1a000c2969bafd", "node_name": "james-virtual-machine"}
        
  • 关闭项目:

    • curl http://localhost:6800/cancel.json -d project=项目名 -d job=项目的jobid
      
  • 删除爬虫项目:

    • curl http://localhost:6800/delproject.json -d project=爬虫项目名称
      

requests模块控制scrapy项目

import requests

# 启动爬虫
url = 'http://localhost:6800/schedule.json'
data = {
	'project': 项目名,
	'spider': 爬虫名,
}
resp = requests.post(url, data=data)

# 停止爬虫
url = 'http://localhost:6800/cancel.json'
data = {
	'project': 项目名,
	'job': 启动爬虫时返回的jobid,
}
resp = requests.post(url, data=data)

生产者消费者模式

认识生产者和消费者模式

生产者和消费者是异步爬虫中很常见的一个问题。产生数据的模块,我们称之为生产者,而处理数据的模块,就称为消费者。

例如:

​ 图片数据爬取中,解析出图片链接的操作就是在生产数据

​ 对图片链接发起请求下载图片的操作就是在消费数据

为什么要使用生产者和消费者模式

​ 在异步世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

import requests
import threading
from lxml import etree
from queue import Queue
from urllib.request import urlretrieve
from time import sleep
headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
    }

#生产数据:解析提取图片地址
class Producer(threading.Thread):#生产者线程
    def __init__(self,page_queue,img_queue):
        super().__init__()
        self.page_queue = page_queue
        self.img_queue = img_queue
    def run(self):
        while True:
            if self.page_queue.empty():
                print('Producer任务结束')
                break
            #从page_queue中取出一个页码链接
            url = self.page_queue.get()
            #从当前的页码对应的页面中解析出更多的图片地址
            self.parse_detail(url)
    def parse_detail(self,url):
        response = requests.get(url,headers=headers)
        response.encoding = 'gbk'
        page_text = response.text
        tree = etree.HTML(page_text)
        li_list = tree.xpath('//*[@id="main"]/div[3]/ul/li')
        for li in li_list:
            img_src = 'https://pic.netbian.com'+li.xpath('./a/img/@src')[0]
            img_title = li.xpath('./a/b/text()')[0]+'.jpg'
            dic = {
                'title':img_title,
                'src':img_src
            }
            self.img_queue.put(dic)

#消费数据:对图片地址进行数据请求
class Consumer(threading.Thread):#消费者线程
    def __init__(self,page_queue,img_queue):
        super().__init__()
        self.page_queue = page_queue
        self.img_queue = img_queue
    def run(self):
        while True:
            if self.img_queue.empty() and self.page_queue.empty():
                print('Consumer任务结束')
                break
            dic = self.img_queue.get()
            title = dic['title']
            src = dic['src']
            print(src)
            urlretrieve(src,'imgs/'+title)
            print(title,'下载完毕!')

def main():
    #该队列中存储即将要要去的页面页码链接
    page_queue = Queue(20)
    #该队列存储生产者生产出来的图片地址
    img_queue = Queue(60)

    #该循环可以将2,3,4这三个页码链接放入page_queue中
    for x in range(2,10):
        url = 'https://pic.netbian.com/4kmeinv/index_%d.html'%x
        page_queue.put(url)

    #生产者
    for x in range(3):
        t = Producer(page_queue,img_queue)
        t.start()
    #消费者
    for x in range(3):
        t = Consumer(page_queue,img_queue)
        t.start()

main()
posted @ 2022-08-17 16:39  hanfe1  阅读(175)  评论(0编辑  收藏  举报