爬虫进阶(基础二)

Scrapy

  • 继续爬虫进阶基础部分
  • Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名
  • 所谓的框架就是一个已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)的具有很强通用性的项目模板
  • 直接帮我们搭建出个爬虫项目来,避免了很多底层的繁琐工作;当然,一般对于大一些的项目才会用
  • 对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可
  • 安装(Windows)
     pip install wheel	# 为了安装离线包
     # 下载对应版本的 twisted 文件 http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
     pip install Twisted-xxx
     pip install pywin32	# Windows用户需要
     pip install scrapy
    
  • 创建项目
    • 命令行:scrapy startproject 项目名称
    • 进入项目目录:scrapy genspider first www.xxx.com 就会生成名为 first 的爬虫文件
    • 启动爬虫(项目内),命令行:scrapy crawl first --nolog--nolog 一般不用
  • 一般会修改配置文件 settings
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    
    # 只看错误日志
    LOG_LEVEL = 'ERROR'
    
    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 对于文本图片的解析是不一样的,先看文本
  • 获取到的是数据对象,提取数据需要用 extract_first()
    import scrapy
    
    class FirstSpider(scrapy.Spider):
        name = 'first'
        # allowed_domains = ['www.xxx.com']
        #对首页进行网络请求
        #scrapy会对列表中的url发起get请求
        start_urls = ['https://www.wogif.com/duanzi/']  # https://ishuo.cn/duanzi
    
        def parse(self, response):
            #获取响应数据
            #调用xpath方法对响应数据进行xpath形式的数据解析
            li_list = response.xpath('/html/body/div[2]/div[2]/div')
            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/h2/a/text()')
                #content = li.xpath('./div[1]/text()')
                #extract_first()可以将列表中第0个列表元素表示的selector对象中data的参数值取出
                print(title.extract_first())
                #print(content.extract_first())
    

持久化存储

  • 基于终端命令的持久化存储
    • 需要在 parse 中将爬取到的数据全部封装到返回值
      class FirstSpider(scrapy.Spider):
          name = 'first'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://www.wogif.com/duanzi/']  # https://ishuo.cn/duanzi
      
          def parse(self, response):
              li_list = response.xpath('/html/body/div[2]/div[2]/div')
              # 全部封装到这里
              all = []
              for li in li_list:
                  title = li.xpath('./div/h2/a/text()')
                  # print(title.extract_first())
                  # 每条数据用字典格式装每个字段
                  dic = {
                      'title': title.extract_first()
                  }
                  all.append(dic)
              return all
      
    • 命令:scrapy crawl first -o title.csv
    • 优点:简单,便捷
    • 缺点:局限性强
      • 只可以将数据存储到文本文件无法写入数据库
      • 数据文件后缀是指定好的,通常使用 .csv
      • 需要将存储的数据封装到 parse 方法的返回值中
    • 一般小问题才会用指令方式
  • 基于管道实现持久化存储(推荐)
    • 步骤复杂一些,为了灵活嘛
      • 在爬虫文件中进行数据解析
      • items.py 文件中定义相关的字段,解析到的数据将封装到 Item 类型的对象中
      • 在爬虫文件中,将 item 对象提交给管道
      • 在管道 pipelines.py 中接收 item 类型对象
      • 对接收到的数据进行任意形式的持久化存储操作
      • 在配置文件中开启管道机制
    • first.py
      import scrapy
      from ..items import FirstprojectItem
      
      class FirstSpider(scrapy.Spider):
          name = 'first'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://www.wogif.com/duanzi/']  # https://ishuo.cn/duanzi
      
          def parse(self, response):
              li_list = response.xpath('/html/body/div[2]/div[2]/div')
              for li in li_list:
                  title = li.xpath('./div/h2/a/text()').extract_first()
                  item = FirstprojectItem()  # 一条数据一个对象
                  item['title'] = title
                  yield item  # 提交给管道
      
    • items.py
      import scrapy
      
      class FirstprojectItem(scrapy.Item):
          # define the fields for your item here like:
          # name = scrapy.Field()
          title = scrapy.Field()
      
    • pipelines.py,灵活保存数据
      # 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 FirstprojectPipeline:
          # 重写父类的方法
          fp = None
      
          def open_spider(self, spider):
              print('open_spider方法,我在项目开始运行环节,只会被执行一次!')
              self.fp = open('tit.txt', 'w', encoding='utf-8')
      
          # process_item方法调用的次数取决于爬虫文件给其提交item的次数
          def process_item(self, item, spider):
              # 管道接收到first传来的 item 对象
              title = item['title']
              self.fp.write(title+'\n')
              print(title, ':爬取保存成功!')
              return item
      
          def close_spider(self, spider):
              print('在爬虫结束的时候会被执行一次!')
              self.fp.close()
      
      
    • settings.py,开启管道模式
      ITEM_PIPELINES = {
         'firstproject.pipelines.FirstprojectPipeline': 300,
      }
      

