scrapy的使用

1 多任务异步协程基础

  • 编码流程

    • 创建协程对象

    • 创建任务对象

    • 创建事件循环对象

    • 将任务注册到事件循环对象中

  • 特殊的函数:使用async修饰函数,则该函数就是一个特殊的函数,

    •  async def get_request(url):
          print('正在请求:',url)
          sleep(1)
          print('请求结束:',url)
  • 协程对象:特殊函数被调用后函数内部的实现语句不会被立即执行,该函数调用后会返回一个协程对象

    •  c = get_request('www.1.com')
  • 任务对象:任务对象就是对协程对象的进一步的封装

    • 任务对象 == 高级的协程对象 == 特殊的函数调用

    • 创建任务对象

      •  task = asyncio.ensure_future('协程对象')
    • 绑定回调

      • 任务对象结束之后执行回调函数

      •  #绑定回调
         task.add_done_callback(parse)
         def parse(task): # tesk参数能获取特殊函数的返回值
            print('i am task callback()!!!=----',task.result()) # task。result()就是特殊函数的返回值
  • 事件循环对象

    •  #创建一个事件循环对象
       loop = asyncio.get_event_loop()
    • 作用:将注册在事件循环对象的任务对象进行异步执行

    •  #将任务对象注册到事件循环对象中并且开启事件循环
       loop.run_until_complete(task),# 不修饰task会报错
       loop.run_until_complete(asyncio.wait(tasks))
  • 注意事项:在特殊函数内部的实现语句中不可以出现不支持异步的模块对应的代码,否则就是终止多任务异步协程的异步效果,例如time模块,requests模块

    •  # 替代time模块
       await asyncio.sleep(3) # 必须加await让程序等待阻塞完成
    • aihttp模块

      • 编码流程

        • 基本架构

          •  with aiohttp.ClientSession() as s:
              with await s.get(url) as response:
                    page_text = response.text()
                    return page_text
        • 补充细节

          • 添加async关键字

            • 每一个with前加async

          • 添加await关键字

            • 加在每一步的阻塞操作前加await

              • 请求

              • 获取响应数据(规定)

          •  async with aiohttp.ClientSession() as s:
                #get/post:proxy = 'http://ip:port' 不一样,不是字典了
                #url,headers,data/prames跟requests一样
                async with await s.get(url) as response:
                    page_text = await response.text()#text()字符串形式的响应数据。read()二进制的响应数据
                    return page_text
      • 支持异步的网络请求模块替代requests模块发起网络请求,请求完毕要close,使用with自动化关闭

      •  async def get_request(url):
            async with aiohttp.ClientSession() as s:
                #get/post:proxy = 'http://ip:port' 不一样,不是字典了
                #url,headers,data/prames跟requests一样
                async with await s.get(url) as response:
                    page_text = await response.text()#text()字符串形式的响应数据。read()二进制的响应数据
                    return page_text

