分布式爬虫

概念

所谓分布式, 多个程序同时对一个任务进行操作

一分多的高效率的任务进行方式

简单说明

一个 10GB 的爬虫任务, 交给10台服务器进行同时爬取

对比单服务器无论怎么优化都是 10倍的效率, 但是成本高

需要硬件环境支持 ( 带宽, 服务器设备等 )

多态主机共享一个爬取队列即为分布式爬虫

物理拓扑

/ -------------服务器 2

| / ------------------服务器3

服务器1    ----------- 服务器 4

| \-------------------服务器5

\-------------服务器6

服务器 1 负责 url队列的分发 ( 本质上是个 redis 数据库 )

其他服务器可能分布在全国各地, 分别和服务器1 建立联系,

通过 服务器 1 的共享队列进行爬取任务的获取

为什么使用 redis

redis 速度快

redis 非关系型数据库, redis 中的集合,  存储每个 request 的指纹

Scrapy 实现分布式

Scrapy 原生是不支持分布式爬虫

Scrapy 的爬取队列是放在调度器中的

因此只要能够实现调度器的共享即可完成爬虫任务的共享

即重写 scrapy 的调度器, ( 别人造的轮子又大又圆 scrapy_redis )

scrapy-redis

简介

scrapy-redis是scrapy框架基于redis数据库的组件,用于scrapy项目的分布式开发和部署。

特征

分布式爬取

可以启动多个spider工程,相互之间共享单个redis的requests队列。最适合广泛的多个域名网站的内容爬取。

分布式数据处理

爬取到的scrapy的item数据可以推入到redis队列中

这意味着你可以根据需求启动尽可能多的处理程序来共享item的队列,进行item数据持久化处理

Scrapy即插即用组件

Scheduler调度器 + Duplication复制 过滤器,Item Pipeline,基本spider

三大作用

去重规则的校验

实现调度器对请求的调配

定制起始URL

scrapy-redis架构

安装

pip install scrapy-redis

使用

所有可选的配置, 未注释表示必选设置

# 替换调度器, 启用 redis 中存储的请求调度队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 替换过滤器, 所有的通向 redis 的爬虫都需要使用此过滤器 ( 去重机制 )
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 默认的 序列化方式为 pickle, 可以更改为 json 或者 msgpack
# SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat" # 是否云讯断点续爬 ( 爬取完成后是否清楚指纹 )
# SCHEDULER_PERSIST = True # 默认使用优先级队列(默认),其他:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue' # 广度优先 基于队列 先进先出
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue' # 深度优先 基于栈 后进先出 # SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' 的时候才有用
# DEPTH_PRIORITY = 1 # 广度优先
# DEPTH_PRIORITY = -1 # 深度优先 后进先出 # 初次启动的时候的阻塞时间 ( 仅适用于队列类为SpiderQueue或SpiderStack时 )
# SCHEDULER_IDLE_BEFORE_CLOSE = 10 # 替换持久化工具为 scrapy-redis 的
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
} # The item pipeline serializes and stores the items in this redis key.
# REDIS_ITEMS_KEY = '%(spider)s:items' # The items serializer is by default ScrapyJSONEncoder. You can use any
# importable path to a callable object.
# REDIS_ITEMS_SERIALIZER = 'json.dumps' # 指定连接 redis 的主机和端口号
# REDIS_HOST = 'localhost'
# REDIS_PORT = 6379 # Specify the full Redis URL for connecting (optional).
# If set, this takes precedence over the REDIS_HOST and REDIS_PORT settings.
# REDIS_URL = 'redis://user:pass@hostname:9001' # Custom redis client parameters (i.e.: socket timeout, etc.)
# REDIS_PARAMS = {}
# Use custom redis client class.
# REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' # If True, it uses redis' ``SPOP`` operation. You have to use the ``SADD``
# command to add URLs to the redis queue. This could be useful if you
# want to avoid duplicates in your start urls list and the order of
# processing does not matter.
# REDIS_START_URLS_AS_SET = False # Default start urls key for RedisSpider and RedisCrawlSpider.
# REDIS_START_URLS_KEY = '%(name)s:start_urls' # Use other encoding than utf-8 for redis.
# REDIS_ENCODING = 'latin1'