深入管道

  • 将爬取到的数据存到三种数据库
    • 通过编写并配置不同管道实现
      ITEM_PIPELINES = {
         # 依次通过各管道,数字越小,优先级越高
         'xiaoshuo.pipelines.MySQLPipeline': 300,
         'xiaoshuo.pipelines.RedisPipeline': 301,
         'xiaoshuo.pipelines.MongoPipeline': 302,
      }
      
  • MySQL
    • 新建表
      CREATE table xiaoshuo(
      	id INT PRIMARY KEY auto_increment,
      	title VARCHAR(100)
      )
      
    • 编写管道
      import pymysql
      
      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',
                  p_w_d='',
                  db='scrapy',
                  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 # 高级管道,需要return item给低级管道
          def close_spider(self,spider):
              # 关闭连接
              self.cursor.close()
              self.conn.close()
      
  • Redis
    • 爬虫文件只会将 item 提交给优先级最高的那一个管道类
    • 优先级最高的管道类的 process_item 中需要写 return item 操作,将 item 对象传递给下一个管道类
    • 编写管道
      # 将数据持久化存储到redis中
      class RedisPipeline:
          conn = None
          def open_spider(self, spider):
              # 在连接需要手动启动redis的服务
              self.conn = redis.Redis(
                  host='192.168.109.128',
                  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)    # xiaoshuo 是个列表,直接存字典元素进去
              self.conn.lpush('xiaoshuo', item['title'])
              print('数据存储redis成功!')
              return item
      
    • 启动 Redis 前要修改配置
      # 注释掉
      # bind 127.0.0.1
      # 改为 no
      protected-mode no
      
  • MongoDB
    • 编写管道
      # 将数据持久化存储到mongo中
      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):
              # collection:xiaoshuo
              self.db_sanqi['xiaoshuo'].insert_one({'title':item['title']})   # 不能直接插入字典
              print('插入成功!')
              return item
      
  • 任务结束!注意不同数据库的核心概念不一样~

