自己动手实现爬虫scrapy框架思路汇总

这里先简要温习下爬虫实际操作:

cd ~/Desktop/spider

scrapy startproject lastspider # 创建爬虫工程
cd lastspider/ # 进入工程

scrapy genspider github github.cn # 创建scrapy爬虫

scrapy genspider -t crawl gitee gitee.com # 创建crawlspider爬虫

# github==========================================
# github.py
# -*- coding: utf-8 -*-
import scrapy


class GithubSpider(scrapy.Spider):
    name = 'github'
    allowed_domains = ['github.cn']
    start_urls = ['http://github.cn/']

    def parse(self, response):
        # 实现逻辑--简单以大分类-中间分类-小分类-商品列表-商品详情-商品价格为例
        # 1. 获取大分类的分组
        # 2. 获取和大分类呼应的中间分类组
        # 3. 遍历提取大分类组和中间分类组的数据,同时获取小分类组
        # 4. 遍历小分类组,提取小分类对应的列表页url,发送列表页请求,callback指向列表页处理函数
        # 5. 如果当前分类页有翻页,则提取下一页url继续发送请求,callback指向自己,以便循环读取
        # 6. 构建列表页数据处理函数,获取列表页商品的分组,遍历提取各商品列表页数据,包括url
        # 7. 根据提取的商品url,构建请求,callback指向详情页数据处理函数
        # 8. 如果当前商品列表页有翻页,则提取下一页url继续发送请求,callback指向自己,以便循环读取
        # 9. 构建商品详情页函数,提取详情页信息,如果其中有数据需要单独发送请求,则再构建请求获取该数据,在最后的请求函数中,需要yield item
        # 10. 上述最终yield 的item 通过在settings中设置spiderpiplelines,就会进入指定的spiderpiplelines中通过process_item()进行进一步数据处理,比如清洗和保存。
        pass

# pipelines.py
class GithubPipeline:
    def open_spider(self,spider): #在爬虫开启的时候执行一次
        if spider.name == 'github':
            # 准备好mongodb数据库及集合,以便接收数据
            client = MongoClient() # host='127.0.0.1',port=27017 左侧是默认值
            self.collection = client["db"]["col"]
            # self.f = open("a.txt","a")
            pass
        
    def process_item(self, item, spider):
        if spider.name == 'github':
            # 数据清洗
            item['content'] = self.process_content(item["content"])
            # 往数据库集合中添加数据
            # 如果item是通过item.py中定义的类实例对象,则不能直接存入mongodb,需要dict(item),如果是字典则可以直接存入。
            self.collection.insert_one(item)
            # pprint(item)
            return item
        
    def process_content(self,content): #处理content字段的数据
        # 对数据进行处理,常用方法-正则匹配,字符串切片,替换等待
        return content

    def close_spdier(self,spider): #爬虫关闭的时候执行一次
        if spider.name == 'github':
            # self.f.close()
            pass
    
    
#================================================
# 字典推导式
# {keys(i):values(i) for i in list1}
# eg:  dict1 = {i.split('=')[0]:i.split('=')[1] for i in str_list}