去重

scrapy-redis  去重原理

redis的集合有去重属性,在保存重复的值的时候返回值会是 0

完全自定义实现去重

from scrapy.dupefilter import BaseDupeFilter
import redis
from scrapy.utils.request import request_fingerprint class DupFilter(BaseDupeFilter):
def __init__(self):
self.conn = redis.Redis(host='140.143.227.206',port=8888,password='beta') def request_seen(self, request):
"""
检测当前请求是否已经被访问过
:param request:
:return: True表示已经访问过;False表示未访问过
"""
fid = request_fingerprint(request)
result = self.conn.sadd('visited_urls', fid)
if result == 1:
return False
return True

使用 scrapy-redis 去重

# 直接加了配置就生效。但是是基于时间戳的,时间戳变了就会失效很不实用 
################ scrapy redis连接 #################### REDIS_HOST = '127.0.0.1'     # 主机名
REDIS_PORT = 6379 # 端口
REDIS_PARAMS = {'password':'yangtuo'} # Redis连接参数,比如密码
# 默认:REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,})
REDIS_ENCODING = "utf-8" # redis编码类型 默认:'utf-8' # REDIS_URL = 'redis://user:pass@hostname:9001' # 连接URL(优先于以上配置)

################ scrapy redis去重 #################### DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'
# 默认的是按照时间戳的。结果每次请求的时候是会进行去重
# 但是下一次请求时间戳不一致就会清空导致无法去重 因此这里存在优化空间 最好定死 # DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' # 如果使用 scrapy-redis 的默认去重是会使用这个类来处理去重
DUPEFILTER_CLASS = 'dbd.xxx.RedisDupeFilter' # 这样使用自己自定义的去重规则

优化

from scrapy_redis.dupefilter import RFPDupeFilter
from scrapy_redis.connection import get_redis_from_settings
from scrapy_redis import defaults class RedisDupeFilter(RFPDupeFilter):
@classmethod
def from_settings(cls, settings):
server = get_redis_from_settings(settings)
# key = defaults.DUPEFILTER_KEY % {'timestamp': int(time.time())} # 源码这里是用的时间戳 不断变换导致每次重置
key = defaults.DUPEFILTER_KEY % {'timestamp': 'yangtuo'} # 不要时间戳了,我定死一个固定值
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(server, key=key, debug=debug)

调度器

settings.py 中进行配置

SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 默认使用优先级队列(默认),其他:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue' # 广度优先 基于队列 先进先出
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue' # 深度优先 基于栈 后进先出 # SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' 的时候才有用
DEPTH_PRIORITY = 1 # 广度优先
# DEPTH_PRIORITY = -1 # 深度优先 后进先出 SCHEDULER_QUEUE_KEY = '%(spider)s:requests' # 调度器中请求存放在redis中的key SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat" # 对保存到redis中的数据进行序列化,默认使用pickle SCHEDULER_PERSIST = False # 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_FLUSH_ON_START = True # 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空
# SCHEDULER_IDLE_BEFORE_CLOSE = 10 # 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)。 SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter' # 去重规则,在redis中保存时对应的key # 优先使用 DUPEFILTER_CLASS,如果没有就是用 SCHEDULER_DUPEFILTER_CLASS
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' # 去重规则对应处理的类

起始url

爬虫文件

import scrapy
from scrapy.http import Request
import scrapy_redis
from scrapy_redis.spiders import RedisSpider class ChoutiSpider(RedisSpider): # 继承改为 用 RedisSpider
name = 'chouti'
allowed_domains = ['chouti.com'] # def start_requests(self): # 不再需要写 start_requests 了
# yield Request(url='https://dig.chouti.com',callback=self.parse) def parse(self, response):
print(response)

另写一个脚本文件

import redis