爬取图片

  • scrapy 中爬取图片数据需要继承它的 ImagesPipeline
  • 先获取图片地址,在方法重写时发起 Request 请求
  • Item 对象
    class ImageSpider(scrapy.Spider):
        name = 'image'
        allowed_domains = ['www.example.com']
        start_urls = ['https://pic.netbian.com/']
    
        def parse(self, response):
            addr = response.xpath('//*[@id="main"]/div[3]/ul/li')
            for i in addr:
                src = 'https://pic.netbian.com/'+i.xpath('./a/span/img/@src').extract_first()
                # print(src)
                # 每个图片都需要一个item对象
                item = ImgItem()
                item['src'] = src
                yield item
    
  • 编写管道
    import scrapy
    from scrapy.pipelines.images import ImagesPipeline
    
    # 自定义的管道类一定要继承ImagesPipeline
    class ImgPipeline(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  # 传递给下一个管道类
    
  • 配置保存地址
    IMAGES_STORE = 'imgs'
    

爬取多页

  • scrapy 默认在 start_urls 中发起请求,但如果将多个页面加到这里,不好管理(太low)
  • 直接在 parse 中操作,修改请求地址,解析函数还是回调 parse
    import scrapy
    from ..items import ImgItem
    
    class ImageSpider(scrapy.Spider):
        name = 'image'
        allowed_domains = ['www.example.com']
        start_urls = ['https://pic.netbian.com/']
    
        # 请求多页
        _url = 'https://pic.netbian.com/index_%d.html'
        _page_num = 1
        def parse(self, response):
            addr = response.xpath('//*[@id="main"]/div[3]/ul/li')
            for i in addr:
                # //*[@id="main"]/div[3]/ul/li[1]/a/img
                if self._page_num == 1:
                    src = 'https://pic.netbian.com/'+i.xpath('./a/span/img/@src').extract_first()
                else:
                    src = 'https://pic.netbian.com/' + i.xpath('./a/img/@src').extract_first()
                # print(src)
                item = ImgItem()
                item['src'] = src
                print(src)
                yield item
    
            if self._page_num < 3:
                # 请求新地址,并回调函数解析
                self._page_num += 1
                new = self._url%(self._page_num)
                print("新地址:", new)
                yield scrapy.Request(url=new, callback=self.parse, dont_filter=True)	# dont_filter,和 allowed_domains 有关
    
  • 如果遇到问题,可以调整日志级别,查看详细信息

请求传参

  • 很多页面标题和详情是分开的,我们获取 title 之后进入详情页的 parse_detail 方法,如何把已经实例化的 Item 对象带过去呢?就需要用到 meta 请求传参
    import scrapy
    from ..items import ArticleItem
    
    class ArtiSpider(scrapy.Spider):
        name = 'arti'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://wz.sun0769.com/political/index/politicsNewest']
    
        # 多页
        page_url = 'https://wz.sun0769.com/political/index/politicsNewest?id=1&page=%d'
        page_num = 1
        # 解析首页数据
        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 = ArticleItem()
                item['title'] = title
                # 对详情页的url发起请求
                # 参数meta可以将自身这个字典传递给callback指定的回调函数
                yield scrapy.Request(meta={'item': item}, url=detail_url, callback=self.parse_detail)
            if self.page_num < 3:
                self.page_num += 1
                new_url = self.page_url%self.page_num
                print("新的一页:", new_url)
                # 对新的一页发起请求
                yield scrapy.Request(url=new_url, callback=self.parse)
    
        # 解析文章详情页数据
        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)
            item['content'] = content
            yield item
    
  • 即请求传参是在不同解析函数之间

post 请求

  • scrapy.Request() 发起 get 请求
  • scrapy.FormRequest() 发起 post 请求
  • 请求百度翻译的接口
    import scrapy
    class FanyiSpider(scrapy.Spider):
        name = 'fanyi'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://fanyi.baidu.com/sug']
        #父类Spider中的方法:该方法是用来给起始的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开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改 CONCURRENT_REQUESTS = 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

核心组件

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

中间件

  • 上图有两个 Middlewares,爬虫中间件和下载器中间件,我们重点看 DownloaderMiddleware
  • 重要方法
    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让中间件能轻松获取爬虫文件的所有对象
        #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)
    
  • 借助中间件,我们可以引入很多爬虫需要的操作,但要注意,在合适的方法中引入才能提高效率
    • 使用代理
      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
          #拦截处理所有的请求对象
          #spider参数的作用可以实现爬虫类和中间类的数据交互
          def process_request(self, request, spider):
              # 如果所有的请求都是用代理,则代理操作可以写在该方法中
              request.meta['proxy'] = 'http://ip:port'
              #弊端:会使得整体的请求效率变低
              print(request.url+':请求对象拦截成功!')
              return None
      
          # 如果发生异常,再使用代理
          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
      def process_request(self, request, spider):
      	# 多搞点UA在这
          request.headers['User-Agent'] = '从列表中随机选择的一个UA值'
          print(request.url+':请求对象拦截成功!')
          return None
      
    • 使用 cookie
      def process_request(self, request, spider):
          request.headers['cookie'] = 'xxx'
          #request.cookies = 'xxx'
          print(request.url+':请求对象拦截成功!')
          return None
      
    • 如何动态获取 cookie 呢?让 cookie 在每次启动爬虫时能自动更新;这个我们可以通过 selenium + scrapy 结合解决
    • 中间件还可以帮助获取动态加载的数据