2 . selenium

  • 概念: 基于浏览器自动化的一个模块,

  • Appnium是基于手机的自动化模块

  • 作用

    • 便捷的爬取到动态加载的数据,可见即可得

    • 便捷的实现模拟登陆,

  • 基本使用

    • 环境安装

      • pip install selenium

  • 下载浏览器的驱动程序

  • 浏览器版本和驱动程序的映射关系

  • https://blog.csdn.net/huilan_same/article/details/51896672

  • 动作链

    •  from selenium.webdriver import ActionChains #动作连的类
    •  #基于动作连实现滑动操作
       action = ActionChains(bro)
       #点击且长按
       action.click_and_hold(div_tag)
       
       for i in range(5):
          #perform()表示让动作连立即执行
          action.move_by_offset(20,20).perform()
          sleep(0.5)
    • 在使用find系列的函数进行标签定位的时候如果出现了NoSuchElementException如何处理? - 如果定位的标签是存在于iframe标签之下的,则在进行指定标签定位的时候 必须使用switch_to.frame()的操作才可。

       - ```
          bro.switch_to.frame('iframeResult') #frame的参数为iframe标签的id属性值
          ```
  • 无头浏览器

    •  from selenium.webdriver.chrome.options import Options
       # 创建一个参数对象,用来控制chrome以无界面模式打开
       chrome_options = Options()
       chrome_options.add_argument('--headless')
       chrome_options.add_argument('--disable-gpu')
  • 如何规避selenium被监测到的风险

    • 网站可以根据:window.navigator.webdriver的返回值鉴定是否使用了selenium

    • undefind:正常

    • true:selenium

    •  #通过加参数让window。navigateor。webdriver返回值为undefined,规避监测
       rom selenium.webdriver import ChromeOptions
       
       option = ChromeOptions()
       option.add_experimental_option('excludeSwitches', ['enable-automation'])
       
       
       # 后面是你的浏览器驱动位置,记得前面加r'','r'是防止字符转义的
       bro = webdriver.Chrome(r'chromedriver.exe',options=option)
  • 示例

    •  from selenium import webdriver
       import time
       #实例化某一款浏览器对象
       bro = webdriver.Chrome(executable_path='chromedriver.exe')
       
       #基于浏览器发起请求
       bro.get('https://www.jd.com/')
       
       #商品搜索
       #标签定位
       search_input = bro.find_element_by_id('key')
       #往定位到的标签中录入数据
       search_input.send_keys('袜子')
       #点击搜索按钮
       btn = bro.find_element_by_xpath('//*[@id="search"]/div/div[2]/button')
       time.sleep(2)
       btn.click()
       time.sleep(2)
       #滚轮滑动(js注入)
       bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')

     