conn = redis.Redis(host='127.0.0.1',port=6379,password='yangtuo')

# 通过往 redis 里面加数据从而控制爬虫的执行
# 如果没有数据就会夯住。一旦有数据就爬取,可以
conn.lpush('chouti:start_urls','https://dig.chouti.com/r/pic/hot/1')

代码流程

1.启动

scrapy crawl chouti --nolog

2.实例化调度器对象

找到  SCHEDULER = "scrapy_redis.scheduler.Scheduler"  配置并实例化调度器对象

  - 执行Scheduler.from_crawler

  - 执行Scheduler.from_settings
- 读取配置文件:

SCHEDULER_PERSIST    # 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_FLUSH_ON_START # 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空
SCHEDULER_IDLE_BEFORE_CLOSE # 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)

- 读取配置文件:

SCHEDULER_QUEUE_KEY    # %(spider)s:requests    # 读取保存在 redis 中存放爬虫名字的 name 的值
SCHEDULER_QUEUE_CLASS # scrapy_redis.queue.FifoQueue # 读取要使用的队列方式
SCHEDULER_DUPEFILTER_KEY # '%(spider)s:dupefilter' # 读取保存在 redis 中存放爬虫去重规则名字的 key 的值
DUPEFILTER_CLASS # 'scrapy_redis.dupefilter.RFPDupeFilter' # 读取去重的配置的类
SCHEDULER_SERIALIZER # "scrapy_redis.picklecompat" # 读取保存在 redis 的时候用的序列化方式

- 读取配置文件:

REDIS_HOST = '140.143.227.206' # 主机名
REDIS_PORT = 8888 # 端口
REDIS_PARAMS = {'password':'beta'} # Redis连接参数 默认:REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,})
REDIS_ENCODING = "utf-8"

- 实例Scheduler对象

3. 爬虫开始执行起始URL

- 调用 scheduler.enqueue_requests()

def enqueue_request(self, request):
# 请求是否需要过滤? dont_filter = False 表示过滤
# 去重规则中是否已经有?(是否已经访问过,如果未访问添加到去重记录中。)
# 要求过滤,且去重记录里面有就变表示访问过了
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False # 已经访问过就不要再访问了 if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
# print('未访问过,添加到调度器', request)
self.queue.push(request)
return True

4. 下载器去调度器中获取任务,去下载

- 调用 scheduler.next_requests()

def next_request(self):
block_pop_timeout = self.idle_before_close
request = self.queue.pop(block_pop_timeout)
if request and self.stats:
self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
return request

总结几个问题

1. 什么是深度优先?什么是广度优先?

深度优先 :基于层级先进入到最深层级进行处理全部后再往上层级处理

广度优先 :基于从第一层级开始,每次层级处理之后进入下一层及处理

2. scrapy中如何实现深度和广度优先?

先进先出,广度优先 FifoQueue

后进先出,深度优先 LifoQueue

优先级队列:

DEPTH_PRIORITY = 1 # 广度优先

DEPTH_PRIORITY = -1 # 深度优先

3. scrapy中 调度器 和 队列 和 dupefilter 的关系?

调度器,调配添加或获取那个request.

队列,存放request。

dupefilter,访问记录。

调度器拿到一个 request 要先去 dupefilter 里面看下有没有存在

  如果存在就直接丢了

  如果不存在才可以放在队列中

然后队列要基于自己的类型来进行相应规则的存放

打比方来说:

  调度器 货车司机

  dupefilter 仓库门卫

  队列 仓库

  司机要往仓库放货。先问门卫

    门卫说里面有货了不让就没法放

    门卫说可以才可以放

      放货的时候仓库要按照自己的规则放货

  司机要拉货 仓库就基于自己的规则直接给司机就行了

示例 - 腾讯招聘分布式爬虫

流程剖析

使用 scrapy-redis 配合 mongoDB 的分布式爬虫

代码

items.py

import scrapy