selenium

  • 配合上 selenium,让 scrapy 框架更强悍!
  • 爬取网易新闻,先分析:需要请求哪些地址,流程如何,是否为动态加载数据等
  • 对于新闻列表页的动态加载数据,就要用到中间件和 spider 参数了
    • selenium 的初始化放在爬虫文件
    • 中间件中用 selenium 重新发起请求,获取动态加载后的完整页面数据,封装成新的 response,交给爬虫文件解析
    • middlewares.py
      # Define here the models for your spider middleware
      #
      # See documentation in:
      # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
      
      from scrapy.http import HtmlResponse
      from time import sleep
      
      class WangyiDownloaderMiddleware:
          # 拦截到下载器传递给Spider的响应对象
          # request:响应对象对应的请求对象
          # response:拦截到的响应对象
          # spider:爬虫文件中对应的爬虫类的实例,里面的数据都能拿到!
          # 因为要返回 response,所以不能放在 process_request
          def process_response(self, request, response, spider):
              # 拦截响应并篡改,因为动态加载,目前的response不能解析
              # if判断,只用selenium获取四个新闻列表页(只有这里动态)
              if request.url in ['https://news.163.com/domestic/', 'https://news.163.com/world/', 'https://news.163.com/air/',
                                 'https://war.163.com/']:
                  print("改之~")
                  spider.bro.get(url=request.url)
                  js = 'window.scrollTo(0,document.body.scrollHeight)'
                  spider.bro.execute_script(js)
                  sleep(5)  # 一定要给与浏览器一定的缓冲加载数据的时间
                  # 页面数据就是包含了动态加载出来的新闻列表
                  page_text = spider.bro.page_source	# 这里有没有 cookie 呢?
                  # 篡改响应对象
                  return HtmlResponse(url=spider.bro.current_url, body=page_text, encoding='utf-8', request=request)
              else:
                  print("啥也没改")
                  return response
      
    • 爬虫文件
      import scrapy
      from selenium import webdriver
      from ..items import WangyiItem
      
      class XinwenSpider(scrapy.Spider):
          name = 'xinwen'
          # allowed_domains = ['www.example.com']
          start_urls = ['https://news.163.com/']
      
          def __init__(self):
              option = webdriver.ChromeOptions()
              # 防止打印一些无用的日志
              option.add_experimental_option("excludeSwitches", ['enable-automation', 'enable-logging'])
              #实例化一个浏览器对象(实例化一次)
              self.bro = webdriver.Chrome(chrome_options=option, executable_path='D:\pythonDemo\pycWorks\spider\scrapy\wangyi\chromedriver.exe')
      
          def parse(self, response):
              module_index = [1,2,4,5]
              # //*[@id="index2016_wrap"]/div[3]/div[2]/div[2]/div[2]/div/ul/li[2]/a
              result = response.xpath('//*[@id="index2016_wrap"]/div[3]/div[2]/div[2]/div[2]/div/ul/li')
              # 请求四个板块
              # 会在中间件被拦截并重新封装response
              for index in module_index:
                  module_url = result[index].xpath('./a/@href').extract_first()
                  print("module:", module_url)
                  yield scrapy.Request(url=module_url, callback=self.parse_list)
      
          # 解析四个板块
          # 这里得到的是中间件重新封装过的response
          def parse_list(self, response):
              print(response)
              list = response.xpath('/html/body/div/div[3]/div[3]/div[1]/div[1]/div/ul/li/div/div')
              print("list:", list)
              content_url = None
              # 请求每个新闻的具体内容
              for div in list:
              	# 遇到广告,可能会解析异常
              	# 能否放在中间件 process_exception 处理?效果是一样的!
                  try:
                      title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
                      content_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
                      item = WangyiItem()
                      item['title'] = title
                  except Exception as e:
                      print("遇到广告,跳过!")
      
                  if content_url != None:
                      yield scrapy.Request(url=content_url, callback=self.detail_parse, meta={'item':item})
      
          # 解析每个新闻的具体内容
          def detail_parse(self, response):
              # 多段
              content = response.xpath('//*[@id="content"]/div[2]/text()').extract()
              content = ''.join(content).strip()
              item = response.meta['item']
              item['content'] = content
      
              yield item
      
          #在整个爬虫结束后,关闭浏览器
          def closed(self,spider):
              print('爬虫结束')
              self.bro.quit()
      
    • 记得打开管道和中间件,定义 Item
    • 记得注释掉 allowed_domains

