5.scrapy中间件&分布式爬虫

1. scrapy中间件

两大中间件:
1. 爬虫中间件: 位于爬虫与引擎之间, 只要工作是处理爬虫的输入requests和输出.(使用少)
2. 下载中间件: 位于引擎与下载器之间, 加代理头, 加头, 集成selenium.(使用多)
两个中间件都在scrapy项目的middlewares.py文件中, 使用前需要在settings.py中配置.
1.1 爬虫中间件
使用爬虫中间件需要先配置, 在使用.
# settings.py
SPIDER_MIDDLEWARES = {
    # 中间件类 : 数据(优先级)
   'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
# middlewares.py
class CnblogsSpiderMiddleware:
    """
    并非所有方法都需要定义。如果没有定义方法,
    scrapy 就好像蜘蛛中间件没有修改传递的对象一样
    """

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy 使用此方法来创建您的蜘蛛。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s
	
    
    def process_spider_input(self, response, spider):
     """
     调用通过蜘蛛中间件并进入蜘蛛的每个响应。
     应该返回 None 或引发异常。
     """
        return None

    def process_spider_output(self, response, result, spider):
    """
    在处理完响应后,使用从 Spider 返回的结果调用。
    必须返回一个可迭代的 Request 或 item 对象
    """
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
    """
    当蜘蛛或 process_spider_input() 方法(来自其他蜘蛛中间件)引发异常时调用。
    应该返回 None 或一个可迭代的 Request 或 item 对象
    """
        pass

    def process_start_requests(self, start_requests, spider):
    """
    与蜘蛛的启动请求一起调用,与 process_spider_output() 方法类似,
    只是它没有关联的响应。必须只返回请求(而不是项目)。
    """
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

1.2下载中间件
使用下载中间件需要先配置, 在使用.
# settings.py
DOWNLOADER_MIDDLEWARES = {
   'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}
class CnblogsDownloaderMiddleware:
	"""
	并非所有方法都需要定义。如果没有定义方法,
	scrapy 就好像下载器中间件不修改传递的对象一样。
	"""

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy 使用他的方法来创建你的蜘蛛
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
		"""
		为通过下载器中间件的每个请求调用。downloader
		中间件必须: 
		- 返回 None:继续处理此请求
         - 或返回 Response 对象 
         - 或返回 Request 对象 
         - 或引发 IgnoreRequest:将调用已安装的下载器中间件的 			    process_exception() 方法
		"""
        return None

    def process_response(self, request, response, spider):
		"""
		使用从下载器返回的响应调用。
		必须要么; 
		- 返回一个 Response 对象
         - 返回一个 Request 对象
         - 或引发 IgnoreRequest
         """
        return response
	
    def process_exception(self, request, exception, spider):
		"""
		当下载处理程序或 process_request()(来自其他下载器中间件)          引发异常时调用。
		必须: 
		- 返回无:继续处理此异常 
		- 返回响应对象:停止 process_exception() 链 
		- 返回请求对象:停止 process_exception() 链
		"""
        pass

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)
1.3 创建测试环境
* 1. 创建项目
C:\Users\13600\Desktop\synchro\Project\test1
New Scrapy project 'test1', using template directory 'c:\program\python38\lib\site-packages\scrapy\templates\project', created in:
   C:\Users\13600\Desktop\synchro\Project\test1

You can start your first spider with:
   cd C:\Users\13600\Desktop\synchro\Project\test1
   scrapy genspider example example.com

* 2. 使用pycharm打开项目
* 3. 创建爬虫脚本
PS C:\Users\13600\Desktop\synchro\Project\test1> scrapy genspider cnblog www.cnblogs.com 
Created spider 'cnblog' using template 'basic' in module:
 test1.spiders.cnblog
* 4. 在项目目录下新建启动脚本文件main.py
# main.py
from scrapy.cmdline import execute

execute(['scrapy', 'crawl', 'cnblog'])
* 5. 在配置文件中配置日志级别
# settings.py
LOG_LEVEL = 'ERROR'
* 6 . 在settings.py中配置中间件参数.
# settings.py
DOWNLOADER_MIDDLEWARES = {
   'test1.middlewares.Test1DownloaderMiddleware': 543,
}
* 7. 在下载中间值中间添加测试代码
# middlewares.py
class Test1DownloaderMiddleware:
    ...
        # 请求处理
    def process_request(self, request, spider):
        print(request.url)
        return None

# settings.py中没有关闭爬虫协议, 爬取四次:
"""
http://www.cnblogs.com/robots.txt
https://www.cnblogs.com/robots.txt
http://www.cnblogs.com/
https://www.cnblogs.com/
爬虫先会爬取爬虫协议, 如果http协议的请求获取不到数据会加上s再次发生请求.
"""
* 8. 关闭遵循爬虫协议
# settings.py
ROBOTSTXT_OBEY = False
* 9. 修改爬虫脚本的类中start_urls属性, 改为https协议.
# cnblog.py

start_urls = ['https://www.cnblogs.com/']
1.4 更换随机请求头
* 1. 从request对象中headers属性中获取请求头
 获取的属性值是一个字段套列表
# middlewares.py

class Test1DownloaderMiddleware:
	...
	
    # 请求处理
    def process_request(self, request, spider):
        print(request.headers)
        print(request.headers['User-Agent'])
        return None
     """
     {	
          b'Accept': [b'text/html, application/xhtml+xml,
              application/xml;q=0.9, */*;q=0.8'], 
          b'Accept-Language': [b'en'],
          b'User-Agent': [b'Scrapy/2.6.2 (+https://scrapy.org)']
     }
     
     b'Scrapy/2.6.2 (+https://scrapy.org)'
     
     User-Agent 为 Scrapy/2.6.2 ... 直接暴露了马脚
     """

* 2.  使用fake_useragent模块随机生成User-Agent字符串.
pip install fake_useragent
# middlewares.py

class Test1DownloaderMiddleware:
    ...
    
    # 请求处理
    def process_request(self, request, spider):
        from fake_useragent import UserAgent

        request.headers['User-Agent'] = UserAgent().random
        return None
1.5 添加随机cookie值
# middlewares.py

class Test1DownloaderMiddleware:
    ...
    
    # 请求处理
    def process_request(self, request, spider):
        from random import randint
        
        # cookie池
    	cookie_list = [{'username': 'xx'}, {'username': 'oo'}, ...]
        
	    request.cookie = cookie_list[randint(0, y)]
        
        return None
1.6 添加代理IP
# middlewares.py
class Test1DownloaderMiddleware:


    # 请求处理
    def process_request(self, request, spider):
        print(request.meta) 
        # {'download_timeout': 180.0} 默认只有超时时间
        
        # 代理ip在meta属性中添加一个key为proxy的字典. 
        # (代理有问题会重试发送请求)
        request.meta['proxy'] = 'https://ip:端口'
        return None
1.7 集成selenium
流程(当次爬虫运行, 都使用同一个流浪器对象, 只是在中间件打开不同的地址):
1. 在爬虫脚本中集成selenium, 先生成一个浏览器对象,
2. 在下载中间件中请求方法使用
3. 在爬虫脚本中关闭浏览器对象
* 1. 将chromedriver.exe谷歌浏览器控制插件复制到scrapy框架的项目目录下.
* 2. 在爬虫脚本总生成浏览器对象
import scrapy


class CnblogSpider(scrapy.Spider):
    name = 'cnblog'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['https://www.cnblogs.com/']

    # 集成selenium
    from selenium import webdriver
    bro = webdriver.Chrome(executable_path='chromedriver.exe')

    # 解析数据
    def parse(self, response):
        print(response.text)

    # close方法在爬虫脚本结束时执行
    def close(self, reason):
        # 关闭浏览器对象
        self.bro.close()

* 3. 在下载中间中使用浏览器对象
class Test1DownloaderMiddleware:
	...
    
    # 请求处理
    def process_request(self, request, spider):
        spider.bro.get(request.url)
        spider.bro.implicitly_wait(10)
        # print(spider.bro.page_source)

        # 在这里获取数据需要返回response对象而不是None
        # 内置封装了一个HtmlResponse对象用于返回
        from scrapy.http import HtmlResponse
        # 这个HtmlResponse则被爬虫脚本的response接口, HtmlResponse需要的参数(url, 数据, 请求对象)
        # body的数据需要时解码在后面添加.encode('utf-8')
        response = HtmlResponse(request.url, body=spider.bro.page_source.encode('utf-8'), request=request)
        return response


1.8 注意事项
在中间件中不允许直接修改request的url属性值.
如果修改了, 会报错
AttributeError(属性错误):Request.url 不可修改,
请改用 Request.replace() instead

2. 去重源码

scrapy内置去重功能, 已经取过的url不会再次爬取.
在配置文件settings.py中配置去重使用的类.
# from scrapy.dupefilters import RFPDupeFilter
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
# 去重类 继承BaseDupeFilter
class RFPDupeFilter(BaseDupeFilter):
	...
  
    def request_seen(self, request: Request) -> bool:
        # yield request, 经过一些算法, 得到fp(指纹)
        fp = self.request_fingerprint(request)
        # 如果fp在集合中则不继续爬取
        if fp in self.fingerprints:
            return True
        # 将fp添加到集合中
        self.fingerprints.add(fp)
        # 写入文件
        if self.file:
            self.file.write(fp + '\n')
        return False
request_fingerprint函数的使用
在项目目录下新建一个py文件用于测试.
from scrapy.utils.request import request_fingerprint
from scrapy import Request

# 先生成两个request对象
url_1 = Request('https://www.baidu.con/xx?name=kid&age=18')
url_2 = Request('https://www.baidu.con/xx?age=18&name=kid')

fingerprint_1 = request_fingerprint(url_1)
fingerprint_2 = request_fingerprint(url_2)

print(fingerprint_1)  # d3625990212837cb7ef7a02c4ccd8859daa24b82
print(fingerprint_2)  # d3625990212837cb7ef7a02c4ccd8859daa24b82

"""
参数顺序问题
?name=kid&age=18
?age=18&name=kid
分隔之后得到的数据会按字母排序, 计算出一个指纹(类型MD5加密)
将值拿去集合中做比较.
"""
指纹的值太长, 当爬取的数据以亿为单位的时候占用的资源就很多.

3. 布隆过滤器

3.1 介绍
bloomfilter 是一个通过多哈希函数映射在一张表的数据结构, 能快速判断一个元素是否在一个集合中,
具有姮好的空间和时效. (爬虫中常用于url去重.)
原理: bloomfilter开辟一个m位的bitArray(位数组), 开始虽有数据全部置0, 当一个元素过来时,
能过多个哈希函数(h1, h2, h3..)计算不同的哈希值, 
并通过哈希值找到对应的bitArray下标, 将里面的值0, 置为1.
关于哈希函数, 他们计算出来的值必须在[0, m] 之中.

布隆过滤器它占用空间更少并且效率更高, 但是缺点是其返回的结果是概率性的, 而不是非常准确的.
理论情况下添加到集合中的元素越多, 误报的可能性就越大.
并且, 存放在布隆过滤器的数据不容易删除.
3.2安装模块
* 1. 安装依赖的包
    pip install bitarray   
* 2. 安装布隆过滤器
    pip install pybloom_live
3.3 固定长度
# BloomFilter 固定长度
from pybloom_live import BloomFilter

# 容量
bf = BloomFilter(capacity=1000)

# 测试url
url_1 = 'https://www.baidu.com'
url_2 = 'https://cnblogs.com'

# 将url添加到过滤器中
bf.add(url_1)

print(url_1 in bf)  # True
print(url_2 in bf)  # False
3.4 自动扩量
# ScalableBloomFilter 自动扩量
from pybloom_live import ScalableBloomFilter

"""
initial_capacity 初始容量
error_rate 错误率
mode 模式, ScalableBloomFilter.LARGE_SET_GROWTH 大规模增长
"""
bloom = ScalableBloomFilter(
    initial_capacity=100,
    error_rate=0.001,
    mode=ScalableBloomFilter.LARGE_SET_GROWTH
)

# 测试url
url_1 = 'https://www.baidu.com'
url_2 = 'https://cnblogs.com'

# 将url添加到bloom过滤中
bloom.add(url_1)

print(url_1 in bloom)  # True
print(url_2 in bloom)  # False

4. 自定义去重规则

* 1. 在项目下目录下新建py文件bloom
from scrapy.dupefilters import BaseDupeFilter
from pybloom_live import ScalableBloomFilter


# 自定义去重继承BaseDupeFilter,

# 模仿自定义的写法,
# 在__init__ 中生成一个布隆过滤器
# 重写request_seen方法
class CustomDeduplication(BaseDupeFilter):
    def __init__(self):
        self.bloom = ScalableBloomFilter(
            initial_capacity=100,
            error_rate=0.001,
            mode=ScalableBloomFilter.LARGE_SET_GROWTH
        )

    def request_seen(self, request):
        # 从request中获取出url
        url = request.url
        if url in self.bloom:
            return True
        self.bloom.add(url)

* 2. 配置文件中配置DUPEFILTER_CLASS属性, 使用自定义的去重类.
DUPEFILTER_CLASS = 'test1.bloom_deduplication.CustomDeduplication'

5. 分布式爬虫

5.1 介绍
把一个爬虫任务放在多太机器中取执行, 提高爬取效率.
关键: 共享队列.
原来scrapy的Scheduler维护的是本机的任务队列
(存放Request对象及其回调函数等信息),
+ 本机的去重队列(存放访问过的url地址)

image-20220801212933426

所以实现分布式爬取的关键就是, 找一台专门的主机运行一个共享的队列(使用Redis)然后重写Scrapy的Scheduler到队列取Request, 并且去除重复的request请求.
总结:
1. 共享队列
2. 重写Scheduler, 让其无论去重,还是获取任务都是去访问共享队列
3. 为Scheduler定制去重规则(利用redis的集合类型)

2022-08-01_00865

5.2 分布式爬取案例
* 1. 创建scrapy项目
    命令: scrapy startproject cnblogs_distributed C:\Users\13600\Desktop\synchro\Project\cnblogs_distributed
New Scrapy project 'cnblogs_distributed', using template directory 'c:\program\python38\lib\site-packages\scrapy\templates\project', created in:
   C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed

You can start your first spider with:
   cd C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed
   scrapy genspider example example.com

C:\Users\13600\Desktop>
* 2. 使用PyCharm打开scrapy项目并创建爬虫脚本(爬虫脚本名称与项目名不能重复)
    命令: scrapy genspider cnblogs www.cnblogs.com 
PS C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed\cnblogs_distributed> scrapy genspider cnblogs www.cnblogs.com 
Created spider 'cnblogs' using template 'basic' in module:
 cnblogs_distributed.spiders.cnblogs
PS C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed\cnblogs_distributed> 


* 3. 安装scrapy_redis模块
    命令: pip install scrapy_redis
* 4. 在项目目录下创建运行爬虫脚本主程序main.py
# main.py
from scrapy.cmdline import execute

execute(['scrapy', 'crawl', 'cnblogs'])
* 5. 修改爬虫配置文件
# 不遵循爬虫协议
ROBOTSTXT_OBEY = False

# 展示错误日志
LOG_LEVEL = 'ERROR'

# 全局USER_AGENT
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \             'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134                  Safari/537.36       Edg/103.0.1264.71'

# 分布式爬虫的配置

# redis的连接(不写默认也是使用这个)
# REDIS_HOST = 'localhost'  # 主机名
# REDIS_PORT = 6379  # 端口

# 使用scrapy-redis的去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 使用scrapy-redis的Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 持久化的可以配置,也可以不配置
ITEM_PIPELINES = {
   'scrapy_redis.pipelines.RedisPipeline': 299
}

* 6. 在item.py中创建item对象.
# item.py
class CnblogsItem(scrapy.Item):
    # define the fields for your item here like:
    title = scrapy.Field()
    article_url = scrapy.Field()
    summary = scrapy.Field()
    content = scrapy.Field()
    
* 7. 爬虫脚本程序
import scrapy
from scrapy import Request
# 使用RedisSpider
from scrapy_redis.spiders import RedisSpider

# 继承 RedisSpider
class CnblogsSpider(RedisSpider):
    name = 'cnblogs'
    allowed_domains = ['www.cnblogs.com']
    # 指定Redis中集合的key名,  key=存放不重复request字符串的集合
    redis_key = 'myspider:start_urls'

    def parse(self, response):
        # 获取item对象
        from items import CnblogsItem
        item = CnblogsItem()
        # 获取所有的article标签
        article_list = response.css('article.post-item')

        # 遍历article标签
        for article in article_list:
            # 获取标签
            title = article.css('a.post-item-title::text').extract_first()
            item['title'] = title

            # 获取文章链接
            article_url = article.css('a.post-item-title::attr(href)').extract_first()
            item['article_url'] = article_url

            # 获取文章摘要
            summary = article.css('p.post-item-summary::text')[-1].extract().strip()
            item['summary'] = summary

            yield Request(article_url, callback=self.parse_detail, meta={'item': item})

    def parse_detail(self, response, **kwargs):
        # 从response中获取出item对象
        item = response.meta.get('item')

        # 获取到html标签的文档, 不然下载再来就是没有排版的文字.
        content = response.css('#cnblogs_post_body').extract_first()
        item['content'] = content

        # 将数据返回
        yield item

redis_key = 'myspider:start_urls' 多个机器使用一个起始地址,
往redis中写入起始地址后放入, 三台机器谁先抢到地址, 谁就先执行任务,爬取这个地址
之后返回一堆地址放入起始地址中, 三台机器再抢, 抢到一个执行一个...
* 8. 在scrapy的__init__.py下将项目路径添加到环境变量中
# __init__.py
import os
import sys
# 将项目路径添加到环境变量中
BASE_PATH = os.path.dirname(__file__)
sys.path.append(BASE_PATH)
* 9. 启动程序
     默认使用本地的redis, 无须配置
     模拟三台机器运行分布式爬虫, 开三个终端, 启动三个爬虫程序
     一个进程算一台机器.
     
     命令: scrapy crawl cnblogs

2022-08-02_00867

* 10. 往redis中写入起始地址
127.0.0.1:6379> lpush myspider:start_urls https://www.cnblogs.com/

启动之后开始爬取数据(信息没有展示到print函数展示到终端, 直接查看数据即可.)

image-20220802163448004

5.3 总结
* 1. pip3 install scrapy-redis
* 2. 原来继承Spider,现在继承RedisSpider
* 3. 不能写start_urls = ['https:/www.cnblogs.com/']
    需要写redis_key = 'myspider:start_urls'
* 4. setting中配置↓
# redis的连接
# 主机名
REDIS_HOST = 'localhost' 
# 端口
REDIS_PORT = 6379           

# 使用scrapy-redis的去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 使用scrapy-redis的Scheduler
# 分布式爬虫的配置

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化的可以配置,也可以不配置
ITEM_PIPELINES = {
   'scrapy_redis.pipelines.RedisPipeline': 299
}
* 5. 使用cmd命令启动scrapy项目, 将项目地址添加到环境变量中, 否则, scrapy中的模块也能提示找不到.
# scrapy项目的__init__.py
import os
import sys
# 将项目路径添加到环境变量中
BASE_PATH = os.path.dirname(__file__)
sys.path.append(BASE_PATH)
* 6. redis中为myspider:start_urls插入一个起始地址
lpush myspider:start_urls https://www.cnblogs.com/

————————————————
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
————————————————

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值