3 . scrapy 框架

  1. 下载和开启项目

  • pip install wheel : 可以通过.whl文件安装Python相关的模块,和.zip一样

  • 下载twisted ,百度下载

  • pip install Twisted-17.1.0-cp35-cp35m-win_admin64.whl

  • pip install scrapy

  • pip install pywin32

  • scrapy startproject ProName : 开启项目

  • cd ProName : 切换到项目中

  • scrapy genspider spiderName www.xxx.com : 新建一个爬虫文件

  • scrapu crawl spiderName

  1. 数据解析

  • 【1】extract()和extract_first()

  1. 持久化存储

  • 基于终端指令进行持久化存储

    • 将parse方法返回值存储到本地的磁盘文件中

    • scrapy crawl spiderName -o filepath

      •  def parse_item(self, response):
            all_data=[]
            li_list = response.xpath('//*[@id="LBMBL"]/ul/li')
            for li in li_list:
                title_text = li.xpath('.//div[@class="title"]/a/text()').extract_first()
                title_url = li.xpath('.//div[@class="title"]/a/@href').extract_first()
                dic = {
                    'title_text':title_text,
                    'title_url':title_url,
                }
                all_data.append(dic)
                return all_data
  • 基于管道进行持久化存储

    • 编码流程

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

      2. 将items.py中定义相关的属性

      3. 将解析到的数据存储到一个item类型的对象中

      4. 将item类型的对象提交给管道

      5. 管道类的process_item中接收item,之后对item进行任意形式的存储

      6. settings中开启管道类

        •  ITEM_PIPELINES = {
              'MoTe.pipelines.MotePipeline': 300, # 优先级,数值越小
           }

           

    • 一个管道类定义一种持久化存储方式

      • 在process_item中返回item,可以实现多个管道类都生效,是将item传递给下一个即将执行的管道类

      • 重写父类方法,让文件和数据库的打开关闭连接等操作都只执行一次,提高效率

      • 注意事项 : 对于存储的数据有单双引号都是用时,用pymysql或者MysqlDB的escape_string()方法对引号进行转义,不然就会报1064的语法错误

      •  ITEM_PIPELINES = {
            'MoTe.pipelines.MotePipeline': 300,
            'MoTe.pipelines.MysqlPipeline': 301,
         }
      • 本地文件存储

        •  class MotePipeline(object):
              fp = None
              # 重写父类方法,该方法只会执行一次,
              def open_spider(self,spider):
              # 该方法调用后就可以接受爬虫类提交的对象
                  print('start spider')
                  self.fp = open('./a.txt','w',encoding='utf-8')
           
              def process_item(self, item, spider):
                  title = item['title_text']
                  url = item['title_url']
                  # print(title,url)
                  self.fp.write(title+url)
                  return item #将item传递给下一个即将执行的管道类
           
              def close_spider(self,spider):
                  print('finish spider')
                  self.fp.close()
      • 数据库存储

        •  # Mysql
           class MysqlPipeline(object):
              conn = None
              cursor = None
              def open_spider(self,spider):
                  self.conn = pymysql.Connection(host = '127.0.0.1',port = 3306,user = 'root',password = '123',db='mote',charset = 'utf8')
                  print(self.conn)
           
              def process_item(self,item,spider):
                  title = item['title_text']
                  url = item['title_url']
           
                  sql = 'insert into cmodel values("%s","%s")'%(pymysql.escape_string(title),pymysql.escape_string(url))#转义引号
                  self.cursor = self.conn.cursor()
                  try:
                      self.cursor.execute(sql)
                      self.conn.commit()
                  except Exception as e:
                      print(e)
                      self.conn.rollback()
                  return item
           
              def close_spider(self,spider):
                  self.cursor.close()
                  self.conn.close()
           # Redis
           class RedisPipeline(object):
              conn = None
           
              def open_spider(self,spider):
                  self.conn = Redis(host='127.0.0.1',port=6379)
                  print(self.conn)
           
              def process_item(self,item,spider):
                  self.conn.lpush('news',item)
        • redis版本的3以上的不支持字典作为值

          • 解决方法

            •  pip install -U redis==2.10.6
  1. 图片流数据的爬取

    • scrapy中封装了一个管道类(ImagesPipeline),基于改管道类可以实现对图片资源的请求和持久化存储

    • 编码流程

      • 爬虫文件中解析出图片地址

      • 将图片地址封装到item并提交给管道类

      • 管道文件中自定义一个管道类(父类为ImagesPipeline)

      • 重写三个方法

        •  from scrapy.pipelines.images import ImagesPipeline
           class ImgproPipeline(ImagesPipeline):
           
              # 该方法用作于请求发送
              def get_media_requests(self, item, info):
                  # 对item中的图片地址进行请求发送,
                  print(item)
                  yield scrapy.Request(url=item['img_src'])
                   
              # 指定文件存储路径(文件夹+文件夹名称)
              def file_path(self, request, response=None, info=None):
                  return request.url.split('/')[-1]
               
              # 将item传递给下一个即将被执行的管道类
              def item_completed(self, results, item, info):
                  return item
      • 在配置文件中开启管道且加上文件路径指定IMAGES_STORE = './image'

    • referer防盗链

      •  DEFAULT_REQUEST_HEADERS = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'en',
            'Referer':'https://image.baidu.com/' #指定来源页面为当前页面
         }
    • 手动发送请求

      •  yield scrapy.Request(nwe_url,callback=self.parse)
         # 写在parse解析函数中,实现自调用,加条件可结束递归
    • scrapy中进行post请求发送

      •  yield scrapy.FormRequest(url,callback,formdata)#formdata是请求参数
    • 对起始的url进行post发送

      • 重写父类的方法

      •  def start_requests(self):
            for url in self.start_urls:
                yield scrapy.FormRequest(url,callback=self.parse(),formdata={})

         

  2. scrapy中如何提升爬取数据的效率?

     增加并发:
            默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。
             
     降低日志级别:
        在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘ERROR’
     
     禁止cookie:
        如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
     
     禁止重试:
        对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
     
     减少下载超时:
        如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 1 超时时间为10s
  3. 请求传参(深度爬取)

    • 爬取的数据没有存在同一页面中

    • 如何实现请求传参

      • Request(url,callback,meta={}):meta字典可以将参数传递给callback函数,多用于传递item

      • callback接受item:用response.meta获取字典

      • 请求传参之前的数据没有重复,将meta参数传送到callback时可能数据会重复,原因是meta的参数传递是浅拷贝,并且scrapy是异步框架才会导致这样的问题

        • 解决方法

          • 使用copy,在yield Request的时候将item深拷贝一下,使每次传入的item都是独立的对象

  4. scrapy五大核心组件

    • 引擎 : 引擎对接收到的数据流进行类型判断,从而知道下一步的操作。触发事务(框架核心)

    • Spider :将提取到的url封装成请求对象,将请求对象给引擎,引擎给调度器,接受到response后,将解析到的数据存到item中吗,通过引擎给管道,进行存储

    • 调度器 : 接受到请求对象,进行过滤后压入队列中,并在引擎再次请求时返回,从队列中取出一个请求对象并返回给引擎,引擎又传给下载器,下载器从互联网中下载数据,封装到response中,response给引擎,引擎将response给spider,进行数据解析

      • 过滤器 : 将请求对象进行重复过滤,删除重复的请求对象

      • 队列 : 将请求对象存进来,取出去

    • 管道 : 从引擎接受item并对item中的数据进行持久化存储

    • 下载器 :接受调度器通过引擎返回的请求对象,下载数据并返回response(scrapy下载器是建立在twisted这个高效的异步模型上)

  5. 中间件

    • 下载中间件

      • 批量拦截所有的请求和响应

        • 为什么拦截请求

          • 请求头信息(UA)篡改

          • 代理操作

        • 为什么拦截响应

          • 篡改响应数据

          • 篡改响应对象,将不满足条件的对象剔除或者改为符合条件的响应对象

      •  

    • 爬虫中间件

  6. 网易新闻数据爬取

    •  爬虫文件
       import scrapy
       import sys
       from selenium import webdriver
       from newsPro.items import NewsproItem
       
       class NewsSpider(scrapy.Spider):
          sys.path.append('E:\ES\IP\chromedriver.exe') # 添加搜索路径
          name = 'news'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['https://news.163.com/']
          bro = webdriver.Chrome(executable_path=r'E:\ES\IP\chromedriver.exe') # 使用selenium获取动态数据,在中间件中对response进行替换修正
          son_urls = []
          # 解析出每个版块对应的url
          def parse(self, response):
              li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
              # indexs = [3,4,6,7]
              indexs = [3]
              for index in indexs:
                  son_url = li_list[index].xpath('./a/@href').extract_first()
                  self.son_urls.append(son_url)
                  # 对每个url进行页面请求发送
                  yield scrapy.Request(son_url,callback=self.parse_son_page)
       
          # 解析每个子url的数据
          def parse_son_page(self,response):
              # 目前的response不能获取到动态加载的新闻数据
              item = NewsproItem()
              div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
              for div in div_list:
                  detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
                  title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
                  if title:
                      item['title'] = title
                  if detail_url:
                      yield scrapy.Request(detail_url,callback=self.parse_detail,meta={'item':item}) # 请求传参
           
          # 解析子url中每条新闻的内容详情
          def parse_detail(self,response):
              item = response.meta['item']
              content = response.xpath('//*[@id="endText"]//text()').extract() # 需要匹配到多个结果,不用extract_first()了
              print(content)
              content = ''.join(content)
              item['content'] = content
              print(item['title'],item['content'])
              yield item
    •  中间件
       # -*- coding: utf-8 -*-
       
       # Define here the models for your spider middleware
       #
       # See documentation in:
       # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
       import time
       from scrapy import signals
       from scrapy.http import HtmlResponse
       from time import sleep
       class NewsproDownloaderMiddleware(object):
       
          def process_request(self, request, spider):
       
              return None
          # 拦截响应对象,将不满足的响应对象修正
          def process_response(self, request, response, spider):
              bro = spider.bro
              son_urls = spider.son_urls
              if request.url in son_urls:
                  bro.get(request.url)
                  sleep(2)
                  page_text = bro.page_source
                  new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)
                  return new_response
              else:
                  return response
       
          def process_exception(self, request, exception, spider):
       
              pass
  7. CrawlSpider实现全栈爬取和深度爬取

    • 新建一个工程

    • cd 工程

    • 创建爬虫文件

    • 链接提取器LinkExtractor

      • 可以根据指定的规则提取出符合条件的链接

        •  link = LinkExtractor(allow='正则表达式') # 不写规则,就提取start_urls中所有的链接
    • 规则解析器Rule

      • 对提取到的链接进行请求发送

        •  rules = (
              Rule(link, callback='parse_item', follow=True),
           )
           # link是通过链接提取器提取出的链接
           # callback 是对链接进行数据解析
           # fallow = True代表链接提取器作用到链接提取器提取到的url上,这样就可以对所有的页码进行爬取,我不是仅仅前几页和最后一页,会有重复的url,但是调度器会进行过滤操作
    • 深度爬取

  8. 分布式爬虫

    • 实现方式: sscrapy+scrapy-redis组件实现分布式,原生的scrapy不支持scrapy,需要借助于组件

    • 什么是分布式

      • 需要搭建一个由n台电脑组成的集群,在每一台电脑中执行同一组程序,让其对同一个网络资源进行联合且分布的数据爬取

    • 为什么scrapy不可以实现分布式

      • 分布式集群对应的调度器不能共享

      • 分布式集群不能实现管道的共享

    • scrapy-redis组件的作用。爬取的数据只能存储在redis

      • 提供可以被共享的管道和调度器

    • 分布式的实现流程

      • 环境的安装:pip install scrapy-redis

      • 创建工程

      • cd工程

      • 创建爬虫文件

        • 基于spider

        • 基于CrawlSpider

      • 修改爬虫文件

        • 导包

          •  from scrapy_redis.spiders import RedisCrawlSpider # 基于crawlSpider的爬虫文件
             from scrapy_redis.spiders import RedisSpider # 基于spider的爬虫文件
        • 删除start_urls和allowed_domains

        • 添加属性redis_key = ‘可以被共享的调度器队列名称’

        • 写常规的爬虫代码

        • 修改爬虫文件

          • UA

          • 日志等级

          • rebots认证

          • settings配置

             
             # 指定管道
             ITEM_PIPELINES = {                  
                'scrapy_redis.pipelines.RedisPipeline': 400
               
             # 指定调度器
             # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
             DUPEFILTER_CLASS="scrapy_redis.dupefilter.RFPDupeFilter"
             
             # 使用scrapy-redis组件自己的调度器
             SCHEDULER = "scrapy_redis.scheduler.Scheduler"
             
             # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
             SCHEDULER_PERSIST = True
             
             # 指定redis数据库
             REDIS_HOST = '192.168.13.254'
             REDIS_PORT = 6379
          • 修改redis配置文件

            • 关闭默认绑定

              • 56行:# bind 127.0.0.1

            • 关闭保护模式(只能读不能写)

              • 75行:protected-mode no

          • 启动redis的服务端和客户端,携带配置文件启动

            • reids-server redis。windows.conf

            • redis-cli.exe

          • 启动分布式的程序

            • cd 爬虫文件所在目录 -spider目录

            • scrapy runspider xxx.py

          • 向调度器的队列中放入一个其实url

            • 队列存在于redis

            • redis 客户端中: lpush ‘可以被共享的调度器队列名称’ www.xxx.com

          • redis中就可以查看爬取到的数据了,组件实现了自动化的持久化存储

  9. 增量式爬虫

    • 概念 : 监测

    • 核心技术: 去重

    • 适合使用增量式的网站

      • 基于深度爬取

        • 对爬取过的url进行一个记录(存在一个记录表中,记录url即可)

        • 不能用Rule对解析到的url进行请求发送,因为RUle是对符合条件的所有URL发起请求,Rule获取的url不能看到,不能做筛选,所以使用手动请求发送,但是Rule还是可以对起始url进行下一深度的url进行匹配

          •  ex = self.conn.sadd('记录表的key',解析到的url)
             if ex = 1:# 解析到的url不存在与redis的set集合中
              print('数据有更新')
              yield scrapy.Request(url,callback = '数据解析函数')
             else:
              print('啥都没')
      • 基于非深度爬取

        • 记录表记录爬取过的数据的唯一指纹

          • 数据指纹: 就是原始数据的一组唯一标识

            • 可以通过指定的格式通过hash加密成唯一的数据指纹

              •  con_id = hashlib.sha256(con.encode()).hexdigest() # 数据指纹
      •  

    • 记录表的存在形式

      • redis的set可以充当记录表,具有去重的特性,

      • 不用Python中的set,因为Python中的set虽然具有去重的特性,但是不符合增量式爬虫的需求,增量式爬虫还需要将记录表持久化存储

  10. filder抓包工具的使用

posted @ 2020-03-26 22:02  恰蜜小嘴  阅读(408)  评论(0编辑  收藏  举报