百度API

  • 借助百度的 API,结合 scrapy,帮助我们实现 NLP 的相关需求!

CrawlSpider

  • 使用crawlspider,爬取全站数据
  • 其实之前的多页爬取,请求传参已经实现了全站爬取,只不过 scrapy 提供了封装好的类
  • 使用
    • scrapy startproject quanzhan
    • scrapy genspider -t crawl allsite www.xxx.com 加上 -t 参数即可
  • 项目里面自动调用了链接提取器 LinkExtractor
    • 其实爬取全站的关键就是获取全部页面的连接,通过正则匹配到各页面的链接,剩下的就和之前一样了!
    • follow=True,可以让当前链接作为起始 URL,帮助我们匹配到所有页的链接
      1
    • allow='' 可以递归获取站内所有 URL,但一般不这么干
      rules = (
          # 这个正则只需要关注变化的部分,以 / 为分割
          Rule(LinkExtractor(allow=r'free_\d+\.html'), callback='parse_item', follow=True),
      )
      
  • 上面 LinkExtractor 只是提取页码链接(列表页),如何请求详情页链接呢?很简单,在 rules 加 Rule,再配上对应的 parse 方法
    • 这里有个问题,不同的 Rule 是异步的,那我们在列表页和详情页爬取到的数据怎么对应上呢?这里是不能用 Item 对象请求传参的!
    • 一般情况下,我们只用一个 Rule,在这个 Rule 的 parse 方法里手动发起其他请求
      import scrapy
      from scrapy.linkextractors import LinkExtractor
      from scrapy.spiders import CrawlSpider, Rule
      from ..items import QuanzhanItem
      
      
      class AllsiteSpider(CrawlSpider):
          name = 'allsite'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://sc.chinaz.com/jianli/free.html'] # https://www.kuaidaili.com/free/inha/1
      
          rules = (
              # 这个正则只需要关注变化的部分,以 / 为分割
              Rule(LinkExtractor(allow=r'free_\d+\.html'), callback='parse_item', follow=False),
          )
      
          def parse_item(self, response):
              jianli = response.xpath('//*[@id="container"]/div')
              for li in jianli:
                  title = li.xpath('./a/img/@alt').extract_first()
                  item = QuanzhanItem()
                  item['title'] = title
                  detail_url = li.xpath('./a/@href').extract_first()
                  yield scrapy.Request(url=detail_url, callback=self.detail_parse, meta={'item':item})
      
          def detail_parse(self, response):
              down_url = response.xpath('//*[@id="down"]/div[2]/ul/li[1]/a/@href').extract_first()
              item = response.meta['item']
              item['down_url'] = down_url
      
              yield item
      
    • 或者,不同的 Rule 页面(列表页中的项和详情页)之间有联系,比如:
      1
      2