# gitee ===========================================================
# gitee.py 
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class GiteeSpider(CrawlSpider):
    name = 'gitee'
    allowed_domains = ['gitee.com']
    start_urls = ['http://gitee.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        item = {}
        #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
        #item['name'] = response.xpath('//div[@id="name"]').get()
        #item['description'] = response.xpath('//div[@id="description"]').get()
        return item
# =============================================

爬虫框架

什么是框架,为什么需要开发框架

  • 框架:为了解决一类问题而开发的程序,能够提高开发效率

  • 第三方的框架不能够满足需求,在特定场景下使用,能够满足特定需求

scrapy_plus中有哪些内置对象和核心模块

  • core

    • engine

    • scheduler

    • downloader

    • pipeline

    • spider

  • http

    • request

    • response

  • middlewares

    • downloader_middlewares

    • spider_middlewares

  • item

scrapy_plus实现引擎的基础逻辑

    1. 调用爬虫的start_request方法,获取start_request请求对象

  • 调用爬虫中间件的process_request方法,传入start-request,返回start_request

    1. 调用调度器的add_request,传入start_request

    1. 调用调度器的get_request方法,获取请求

  • 调用下载器中间件的process_request,传入请求,返回请求

    1. 调用下载器的get_response方法,传入请求,返回response

  • 调用下载器中间件的process_response方法,传入response,返回response

  • 调用爬虫中间件的process_response方法,传入response,返回response

    1. 调用spider的parse方法,传入resposne,得到结果

  • 调用爬虫中间件的process_request方法,传入request,返回request

    1. 判断结果的类型,如果是请求对象,调用调度器的add_request,传入请求对象

    1. 否则调用管道的process_item方法,传入结果

如何在项目文件中添加配置文件能够覆盖父类的默认配置

    1. 在框架中conf文件夹下,建立default_settings,设置默认配置

    1. 在框架的conf文件夹下,建立settings文件,导入default_settings中的配置

    1. 在项目的文件夹下,创建settings文件,设置用户配置

    1. 在框架的conf文件夹下的settings中,导入settings中的配置,会覆盖框架中的默认配置

  • url地址补全

  urllib.parse.urljoin(完整的url地址demo,不全的url地址) 返回的是补全后的url地址.

  • 提取对象身上的方法,再单独使用.

class Test:
   def func(self,param):
  print('this is {}'.format(param))
       
test = Test()
ret = getattr(test,'func')
print(ret)
ret('python') # this is python
  • python中内置发送请求的方法

    import requests

    # 发送get请求
    req = requests.get('https://www.python.org')
    # 或者--上面的实现其实本质上是下面代码
    req = requests.request('GET', 'http://httpbin.org/get')

    # 发送post请求
    payload = dict(key1='value1', key2='value2')
    req = requests.post('http://httpbin.org/post', data=payload)

    # 或者--上面的实现其实本质上是下面代码
    req = requests.request('POST', 'http://httpbin.org/post',data=payload)

    # put,options,patch,delete方法同上

框架开发分析:

  • 了解框架,框架思路分析

  • 框架雏形

    • http模块和item模块(传递的数据)

    • core模块(五大核心模块)

    • 框架中间件

    • 框架安装

    • 框架运行

  • 框架完善

    • 日志模块使用

      import logging
      # 日志的五个等级,等级依次递增
      # 默认是WARNING等级
      logging.DEBUG
      logging.INFO
      logging.WARNING
      logging.ERROR
      logging.CRITICAL
      # 设置日志等级
      logging.basicConfig(level=logging.INFO)
      # 使用
      logging.debug('DEBUG')
      logging.info('INFO')
      logging.warning('WARNING')
      logging.error('ERROR')
      logging.critical('CRITICAL')

      # 捕获异常信息到日志
      try:
         raise Exception('异常')
      expect Exception as e:
         logging.exception(e)
         
      # 可以对日志输出格式进行自定义
      %(name)s Logger的名字
      %(asctime)s 字符串形式的当前时间
      %(filename)s 调用日志输出函数的模块的文件名
      %(lineno)d 调用日志输出函数的语句所在的代码行
      %(levelname)s 文本形式的日志级别
      %(message)s 用户输出的消息
      # 默认日志格式
      '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s'  

      利用logger封装日志模块

      # 自行封装了一个Logger类
      # 思考在框架中那些地方需要输出日志信息
      # 1. 引擎启动时--输出开始时间
      # 2. 引擎结束时--输出结束时间,以及总耗时
    • 配置文件实现

      # 思路
      # 1. 先设置个默认配置文件default_settings.py(里面可以包含用户看不到的配置)
      # 2. 再在同级下生成settings,把default_Settings中配置全部导入
      # 3. 考虑到用户使用在外部要修改配置,所以框架也会在外层生成个settings.py文件
      # 4. 如何把用户和默认的同时兼顾呢,就是把用户修改了的覆盖默认的
      # 5. 覆盖的方法就是在default_settings的同级目录下创建settings.py,先导入默认配置,再导入用户配置。参考项目的启动顺序,到外部的settings.py文件,直接 from settings import * 即可。
    • 实现同时多请求

      """
      实现思路:
      1.设置start_url为一个列表
      2.爬虫组件 遍历该列表,来构造请求,返回生成器对象
      3.引擎组件 遍历上述请求生成器,逐一添加到调度器中
      4.同时为了避免阻塞,取的过程设置为不等待取,queue.get(block=False),取不到返回none
      5.可能得到none,所以引擎中要先判断,如果没有,就直接return
      """
    • 实现同时多解析函数

      """
      多解析函数就是:实现scrapy请求中的callback
      实现思路:
      1.在request模块,添加callback,meta两个参数
      2.在response中添加meta参数,接收request中的meta
      3.在引擎中调用callback指定的函数解析响应
        即使用getattr方法,获取爬虫对象上的callback方法,然后来解析响应
        parse = getattr(self.spider,request.callback)
      """
    • 实现同时多个spider文件执行

      """
      多个spider文件,先考虑传列表
      首先引擎中修改代码,spiders参数变成列表,遍历该列表,进行各爬虫请求入调度器队列
      然后,执行 请求-响应-item,但是考虑上上述多解析函数,解析请求的方法是爬虫中的callback,该过程中,并不知道请求对应的爬虫是谁,只知道爬虫列表。 考虑构建请求对应的爬虫,即绑定,遍历爬虫文件构建请求的构成中,给请求赋值个爬虫索引属性-
      request.spider_index = self.spiders.index(spider)
      如此可知请求对应的爬虫是,spider = self.spiders[request.spider_index]
      上述方法用字典页可以实现。看效率,字典可能好些吧。
      """
    • 实现多个管道

      """
      修改引擎,让pipeline由外界传入,在爬虫传item到pipelines中处理时,process_item()传入两个参数,item和spider,以此来决定用哪个爬虫
      """
    • 实现项目中传入多个中间件

      """
      不同的中间件可以实现对请求或者是响应对象进行不同的处理,通过不同的中间件实现不同的功能,让逻辑更加清晰
      修改engine让spider_middleware,download_middleware由外界传入,因为传入的都是列表
      所以,使用中间件时,遍历使用。
      for downloader_mid in self.download_mids:
      request = downloader_mid.process_request(request)

      for spider_mid in self.spider_mids:
      start_resquest = spider_mid.process_request(start_resquest)
      """
    • 动态模块导入

      """
      通过前面的代码编写,我们已经能够完成大部分的任务,但是在main.py 中的代码非常臃肿,对应的我们可以再•settings.py 配置哪些爬虫,管道,中间件需要开启,•能够让整个代码的逻辑更加清晰

      利用importlib.import_modle能够传入模块的路径,即可即可实现根据模块的位置的字符串,导入该模块的功能.


      1.从conf.settings配置中导入 SPIDERS,PIPELINES,SPIDER_MIDDLEWARE,DOWNLOAD_MIDDLEWARE,
      上述分别都是列表
      把 self.spiders=spiders 列表替换为,从配置文件中构建的列表
      eg:‘spider.kcspider.BaiduSpider’
      eg:'pipelines.BaiduPipeline
      首先从中切割出模块和类--从右按.切割,左边为模块名,右边为类名
      调用方法
      self.spiders = _auto_import_instance(SPIDERS,isspider=True)

      def _auto_import_instance(path,isspider)
      if isspider:
      instance={}
      else:
      instanct=[]
        for p in path:
            module_name = p.rsplit('.',1)[0]
            cls_name = p.rsplit('.',1)[1]
            # 动态导入模块
            ret = importlib.import_module(module_name)
            cls = getattr(module,cls_name)
            if isspider:
                instance[cls_name]=cls()
            else:
            instance.append(cls())
          return instance
      """
    • 请求去重

      """
      去重的是爬虫创建的request对象,scrapy_redis中使用的是hash.sha1创建文件指纹的方式。

      根据请求的url、请求方法、请求参数、请求体进行唯一标识,进行比对,由于这四个数据加到一起,内容较长,因此使用求指纹的方式来进行去重判断。

      指纹计算方法,最常用的就是md5、sha1等hash加密算法,来求指纹

      考虑去重的调用
      引擎中在添加爬虫组件生成的请求对象入调度器队列之前,会先去重再添加
        
      """
    • 使用线程/协程池

      def _callback(self, temp):
             '''执行新的请求的回调函数,实现循环'''
             if self.running is True:  # 如果还没满足退出条件,那么继续添加新任务,否则不继续添加,终止回调函数,达到退出循环的目的
                 self.pool.apply_async(self._execute_request_response_item, callback=self._callback)

         def _start_engine(self):
             '''依次调用其他组件对外提供的接口,实现整个框架的运作(驱动)'''
             self.running = True  # 启动引擎,设置状态为True
             # 向调度器添加初始请求
             self.pool.apply_async(self._start_requests)  # 使用异步

             self.pool.apply_async(self._execute_request_response_item, callback=self._callback)  # 利用回调实现循环
             
      # ===========================================
      # 协程
      # scrapy_plus/async/coroutine.py
      '''
      由于gevent的Pool的没有close方法,也没有异常回调参数
      引出需要对gevent的Pool进行一些处理,实现与线程池一样接口,实现线程和协程的无缝转换
      '''
      from gevent.pool import Pool as BasePool
      import gevent.monkey
      gevent.monkey.patch_all()    # 打补丁,替换内置的模块


      class Pool(BasePool):
         '''协程池
        使得具有close方法
        使得apply_async方法具有和线程池一样的接口
        '''
         def apply_async(self, func, args=None, kwds=None, callback=None, error_callback=None):
             return super().apply_async(func, args=args, kwds=kwds, callback=callback)

         def close(self):
             '''什么都不需要执行'''
             pass
  • 框架升级

    • 分布式爬虫,scrapy-redis

      """
      利用redis实现队列

      注意pickle模块的使用:如果将对象存入redis中,需要先将其序列化为二进制数据,取出后反序列化就可以再得到原始对象
      接口定义一致性:利用redis使用一个Queue,使其接口同python的内置队列接口一致,可以实现无缝转换
      redis -- 存储指纹和待抓取的请求对象
      mysql -- 数据存储
      """
    • 增量爬虫

      """
      增量抓取,意即针对某个站点的数据抓取,当网站的新增数据或者该站点的数据发生了变化后,自动地抓取它新增的或者变化后的数据
      设计原理:
      1.定时向目标站点发起请求
      2.关闭对目标站点请求的去重判断
      3.对抓取来的数据,入库前进行数据判断,只存储新增或改变的数据
      """
    • 断点续爬

      """
      断点续爬的效果:爬虫程序中止后,再次启动,对已发送的请求不再发起,而是直接从之前的队列中获取请求继续执行。
      意味着要实现以下两点:
      1.去重标识(历史请求的指纹)持久化存储,使得新的请求可以和之前的请求进行去重对比
      2.请求队列的持久化
      之前的分布式实现了上述两点,但可能会出现问题:
      1.如果其中部分或全部执行体被手动关闭或异常中止,那么这不未被正常执行的请求体,就会丢失,因为请求后,队列中就删除了该请求。
      解决办法:创建请求备份容器,当请求成功后,再把该请求从容器中删除,而不是队列中的,pop删除取出请求。这样的话,当队列中请求全部执行完毕后,备份容器中的请求就是丢失的请求,接下来只需要把它们重新放回请求队列中重新执行就好了。

      2.如果某个请求无论如何都无法执行成功,那么这里可能造成死循环。
      解决办法:考虑给request请求对象设置‘重试次数’属性。
      a1.每次从队列中弹出一个请求时,就把它在备份容器中对应的‘重试次数’+1
      a2. 每次从队列中弹出一个请求后,先判断它的'重试次数'是否超过配置的'最大重试次数',如果超过,就不再处理该对象,把它记录到日志中,同时从备份容器中删除该请求。否则就继续执行。
      """
  • 框架项目实战

posted on 2019-04-09 14:14  一剑风徽  阅读(226)  评论(0编辑  收藏  举报

导航