class TencentItem(scrapy.Item):
# define the fields for your item here like:
zh_name = scrapy.Field()
zh_type = scrapy.Field()
post_id = scrapy.Field()
duty = scrapy.Field()
require = scrapy.Field()

settings.py

BOT_NAME = 'Tencent'

SPIDER_MODULES = ['Tencent.spiders']
NEWSPIDER_MODULE = 'Tencent.spiders' # Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0' # Obey robots.txt rules
ROBOTSTXT_OBEY = False
# 使用scrapy_redis的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 使用scrapy_redis的去重机制
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 爬取完成后是否清除请求指纹
SCHEDULER_PERSIST = True
# 'scrapy_redis.pipelines.RedisPipeline': 200
REDIS_HOST = '172.40.91.129'
REDIS_PORT = 6379 ITEM_PIPELINES = {
'Tencent.pipelines.TencentPipeline': 300,
# 'scrapy_redis.pipelines.RedisPipeline': 200 # redis 的开启这个
'Tencent.pipelines.TencentMongoPipeline': 200,
}

pipline.py

import pymongo

class TencentPipeline(object):
def process_item(self, item, spider):
print(dict(item))
return item class TencentMongoPipeline(object):
def open_spider(self, spider):
conn = pymongo.MongoClient('localhost',27017)
db = conn['tencent']
self.myset = db['job'] def process_item(self, item, spider):
self.myset.insert_one(dict(item))

spider.py

# -*- coding: utf-8 -*-
import scrapy
import json
from ..items import TencentItem class TencentSpider(scrapy.Spider):
name = 'tencent'
allowed_domains = ['tencent.com']
start_urls = [
'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1557114143837&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex=1&pageSize=10&language=zh-cn&area=cn'] def parse(self, response):
for page_index in range(1, 200):
url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1557114143837&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex=%s&pageSize=10&language=zh-cn&area=cn' % str(
page_index)
yield scrapy.Request(
url=url,
callback=self.parse_one_page
) # 一级页面解析
def parse_one_page(self, response):
html = json.loads(response.text) for h in html['Data']['Posts']:
item = TencentItem() item['zh_name'] = h['RecruitPostName']
item['zh_type'] = h['LocationName']
# 一级页面获取PostId,详情页URL需要此参数
item['post_id'] = h['PostId']
# 想办法获取到职位要求和职责,F12抓包,抓到地址
two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1557122746678&postId=%s&language=zh-cn' % \
item['post_id'] yield scrapy.Request(
url=two_url,
meta={'item': item},
callback=self.parse_two_page
) def parse_two_page(self, response):
item = response.meta['item']
html = json.loads(response.text)
# 职责
item['duty'] = html['Data']['Responsibility']
# 要求
item['require'] = html['Data']['Requirement'] yield item

