资讯动态

用scrapy-redis实现分布式爬虫:调度、去重与断点续爬全解析

发布时间:2026/10/15 14:16:37 来源:尧图企业网站定制
这个项目说起来并不是什么炫技的东西但真把它从单机版往分布式方向改造时坑确实比想象中多一点。简单一句话概括在 Scrapy 基础上把调度器和去重组件全部切换到依赖 Redis 的实现让多台机器上的爬虫进程共用同一个 URL 队列谁空闲谁取任务谁取到谁下载解析最终把招聘岗位信息按统一结构落库。标题里的目标站点换成了某招聘平台但你只要掌握了这套思路换成其他招聘站、租房信息站、商品列表页流程基本是通用的。这个项目适合这么几类人看想用招聘数据做薪资统计和技能趋势分析的已经写了好几个 Scrapy 单机爬虫想搞明白分布式到底怎么玩的以及在公司里搭过所谓“伪分布式”几台机器各爬各的最后再合并去重觉得无比痛苦的同学。读完这篇你至少能搞清楚分布式爬虫的调度逻辑、Redis 里那些 key 到底存了什么东西、多节点协作怎么避免重复和失控以及跑挂了之后怎么安全恢复。我尽量按实际动手的顺序写代码和配置都会给到位也把踩过的坑放在最后单独列出来。1. 项目整体设计与架构拆解1.1 核心需求与问题边界很多人在开始这类项目时最容易犯的错是一上来就翻配置、改代码却没想清楚自己到底要解决什么问题。我当时梳理一下核心需求其实是四层第一数据源是招聘网站的公开岗位列表页和详情页目标字段包括岗位名称、公司名称、薪资范围、工作城市、经验要求、学历要求、发布时间、岗位描述等。第二数据量不小如果要做城市维度的薪资分析光一个岗位关键词在全国范围内可能就是几万条多个关键词叠加后很可能到几十万条记录。第三不能一次性爬完就结束后续还要按周或者按月增量更新观察岗位发布趋势。第四整个过程要稳定抓了一半断掉不可怕可怕的是重启后从零开始。把需求理清后问题边界就清晰了单机爬虫跑一遍几十万条数据也能跑但耗时很长而且一旦进程崩溃、机房断电队列里的请求全部丢失之前抓到一半的数据也变成了一堆残缺记录。分布式的本质不是为了显得牛是为了让多个进程分摊任务并且把“待抓取队列”和“已抓取指纹”这两块状态放到所有节点都能访问的地方。1.2 为什么选 scrapy-redis而不是自己造轮子选型之前我认真评估过几条路。一是自己写多线程加 Redis FIFO 队列。理论上能实现但你需要自己处理请求的调度、去重指纹、失败重试、进程崩溃后的恢复实际工程量非常大而且容易写出一堆只在特定条件下正常的代码。二是把任务切分好多台机器各分配一批 URL 去爬。这种方案的痛处在于 URL 分配不均某个节点网络差或者被反爬封了整批任务就堵住更麻烦的是不同节点爬到的详情页有大量重复最后合并数据时要处理规模很大的重复数据。三是直接用 scrapy-redis。它的思路很简单Scrapy 原本的调度器是进程内的一个队列去重类是一个进程内的集合scrapy-redis 只是把这两个组件替换成操作 Redis 的版本。其他一切包括 Spider 逻辑、Item 定义、下载中间件、Pipeline全部保持原样。打个比方方便理解单机爬虫就像一个单人厨房菜谱一张接一张在厨师脑子里排队做完一个想下一个分布式爬虫就是多个厨师站在同一个冰箱前取食材冰箱里有全局的取料单 A做完一道菜要去冰箱登记一下避免另一厨师重复做同一道菜。Redis 就是那台所有人都看得见的冰箱。这个方案的好处是改造成本极低你基本不需要动已经写好的解析逻辑只需改几个设置项并让 Spider 从一个约定的 Redis key 读取起始 URL 而已。1.3 项目目录结构与组件划分项目用 Scrapy 官方脚手架创建我按习惯分成几块整体长这样job_spider/ ├── scrapy.cfg ├── job_spider/ │ ├── __init__.py │ ├── items.py # 定义岗位数据字段 │ ├── middlewares.py # User-Agent轮换、代理中间件 │ ├── pipelines.py # 数据清洗、MySQL入库 │ ├── settings.py # 核心配置含分布式开关 │ ├── spiders/ │ │ ├── __init__.py │ │ └── job.py # 实际抓取逻辑 │ └── db/ │ └── models.py # 建表语句与连接管理 ├── requirements.txt └── start_crawl.sh # 节点启动脚本从职责上看Spider 只负责页面解析和提取 URLPipeline 只负责清洗入库调度和去重交给 Redis 完成。这样拆的好处是调试时定位非常快某个字段没入库先看 Pipeline某个页面没抓到先看 Scheduler 里的队列有没有增长所有节点都停住了先看 Redis 的 key 是不是空了。2. 环境准备与工程初始化2.1 Python 依赖安装与版本选择这个项目不建议用太新的实验性版本Python 3.8 到 3.10 之间都可以我实际用的是 3.10。包依赖很简单python3 -m venv venv source venv/bin/activate pip install scrapy2.11.2 pip install scrapy-redis0.7.3 pip install redis4.6.0 pip install pymysql1.1.0版本说明一下scrapy-redis 这个库本身维护节奏比较慢0.7.x 已经足够稳定网上有些教程让你从 GitHub 拉最新 master没必要。scrapy 2.x 和 scrapy-redis 0.7 组合起来是经过大量项目验证的不建议乱升。另外Redis 建议单独装而不是用 Windows 上的老版本。Linux 环境直接yum install redis或apt install redis-server都行版本 5.0 以上就足够了我们用到的主要是 List、Set、Hash 这些基础结构不需要搞太复杂的模块。2.2 Redis 配置里最容易忽略的几个点很多同学在本地测试时 Redis 没设密码也没绑 IP分布式一开跨机器的节点连不上或者连上了之后因为没有密码被扫描端口的程序当肉鸡这些都是实际项目里踩过的教训。我的建议是至少在配置阶段就把下面几项设好bind 0.0.0.0 requirepass your-redis-password appendonly yes maxmemory 2gb maxmemory-policy allkeys-lrubind 0.0.0.0是为了允许其他机器上的采集节点访问前提是你的网络环境可信如果有多租户安全要求更应该配合防火墙白名单开放 6379 端口而不是一放全放。appendonly yes开启 AOF 持久化这样爬虫跑到一半 Redis 重启队列数据也不至于归零。maxmemory设一个上限防止 start_urls 塞太多或者 dupefilter 无限膨胀把 Redis 内存吃光。启动后一定要用 redis-cli 验证连通性redis-cli -a your-redis-password ping返回 PONG 说明基本连接没问题。2.3 创建项目与初步设置执行scrapy startproject job_spider后先生成一个最小的可用项目之后直接改 settings.py。创建完目录先清掉默认示例代码把items.py里的字段定义替掉再写 Spider。我用一个简单的测试页跑通流程时曾花过不少时间卡在“改了半天配置Spider 却根本没走到”的情况上。不要急着开分布式先保留单机方式写一个基础 Spider确认解析正确再往分布式切换。否则调试时你根本分不清是解析问题还是 Redis 调度问题。3. Spider 核心逻辑与页面解析3.1 构造列表页 URL 与翻页思路招聘平台的结构通常是两类页面搜索/列表页和详情页。列表页 URL 一般带有城市、关键词、页码之类的参数。我以一个搜索页为例URL 大致形如https://example-job-site.com/search?keywordpythoncity101010100page1这个 URL 不需要写死全部因为你的目标是要覆盖多个关键词、多个城市、多个分页。所以 Spide 里可以先把关键词和城市的组合列出来然后按页数生成种子地址。这里有一个非常关键的工程细节不要把所有分页 URL 一次性塞进 start_urls而是可以在第一个解析函数里根据总页数动态生成后续请求也可以预先批量生成全部页码。两种方式各有适用场景。如果网站明确显示搜索结果页数比如共 100 页你可以写一个循环生成 1 到 100 的 URL 列表一次性交给调度器如果网站是懒加载或者页数上限不确定更稳妥的是先用第一页解析出“下一页”链接或总页数再动态拼接。我刚做这个项目时为简单起见把所有关键词、城市和页码组合都放到了 start_urls 里结果列表页有将近二十万个 URL虽然分布式能扛住但 Redis 队列瞬间暴涨不利于观察进度。后面改成按“城市 8 个关键词 10 个”先抓第一页解析完成后再根据返回的总页数动态生成后续分页请求整个队列压力就平稳多了。3.2 编写继承自 RedisSpider 的采集类scrapy-redis 的使用方式很简单Spider 不直接继承scrapy.Spider而是继承scrapy_redis.spiders.RedisSpider。它的核心变化是你不再把 start_urls 写在代码里而是写一个redis_key程序启动后不会马上抓取而是等待你的种子 URL 进入到那个 Redis key 队列它才开始消费。这是分布式爬虫必须的启动方式因为所有节点代码都是一样的你不能让每个节点各自带一份 start_urls那样天然会重复。示例代码import scrapy from scrapy_redis.spiders import RedisSpider from job_spider.items import JobItem class JobSpider(RedisSpider): name job redis_key job:start_urls allowed_domains [example-job-site.com] def parse(self, response): # 处理列表页提取详情页链接 detail_links response.css(a.job-title::attr(href)).getall() for href in detail_links: url response.urljoin(href) yield scrapy.Request(url, callbackself.parse_detail) # 处理翻页提取下一页链接 next_page response.css(a.next-page::attr(href)).get() if next_page: yield scrapy.Request(response.urljoin(next_page), callbackself.parse) def parse_detail(self, response): # 详情页字段解析 item JobItem() item[job_title] response.css(h1::text).get() # ... yield item注意RedisSpider 的内部实现里make_requests_from_url已经被替代了你不能像普通 Scrapy 那样在 Spider 里重写start_requests()去自动生成初始种子因为所有初始 URL 都通过外部推入redis_key这个 List。这个细节很容易忽略很多人改完代码发现爬虫跑起来却没有任何请求日志就是因为它一直在等 Redis 队列里的种子数据。3.3 Item 字段设计与详情页取值Items 定义要覆盖岗位分析所需的核心字段我在这份项目里定了这些import scrapy class JobItem(scrapy.Item): job_id scrapy.Field() # 详情页 URL 的特征哈希用于去重 job_title scrapy.Field() company_name scrapy.Field() salary_text scrapy.Field() # 原始薪资如 20-40K·16薪 city scrapy.Field() experience scrapy.Field() # 经验要求文本 education scrapy.Field() publish_time scrapy.Field() job_desc scrapy.Field() job_url scrapy.Field() crawl_time scrapy.Field()详情页里的字段如果页面结构稳定用 CSS 或 XPath 提取都行。比如岗位名称用h1::text公司名称用.company-name::text薪资用.salary::text这些都是基于常规结构的取法实际要以你调试时的 response.css 输出为准。我在前期写过不少getall()然后逐一清空列表的小工具函数但最有效的方式还是在 Scrapy shell 里现测现取不要盲目抄网上的选择器。另外一个经验是对抓下来的文本一定不能直接入库。招聘网站的薪资字段往往是“20-40K·16薪”这种岗位描述则是一大段 HTML里面还混着各种空格、换行、特殊符号。所以需要在 Pipeline 里做清洗后面单独说。3.4 请求频率、User-Agent 与代理设置做分布式爬虫很多人一上来就把并发调到 64、128觉得分布式就该暴力爬。实际上这是一个非常危险的认知。大部分招聘平台都有比较严格的反爬策略分布式节点如果不知道收敛最可能出现的是 403 点遍地开花然后是 IP 被临时限制最后整个采集任务瘫痪。我的设置习惯是DOWNLOAD_DELAY 1.0 CONCURRENT_REQUESTS 16 ROBOTSTXT_OBEY FalseROBOTSTXT_OBEY我之所以设 False原因比较现实很多平台的 robots.txt 对爬虫并不友好但我们抓取的是公开岗位信息而且我们做了限速。注意这里不是鼓励无视协议恰恰相反如果目标站明确禁止那就必须通过官方接口或其他合规方式获取。我在实际项目中会先人工看一眼 robots.txt再决定是否调整。合规是一根底线宁可慢不要碰红线。User-Agent 最好做一个可轮换的下载中间件不要让所有节点都发送同一个 UA。最简单的方式是准备一个 UA list每次请求随机取一个代码很简单class RandomUserAgentMiddleware: def process_request(self, request, spider): ua random.choice(spider.settings.get(UA_LIST, [])) request.headers.setdefault(bUser-Agent, ua)代理方面如果测试目标对 IP 限制不明显可以不挂代理如果明显出现 403则需要购买可靠代理池并按请求轮换。这个项目因为是自己做数据分析用我先用低并发重试顶住了没有马上接入付费代理。4. 分布式关键配置与 Redis Key 解读4.1 settings.py 里的核心改动这是整个项目最重要的一部分只需要在 settings.py 里做几处修改就能让 Scrapy 从单机调度切成 Redis 调度。我直接贴自己验证过的配置DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter SCHEDULER scrapy_redis.scheduler.Scheduler SCHEDULER_PERSIST True SCHEDULER_QUEUE_CLASS scrapy_redis.queue.PriorityQueue REDIS_HOST 192.168.1.10 REDIS_PORT 6379 REDIS_PARAMS { password: your-redis-password, db: 0, } REDIS_START_URLS_KEY job:start_urls逐项解释一下DUPEFILTER_CLASS覆盖了默认的RFPDupeFilter该实现会把每个请求的指纹写入 Redis 的一个 Set这样所有节点在发起请求前都会去这个 Set 查重达到“全局限重”的目的。SCHEDULER覆盖了默认调度器调度器把待抓取请求写入 Redis 的队列而不是本地内存。SCHEDULER_PERSIST True意味着爬虫终止时不清空 Redis 里的队列和去重集合下次启动能够断点续爬。要是设成 False每次爬虫结束都会清空分布式意义就少了一大半。SCHEDULER_QUEUE_CLASS选择优先级队列。默认PriorityQueue会让 Scrapy 的请求优先级生效如果你的项目里没有设置请求优先级用FifoQueue也可以队列语义更直观。REDIS_START_URLS_KEY这里可以覆盖默认的spider:name:start_urls我习惯在配置里显式写死避免拼接期望不对。还有一个容易踩的坑如果你同时在代码里保留了原来的scrapy.dupefilter.RFPDupeFilter这种配置那么你会得到看似正常但去重失效的诡异现象。配置文件里要确保只有一处设置不要叠加。4.2 Redis 里的几个关键 key不再神秘分布式跑起来之后你需要习惯用redis-cli去看 Redis 内部状态。我很建议开一个终端窗口随时盯着这几个 key 的变化Redis Key类型存储内容监控命令job:start_urlsList种子URL列表分布式爬虫启动的源头LRANGE job:start_urls 0 -1job:requestsList/ZSet待抓取的请求队列LLEN job:requests或ZCARD job:requestsjob:dupefilterSet已抓取请求的指纹SCARD job:dupefilterjob:itemsList如果调用了默认Pipeline会暂存 itemLLEN job:itemsspider:job:start_urls等List老版本默认键名KEYS *可查看所有相关键注意默认redis_key是job:start_urls请求队列键名默认是job:requests去重集合键名默认是job:dupefilter。但如果你设置了REDIS_START_URLS_KEY又要知道 scheduler 队列和去重集合仍使用 spider name 拼接所以在调试前最好先通过KEYS *看一下实际生成了哪些键别凭记忆猜名字。实际在推送种子 URL 时使用redis-cli -a your-redis-password LPUSH job:start_urls https://example-job-site.com/search?keywordpythoncity101010100page1用LPUSH还是RPUSH影响队列的方向scrapy-redis 内部用lpop从左侧消费所以你可以根据自己的需求控制消费顺序。我习惯用RPUSH保持顺序执行避免列表页先抓后面的页码产生不必要的乱序。4.3 多节点协作的节奏与注意事项完成了上面的配置理论上你把项目代码复制到第二台机器执行scrapy crawl job两台机器上的进程会立刻连到同一台 Redis共同消费job:requests队列。这里有个非常容易误导人的点每台机器的并发设置是各算各的比如单机CONCURRENT_REQUESTS16两台机器同时跑总并发就是 32。别以为配置了一次并发所有节点都共享这个数字。反之如果某台机器性能差可以单独把它的并发调低。节点数怎么定我的经验是不要拍脑袋。先看 Redis 队列的消费速度如果每秒出队的新请求数量已经高于解析速度说明瓶颈在解析和入库如果队列积压量持续上升而且目标网站没有出现反爬拦截则可以加节点。反过来如果出现了大量 403第一反应应该是降并发、降节点而不是继续堆机器。实际操作中还要注意让所有节点的工作目录和代码版本保持一致。曾有段时间我在一台节点上改了字段名结果那台节点不断往 Redis 里推送带旧字段名的 item导致入库报错排查了整整一下午。后来我用构建产物加校验文件的方式确保每个节点代码一致再也没出过这类问题。5. 数据管道、去重与存储设计5.1 清洗预处理做成 Pipeline 原子操作岗位数据入库前Pipeline 至少要完成几件事文本去空格、薪资标准化、URL 特征提取、时间格式化。我封装了一个清洗模块以薪资为例import re def parse_salary(salary_text): # 示例输入20-40K·16薪 或 面议 if not salary_text or 面议 in salary_text: return None, None nums re.findall(r(\d\.?\d*), salary_text.replace(万, )) if not nums: return None, None low float(nums[0]) high float(nums[1]) if len(nums) 1 else low return low, high对于经验字段类似地提取“1-3年”中的最低年限和最高年限时间字段则统一格式化成YYYY-MM-DD HH:MM:SS。这个标准化非常有用后面做“平均薪资”“技能分布”统计时不用再在分析脚本里二次清洗。清洗逻辑最好全部集中在 Pipeline 里而不是散落在各个解析函数。否则数据来源一旦增加SQL 入库前的格式就不可控了到时候查数据质量问题会查到崩溃。5.2 MySQL 表结构与索引设计Pipeline 的出口是 MySQL。建表语句示例CREATE TABLE job_position ( id INT AUTO_INCREMENT PRIMARY KEY, job_key VARCHAR(64) NOT NULL UNIQUE, job_title VARCHAR(128), company_name VARCHAR(256), salary_low DECIMAL(10,1), salary_high DECIMAL(10,1), salary_text VARCHAR(32), city VARCHAR(32), experience_low INT, experience_high INT, education VARCHAR(16), publish_time DATETIME, job_desc TEXT, job_url VARCHAR(512), crawl_time DATETIME, UNIQUE KEY uk_job_key (job_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;job_key用来做内容级去重。这个字段我用的是“岗位名 公司名 城市 详情 URL 摘要”的组合。直接使用 URL 做唯一键有个问题同一个岗位信息可能存在于不同关键词搜索结果下详情 URL 可能相同也可能带了不同跳转参数简单用全文 URL 会造成重复。所以我会用 URL 路径部分结合标题、公司字段做一次哈希并截断既保证唯一性又防止过长索引。索引方面核心查询条件是城市、发布时间、岗位关键词所以至少给city和publish_time建索引。数据量到几十万条后没有索引会导致分析查询秒变全表扫描。5.3 断点续爬与增量更新策略项目跑了一段时间后你会发现“分布式”和“可恢复”同样是刚需。如果某台节点半夜因为网络问题断开其他节点还在照常工作第二天你只需要把那台节点重新启动它会继续从 Redis 队列消费任务不会重新抓取已经抓过的 URL因为 Redis 里的 dupefilter 集合还在。增量更新的思路是这样的每周把keyword和city列表重新组合生成第一批种子 URL推入job:start_urls让爬虫重新爬一轮列表页详情页中如果同一个job_key已经存在就更新publish_time和job_desc如果发现之前没有记录就执行插入。这样长期维护下来的数据表既能反映岗位变化趋势又不会无限膨胀重复数据。Pipeline 里的入库逻辑我建议写成insert_sql INSERT INTO job_position (...) VALUES (...) ON DUPLICATE KEY UPDATE ...用ON DUPLICATE KEY UPDATE而不是先查再插可以省掉大量查询开销。这也是实际项目中提高性能最明显的一处优化。6. 常见问题与排查技巧实录这部分是实际跑过程中踩过、或者帮别人排查过的真实问题我按“现象-原因-解法”整理成速查表后面再展开讲几个典型的。现象大概率原因解决办法爬虫启动后没有任何抓取日志Redis 的 start_urls 里没有种子 URLRPUSH job:start_urls 第一个URL所有节点都在抓同一个 URL请求指纹还没同步到 Redis多节点竞争新 URL属于正常现象队列消费后会自动避免检查SCARD job:dupefilter是否持续增长连续出现 403 错误并发过高或 UA 被识别调低CONCURRENT_REQUESTS增加 UA 池和下载延迟入库时中文乱码终端/连接字符集没配置MySQL 连接参数加charsetutf8mb4抓到一定数量后停止队列已空或 Redis dupefilter 误杀看LLEN job:requests如果为 0 说明任务完成Redis 内存暴涨dupefilter 和 requests 队列过大设置 maxmemory考虑用布隆过滤器替代超大 Set不同节点入库数据重复详情页 URL 有多个不同参数变体用job_key做内容级去重6.1 改完配置发现爬虫一动不动这个现象太典型了。你敲了scrapy crawl job终端显示启动信息但就是没有任何请求打出去。墙内很多教程在讲分布式时默认读者知道要往 Redis 推种子URL结果新手就卡在这里。解法很简单手动执行redis-cli -a your-redis-password RPUSH job:start_urls https://example-job-site.com/search?keywordpythoncity101010100page1然后观察日志几秒内应该出现抓取请求。如果你用的是默认redis_key但配置里设置了REDIS_START_URLS_KEY请确认两者一致否则推错 key爬虫永远等不到任务。另外种子 URL 的状态要检查。如果这个 URL 曾经在 dupefilter 里出现过并且你重新推送一模一样的 URL它会因去重被忽略。测试时可以先把 dupefilter 删掉再推送redis-cli -a your-redis-password DEL job:dupefilter注意这个命令会把去重状态清空生产环境决不能在任务执行到一半时随意删。只建议在明确要重爬所有页面时使用。6.2 多节点下总感觉“调度变慢了”分布式并非一定比单机快很多尤其在目标站点限速严格的情况下。如果每台机器的DOWNLOAD_DELAY都设成 0.5 秒10 台机器总体速率也只有每秒 20 个请求并不算快。如果确认响应时间正常、没有 403但吞吐量提不上去可以从这几个方向查检查 Redis Server 的 CPU 和连接数INFO STATS里能看到命令执行量和连接数检查节点机器是否有 CPU 密集型解析任务比如 job_desc 里的清洗逻辑写得非常重拉低了单节点吞吐检查 MySQL 是否存在慢查询Pipeline 入库阻塞了整条链路。分布式调度本身是很快的Redis 吞吐能轻松支持上万 QPS瓶颈通常不会在调度器而在于下游存储和解析。6.3 数据入库重复但请求明明去重了这一点值得展开讲。RFPDupeFilter去重的是“请求”指纹由请求 URL、方法、请求体等生成。如果同一个详情页 URL 在不同列表页中以不同形式出现比如带?fromsearch_1和?fromtag_2那指纹就不一样结果同一个页面被反复抓取。更常见的是不同城市的关键词搜索下同一家公司的同一个职位出现多次详情 URL 相同但部分平台会带上utm之类的跳转参数导致去重失效。因此请求级去重只是第一道防线入库时必须有内容级去重。我维护的job_key就是第二道防线把标题、公司、城市做标准化哈希后查重。两道防线结合重复率能压到非常低。6.4 字符集乱码与解析为空招聘平台不少页面仍然使用 GBK 或 GB18030 编码而 Scrapy 默认按响应头里的 charset 解码有一部分页面没有声明 charset或者声明错误就会解析出一堆乱码或空列表。处理方式是在process_response里加一个编码修正下载中间件class EncodingMiddleware: def process_response(self, request, response, spider): if bmeta charset not in response.body and gb in request.url: response response.replace( textresponse.body.decode(gb18030, errorsignore) ) return response如果页面是 GBK 但声明成 UTF-8直接用GB18030暴力解码通常能救回来。不过这个方法要谨慎用只对明确目标站源做不能全局通用否则百分之百误伤其他编码页面。6.5 爬虫跑到半夜无响应如何安全重启分布式的好处是某个节点挂了其他节点能继续跑。但如果 Redis 也重启了或者跑完一批任务后没有新任务你需要一套恢复流程先确认 Redis 队列里有没有积压LLEN job:requests如果队列为空而且你的分页任务还没覆盖完需要再往 start_urls 推入下一批种子 URL如果队列有积压直接重启所有节点即可dupefilter 和队列都还在节点会自动从断点继续如果出现节点代码版本不一致务必先更新为同一版本再重启否则会出现入库字段缺失。不要为了省事把SCHEDULER_PERSIST改成 False这样每次重启都清空队列大规模采集时会非常浪费。7. 项目复盘与后续演进方向7.1 实测下来的性能与心得最后说一下实际效果。数据量级大概在几十万条记录时三台节点、每台并发 16、下载延迟 1 秒左右跑完全量列表页和详情页大约需要十几个小时。如果单机跑同样的量级可能要两三天而且一旦中断恢复成本高得令人崩溃。分布式最直接的收益不只是“快”更多是“稳”——任何一个节点瞬断其他节点不需要任何人工干预就能继续扛。我个人的体会是做这类项目最忌讳的是把大量时间耗在调参上。先把单个节点、单个 URL 的抓取解析跑通把 Pipeline 入库跑通再打开分布式。很多同学连单机解析都还没验证就开始分布式调优最后只会得到一个“分布式、但错误很多”的复杂系统。还有一点经验调度队列里永远不要一上来就塞满所有 URL。先推少量种子观察解析函数是否稳定再逐步扩大。这和最小化发布是一个道理网络上的资源看得到但摸不得盲目的量只会放大问题。7.2 建在分布式爬虫之上的数据应用爬下来的岗位数据本身是死数据真正有价值的是基于它的分析。我后续在这个数据集上做了几个方向的扩展也可以供你参考。一是用 SQL 按城市、岗位关键词聚合输出平均薪资区间这是最基本的统计二是把 job_desc 清洗后做词频统计和技能共现分析看看不同技术栈在岗位描述里的热度变化三是定时增量抓取按周对比岗位发布数量的涨跌。这些应用跑起来后爬虫的价值就从“采集”延伸到了“数据洞察”。技术上还可以继续演进。dupefilter 集合越来越大时可以集成 bloom filter 算法族来降低内存Redis 队列可以用哨兵或集群模式做成高可用入库层可以接消息队列缓冲避免 MySQL 成为瓶颈监控方面则可以做一个简单的看板实时展示 Redis 队列长度、节点存活状态和入库速率。如果让我重写一遍这个项目我一定会先把数据模型和清洗规则设计得再稳一点再写爬虫。因为后期调整字段、清洗逻辑远比多抓几千条 URL 更耗时。整体来看scrapy-redis 给分布式爬虫提供了一个很实用的基础底座它不负责解决所有问题但把最麻烦的调度和去重问题接手了。剩下的事就是你想清楚要爬什么、存成什么样、分析出什么结论。这个思路清晰了距离跑出一个真正可用的采集系统就不远了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