分布式

  • 分布式在日常开发中并不常用!因为效率太高很容易触发反爬升级,一般也就爬取公司内部数据用一下,更多的是面试问一问,考一考技术深浅
  • 任何分布式技术的核心都是任务调度器,因为要分配任务,否则所有机器都从头开始干,分布毛线
    • 这里使用 scrapy-redis,需要安装
  • 编程重点
    • 创建基于 CrawlSpider 的爬虫文件,并修改
      • from scrapy_redis.spiders import RedisCrawlSpider 并继承
      • start_urls 替换成 redis_key 字符串,该字符串表示任务队列的名称
      • 进行常规的请求操作和数据解析
    • 修改 settings.py
      • 被共享的管道类,存放任务队列的任务
        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'	# 可以是其他机器的ip
        REDIS_PORT = 6379
        
    • 启动 redis
    • 运行项目,等待爬取任务
    • 向任务队列中放入一个起始的 URL
      • lpush queueTask https://wz.sun0769.com/political/index/politicsNewest?id=1&page=1

增量式

  • 意思就是爬取网站更新的那部分数据,以前爬过的不需要
  • 当然,这里不是通过记录上次爬取到的点然后接着爬,因为这样就比较麻烦了
  • 普遍的做法是:全部再爬一遍,然后和之前爬过的比较,去重
    • 方案:使用记录表,记录已经爬取过的数据的指纹
    • 这个表可以使用 redis 的 set,如果 sadd 返回的是 1,说明是新数据,就在管道里持久化到列表
  • 看两个例子
    • 无需深度数据爬取
      #爬虫文件
      import scrapy
      import redis
      from ..items import ZlsItem
      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 = ZlsItem()
                      item['title'] = title
                      item['content'] = content
                      yield item	# 交给管道做持久化
                  else:#sadd没有执行成功(数据指纹已在set集合中)
                      print('暂无最新数据更新......')
      
      class DuanziPipeline:
          def process_item(self, item, spider):
              conn = spider.conn
              conn.lpush('duanzi', item)  # redis=2.10.6
              return item
      
    • 深度爬取,即需要爬取详情页;以详情页 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
      

scrapyd

  • scrapyd 是一个用于部署和运行 scrapy 爬虫项目的程序,允许你通过 JSON API 来部署爬虫项目和控制爬虫运行
  • 在你要部署爬虫的 server 安装并启动
    • pip install scrapyd
    • pip install scrapyd-client(可不装)
    • scrapyd,访问 server 的 6800 端口,可以看到管理界面
  • 部署
    • 创建的 scrapy 项目中有个 scrapy.cfg 配置文件
      # deploy 后面的部署名随便起
      [deploy:firstproject]
      url = http://192.168.109.128:6800/	# 部署到这里,我的一个Linux虚拟机
      project = article
      
    • scrapyd-deploy firstproject -p article article 是 scrapy startproject 时定义的项目名
      1
    • 要修改:python3.7/site-packages/scrapyd/default_scrapyd.confbind 换为 0.0.0.0,让本地局域网均可访问(计网,路由中的知识)
    • 检查部署结果:scrapyd-deploy -L firstproject
  • 管理
    • 安装 curl 命令行工具(Windows)
    • 启动 Job:curl http://192.168.109.128:6800/schedule.json -d project=article -d spider=arti,这些操作直接在虚拟机上就行,因为已经部署上去了;注意:需要将爬虫用到的服务,比如 Redis,提前安装好
      2
    • 停止 Job:curl http://192.168.109.128:6800/cancel.json -d project=article -d job=eacebcac8f0411ed95e5000c2982ee9a
    • 删除 Job:curl http://192.168.109.128:6800/delproject.json -d project=article
  • 也可以使用 requests 控制
    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)
    

生产者消费者模式

  • 了解一下 python 多线程编程即可
    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()
    

小结

  • 至此,回顾了爬虫的基础知识,也对 scrapy 有了更多的了解
  • 下一阶段将进入 js 常见加密算法的学习,为后续的逆向、实战部分打下基础
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Roy_Allen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值