Scrapy-redis 组件的更多相关文章

  1. Scrapy+redis实现分布式爬虫

    概述 什么是分布式爬虫 需要搭建一个由n台电脑组成的机群,然后在每一台电脑中执行同一组程序,让其对同一网络资源进行联合且分布的数据爬取. 原生Scrapy无法实现分布式的原因 原生Scrapy中调度器 ...

  2. scrapy 基础组件专题(八):scrapy-redis 框架分析

    scrapy-redis简介 scrapy-redis是scrapy框架基于redis数据库的组件,用于scrapy项目的分布式开发和部署. 有如下特征:  分布式爬取 您可以启动多个spider工 ...

  3. 基于async/non-blocking高性能redis组件库BeetleX.Redis

    BeetleX.Redis是基于async/non-blocking模式实现的高性能redis组件库,组件支持redis基础指令集,并封装更简便的List,Hashset和Subscribe操作.除了 ...

  4. Node.js与Sails~redis组件的使用

    有段时间没写关于NodeJs的文章了,今天也是为了解决高并发的问题,而想起了这个东西,IIS的站点在并发量达到200时有了一个瓶颈,于是想到了这个对高并发支持比较好的框架,nodeJs在我之前写出一些 ...

  5. laravel集成workerman,使用异步mysql,redis组件时,报错EventBaseConfig::FEATURE_FDS not supported on Windows

    由于laravel项目中集成了workerman,因业务需要,需要使用异步的mysql和redis组件. composer require react/mysql composer require c ...

  6. 基于Python,scrapy,redis的分布式爬虫实现框架

    原文  http://www.xgezhang.com/python_scrapy_redis_crawler.html 爬虫技术,无论是在学术领域,还是在工程领域,都扮演者非常重要的角色.相比于其他 ...

  7. 新生命Redis组件(.Net Core 开源)

    NewLife.Redis 是一个Redis客户端组件,以高性能处理大数据实时计算为目标.Redis协议基础实现Redis/RedisClient位于X组件,本库为扩展实现,主要增加列表结构.哈希结构 ...

  8. 【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流

    分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...

  9. scrapy 基础组件专题(九):scrapy-redis 源码分析

    下面我们来看看,scrapy-redis的每一个源代码文件都实现了什么功能,最后如何实现分布式的爬虫系统: connection.py 连接得配置文件 defaults.py 默认得配置文件 dupe ...

  10. scrapy 基础组件专题(七):scrapy 调度器、调度器中间件、自定义调度器

    一.调度器 配置 SCHEDULER = 'scrapy.core.scheduler.Scheduler' #表示scrapy包下core文件夹scheduler文件Scheduler类# 可以通过 ...

随机推荐

  1. AndroisStudio列选择模式

    今天敲代码的时候可能由于误开了“列选择模式”,在移动光标时,发现若光标所在列超过当前行的行尾列,不像一般情况应该跳到行尾,而变成了保持列的位置,停在了超过行尾的空白处, 如图:非一般情况 一般情况: ...

  2. Integer a= 127 与 Integer b = 128相关

    Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; a == b 与 c == d 的比较结果是什么? a == b ...

  3. 解决Win10系统本地主机,网络受限占用CPU过高的问题

    Win10版本为2015年第一个版本,第一次安装时没有这个问题,后面每次安装后开机正常,但是只要运行一段时间后(机子有运行各种软件的情况),发现CPU使用率为100% 即使结束所有在运行的程序,依然居 ...

  4. libstdc++适配Xcode10与iOS12

    编译报错 当你开心得升级完新 macOS,以及新 XCode,准备体验了一把 Dark Mode 编程模式,开心的打开自己的老项目的时候,发现编译不通过了╮(╯_╰)╭ 如果你的工程中如果依赖 lib ...

  5. 【学习】Linux Shell脚本编程

    1.脚本的组成和执行 Linux shell脚本的结构并不复杂,其主要由变量.内部命令以及shell的语法结构和一些函数.其他命令行的程序等组成,以下是一个简单的shell脚本. #!/bin/bas ...

  6. SQLServer之UNIQUE约束

    UNIQUE约束添加规则 1.唯一约束确保表中的一列数据没有相同的值. 2.与主键约束类似,唯一约束也强制唯一性,但唯一约束用于非主键的一列或者多列的组合,且一个表可以定义多个唯一约束. 使用SSMS ...

  7. daily english dictation 学习笔记[1-10]

    b站网址https://www.bilibili.com/video/av17188299/?p=2 1. Mother Teresa, who received a Nobel Peace Priz ...

  8. linux ssh免密登陆远程服务器

    10.170.1.18服务器免密登录到10.170.1.16服务器 首先登入一台linux服务器(10.170.1.18),此台做为母机(即登入其他linux系统用这台做为入口):执行一行命令生成ke ...

  9. oc中的委托模式

    通过一个例子来理解委托模式 首先定义个协议 协议(protocol) :它可以声明一些必须实现的方法和选择实现的方法  (在java中称为接口) // // StudentDelegate.h // ...

  10. 超哥笔记--linux准备知识(1)

    一 岗位 前端小姐姐 python后端大神 测试工程师 测试+python 测试开发 运维工程师(背锅侠) -安全运维 -linux系统管理员 -桌面运维(helpdesk) -IDC机房运维(服务器 ...