
简介基于scrapy-redis的多平台新闻资讯采集系统面向有一定Python基础、希望掌握分布式爬虫开发的工程师。项目利用Redis实现请求调度与URL去重支持对多个新闻站点并行采集并配套数据处理、存储与部署配置。资源共41个文件以27个Python脚本为主涵盖spiders爬虫、pipelines管道、middlewares中间件、settings配置等核心模块另含scrapy.cfg、docker-compose.yml、Dockerfile等工程化文件便于快速理解项目结构和环境搭建。压缩包整体仅32KB轻量但结构完整适合作为学习scrapy-redis架构和扩展功能的参考模板。已有41人学习下载。通过阅读源码可掌握Redis Scheduler与DupeFilter的接入方式、多站点爬虫的组织方法以及MySQL存储的完整链路并能基于此改造适配自己的采集需求是入门分布式爬虫的实用案例。1. 基于scrapy-redis的多平台新闻资讯采集系统单机爬虫为什么先撑不住多平台新闻采集听起来简单真正做了才知道瓶颈永远不在写爬虫而在调度和去重。基于scrapy-redis的采集系统核心是把待抓取URL队列和已抓取指纹集合搬到Redis上让多台机器共享同一个任务池谁空闲谁取任务。新闻网站的特征是栏目多、更新密集、URL参数杂乱单机爬虫往往跑到一半就被反爬策略限速或者进程一崩队列清零整个采集链路停摆。这篇文章按我实际部署的顺序把scrapy-redis的原理、搭建、参数调优和踩坑记录完整过一遍适合正在做资讯聚合、舆情监控和内容库建设且手上已经有一至三台空闲服务器的后端工程师。2. scrapy-redis的分布式拆解调度器、去重器和Redis里到底存了什么2.1 scrapy-redis改变了爬虫哪三个行为任务队列、去重集合、断点续爬要理解scrapy-redis先要理解原生Scrapy单机运行时内部发生了什么事。一个普通的Scrapy爬虫调度器在内存里维护一个优先队列所有待抓取请求都压进这个队列同时RFPDupeFilter在内存里维护一个Python集合用来记录已经见过的请求指纹。Spider启动时把start_urls转换成初始请求之后调度器不断弹出请求交给下载器执行下载器返回Response再交给Spider解析解析出的新请求又回到调度器——整个闭环跑在单进程里。scrapy-redis做的事情是把这个闭环里的两个关键存储全部替换成Redis。待抓取请求不再是内存优先队列而是Redis里的List或ZSet去重集合不再是Python的set而是Redis的Set数据结构。这样产生的第一个变化是队列共享三台机器跑同一个Spider它们从同一个Redis队列里取请求天然实现了消费端负载均衡。第二个变化是断点续爬进程崩溃、机器重启队列和去重集合还在Redis里重新拉起爬虫接着跑不需要担心丢任务。第三个变化是任务注入解耦给某个Spider分配新任务不用登录机器执行命令直接往Redis里写一个URL就能触发。这三个变化放在新闻资讯采集场景里非常关键。新闻源站点多但每个站点的更新量不大如果每个站单独起一台爬虫资源利用率很低而把它们合并到同一套分布式队列里哪台机器有空就多抓几个栏目整体吞吐量上去了部署和运维反而简化了。另外新闻爬虫最常见的故障形态是长时间运行后内存泄漏、进程被杀有了Redis里的断点重启成本降到几乎为零。2.2 四个核心组件Scheduler、DupeFilter、Pipeline和RedisSpider的协作方式scrapy-redis对外暴露了四个可以直接替换的组件理解它们各自的职责后面配置时才能知道每个参数在调什么。Scheduler是调度器。它取代了原生Scrapy的PriorityScheduler作用是从Redis队列里弹出下一个待抓取请求并在请求完成后决定是否把它放回队列。Scheduler同时负责调用DupeFilter判断一个请求是否已经抓过。scrapy-redis提供了三种队列实现FifoQueue先进先出、LifoQueue后进先出、PriorityQueue按优先级出队。新闻采集默认用FifoQueue同一个站点列表页的翻页顺序就是从左到右翻比较符合直觉PriorityQueue用于多平台混合任务时给实时性高的站点插队后文会展开。DupeFilter是去重器。scrapy-redis的默认实现是RFPDupeFilter它对请求URL做指纹计算然后检查指纹是否在Redis的Set集合里。这个去重是全局的不是单机内存去重也就是说A机器已经抓过的URLB机器不会重复抓。对新闻采集来说这是最重要的特性因为多家站点经常会互相转载同一条新闻多机协作时靠本地内存去重根本挡不住重复抓取。Pipeline在scrapy-redis里默认有一个RedisPipeline作用是把抓取到的Item序列化后写入Redis一般写入一个List。但生产上我几乎不用它来做最终存储——把几十万条新闻Item堆在Redis里既不经济也不方便查询。更常见的做法是只利用它的暂存能力或者直接用自研Pipeline把Item写入MySQL、MongoDB或Elasticsearch。RedisPipeline真正的价值在于调试阶段爬虫跑起来后敲一条LRANGE key 0 -1就能看到抓到了什么比查数据库直观。RedisSpider是Spider基类。它和普通scrapy.Spider的区别在于start_urls不再写在Spider代码里而是启动时从一个Redis Key比如news:start_urls读取初始URL。爬虫启动后不再是一次性拉取所有初始URL而是持续监听这个Key外部通过LPUSH或SADD往里写URL爬虫感知到新任务后就开始抓取。这个机制非常适合新闻资讯编辑发现某个突发新闻源需要立即采集直接往Redis里推一条链接即可不用重启任何进程。2.3 新闻资讯场景为什么天然匹配scrapy-redis多站点、增量、谁先到谁先抓新闻采集有两个显著特征。第一是增量导向同一个站点每天的新闻URL几乎全是新的历史数据很少重抓所以请求队列是一个高速消费模型积压量不大但请求生成速度快列表页一翻页就能产生几百个详情页请求。第二是站点差异大有的新闻站响应极快几百毫秒有的站拖着五秒才返回如果所有请求都压在单台机器上慢站会拖住快站。scrapy-redis的Redis队列天然支持“谁先到谁先抓”的调度模型但这个模型在混合多站点时有一个隐含问题队列是全局FIFO快站和慢站的请求混在同一个队列里如果慢站的列表页解析出大量请求会占据队列头部导致快站的请求延迟升高。解决思路有两种。一种是用PriorityQueue给不同站点设置不同的优先级快站优先级高出队时优先被弹出。另一种是队列分离按站点或按站点响应速度拆成多个Redis Key每个Key对应一个Spider实例让快站和慢站各跑各的队列这是我在生产上更推荐的方式因为隔离效果更彻底单个站点反爬升级时不会波及其他站点。理解这个协作方式后配置scrapy-redis就变成了一个有明确目标的动作把Scrapy的调度器、去重器指向Redis确认队列类型再决定RedisPipeline是不是自己需要的。下面进入工程落地。3. 搭一个可运行的多平台新闻采集工程settings配置、RedisSpider和数据入库3.1 环境检查和Redis初始化连接用REDIS_URL而不是逐项配置动手前先确认环境。scrapy-redis要求Redis服务端在2.8以上客户端建议用redis-py的最新稳定版本。检查Redis是否可用直接看PING的返回redis-cli -h 127.0.0.1 -p 6379 PING # 期望输出: PONG然后是Python侧依赖的安装。一个常见做法是在虚拟环境里安装scrapy、scrapy-redis和redis-pypip install scrapy scrapy-redis redis装完后用Python验证连接import redis r redis.Redis(host127.0.0.1, port6379, db0, socket_timeout5) print(r.ping()) # 输出 True 表示连接正常连接正常后初始化时不建议在代码里逐项写host、port、password而是统一用REDIS_URL格式是redis://:passwordhost:port/db。用REDIS_URL的好处是配置集中、跨环境迁移方便测试环境指向本机6379生产环境改一个环境变量即可不需要动代码。3.2 settings.py里必须改的六个参数调度器、队列、去重和持久化爬虫工程的灵魂在settings.py。原生Scrapy项目接scrapy-redis只需要在settings.py里替换和追加以下几项# settings.py # 1. 替换调度器为scrapy-redis的分布式调度器 SCHEDULER scrapy_redis.scheduler.Scheduler # 2. 替换去重器为基于Redis的指纹去重器 DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter # 3. 请求队列类型FifoQueue / LifoQueue / PriorityQueue SCHEDULER_QUEUE_CLASS scrapy_redis.queue.FifoQueue # 4. 是否在爬虫退出时保留Redis中的队列和指纹集合 SCHEDULER_PERSIST True # 5. Redis连接串包含密码和库编号 REDIS_URL redis://:yourpassword127.0.0.1:6379/1 # 6. scrapy-redis启动时读取初始URL的Key前缀 REDIS_START_URLS_AS_SET True这里逐个说明。SCHEDULER和DUPEFILTER_CLASS是scrapy-redis的入口缺了它们爬虫只是把Scrapy原样跑单机。SCHEDULER_QUEUE_CLASS用FifoQueue保持新闻列表页的翻页顺序。SCHEDULER_PERSIST True的含义是爬虫正常结束或被Ctrl-C中断后Redis队列里的剩余请求和去重指纹都被保留下次启动继续抓。如果设为False爬虫结束时调度器会清除Redis里的队列数据断点续爬的能力就没了。REDIS_START_URLS_AS_SET True值得多说一句。这个参数决定scrapy-redis从Redis里读初始URL时是当作List还是Set。设为True时写入初始URL要用SADD而不是LPUSH好处是去重——同一个初始URL重复写入不会有副作用适合多台机器同时启动爬虫时防止大家把同一批URL重复注入。3.3 写一个能兼容多个平台的RedisSpiderstart_urls从哪里来准备工作做完写Spider。多平台新闻爬虫的一个常见做法是每个平台一个Spider类但继承同一个基类把公共的解析逻辑放在基类里。这样scrapy-redis的RedisSpider只需要写一次# spiders/news_base.py import scrapy from scrapy_redis.spiders import RedisSpider from urllib.parse import urljoin class NewsBaseSpider(RedisSpider): 多平台新闻采集的基础爬虫。 redis_key 是初始URL所在的Redis Key 启动前先向该Key写入各平台首页URL。 name news_base # 所有子类共享的Redis Key初始URL从这里读取 redis_key news:start_urls def parse(self, response): # 子类重写: 不同网站的新闻链接选择器完全不同 news_links self.extract_news_links(response) for link in news_links: # 相对路径补全为绝对URL absolute_url urljoin(response.url, link) # 新闻详情页交给 parse_detail 处理 yield scrapy.Request(absolute_url, callbackself.parse_detail) def parse_detail(self, response): # 子类重写: 提取标题、正文、发布时间、来源 item self.extract_news_item(response) if item: yield item def extract_news_links(self, response): raise NotImplementedError def extract_news_item(self, response): raise NotImplementedError这个基类做的事情很纯粹从列表页提取新闻链接构造详情页请求再把详情页的响应交给parse_detail提取结构化信息。子类只重写extract_news_links和extract_news_item两个方法# spiders/sina_news.py from spiders.news_base import NewsBaseSpider class SinaNewsSpider(NewsBaseSpider): name sina_news redis_key news:start_urls:sina def extract_news_links(self, response): # 以新浪新闻首页为例实际选择器随站点结构调整 return response.css(a[href*news.sina.com.cn]::attr(href)).getall() def extract_news_item(self, response): return { title: response.css(h1::text).get(), content: response.css(#artibody p::text).getall(), publish_time: response.css(.time-source::text).get(), source: sina, }注意这里redis_key被指定为news:start_urls:sina。scrapy-redis的机制是爬虫启动后阻塞监听该Key外部用SADD news:start_urls:sina http://news.sina.com.cn/写入入口URL爬虫立刻开始消费。这样设计的好处是多平台入口分离——给澎湃推送URL不会影响新浪队列的消费进度某一个平台的入口URL写错了也不会污染其他平台。3.4 把多个平台的抓取结果汇总到一张表Item与Pipeline的落地写法多平台爬虫的Item应该统一结构至少包含title、content、url、source、publish_time、crawl_time六个字段。统一的Item定义让后续入库、去重和搜索都简单# items.py import scrapy class NewsItem(scrapy.Item): url scrapy.Field() # 原文链接 title scrapy.Field() # 标题 content scrapy.Field() # 正文纯文本 publish_time scrapy.Field() # 页面标注的发布时间 source scrapy.Field() # 来源站点标识如 sina、thepaper crawl_time scrapy.Field() # 本系统抓取时间Pipeline负责把Item写入目标存储。这里有一个重要选择是否启用scrapy-redis自带的RedisPipeline。我一般会关闭它改用自己实现的MySQL Pipeline因为新闻数据最终要提供给业务查询存在Redis里没有意义。一个典型的MySQL Pipeline如下# pipelines.py import pymysql from datetime import datetime class MySQLNewsPipeline: def open_spider(self, spider): self.conn pymysql.connect( host127.0.0.1, usernews_writer, passwordyourpassword, databasenews_db, charsetutf8mb4, ) self.cursor self.conn.cursor() def process_item(self, item, spider): # 以url作为唯一键重复抓取时更新标题和正文不插入新行 sql INSERT INTO news (url, title, content, publish_time, source, crawl_time) VALUES (%s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE title VALUES(title), content VALUES(content), crawl_time VALUES(crawl_time) self.cursor.execute(sql, ( item[url], item[title], .join(item[content]), item[publish_time], item[source], datetime.now(), )) self.conn.commit() return item def close_spider(self, spider): self.cursor.close() self.conn.close()这段Pipeline有两个细节值得注意。第一个是用了ON DUPLICATE KEY UPDATE而不是先SELECT再判断在不在——新闻详情页可能因为转载修正被重新抓取这种写法用一次数据库操作完成“存在则更新、不存在则插入”避免并发下重复插入。第二个是传入时间用datetime.now()而不依赖Item里的crawl_time字段防止Spider端时钟不准导致入库时间混乱。url字段在表里必须建UNIQUE KEY这是去重的最后一道保障——即使scrapy-redis的去重器出了问题数据库层面还能兜底。接入Pipeline只需要在settings.py里激活ITEM_PIPELINES { pipelines.MySQLNewsPipeline: 300, }到这一步一套能跑通的多平台新闻采集系统雏形已经具备了Redis保存任务队列和去重集合多个平台各自的Spider从各自Key里取初始URL解析出的新闻Item写入MySQL。但真正生产上线你很快就会撞上几个扎手的坑。4. scrapy-redis跑新闻采集的四个必踩的坑现象、原因和解决办法4.1 Redis连接数被打满爬虫全部卡住不动现象爬虫运行几个小时后日志里突然全部变成TimeoutError: Redis connection timed out或者ConnectionRefusedError后续请求不再执行页面也不报错整个爬虫处于假死状态。原因scrapy-redis的Scheduler和DupeFilter在每次请求或每条Item经过时都从连接池获取Redis连接。如果某台机器的并发请求数是32连接池默认max_connections可能只有10那么高并发下Redis连接被抢空等待连接的请求互相阻塞最终超时。新闻站点的列表页响应很快并发一高瞬时可能产生几十个请求连接池很快被打穿。解决一是调大连接池上限二是在settings.py里明确设置连接超时和Socket超时REDIS_PARAMS { max_connections: 50, socket_timeout: 30, socket_connect_timeout: 30, }另一个容易忽略的点是Redis服务端本身的maxclients配置。Linux下默认Redis最多允许10000个客户端连接看起来够多但多个爬虫实例加上监控和运维工具同时连接时也容易被耗尽。检查Redis当前连接数redis-cli -h 127.0.0.1 INFO clients | grep connected_clients如果接近maxclients优先排查是否有死循环爬虫在频繁重连而不是直接调大上限——连接数异常升高往往是代码里有连接泄漏比如每次请求都新建了Redis连接而没有归还。scrapy-redis的连接池一般不会泄漏但如果自己写了Pipeline里的Redis操作要注意用完归还。4.2 同一条新闻被多个平台转载去重指纹没拦住现象同一个事件新浪、搜狐、网易都发了报道URL不同标题相似内容大同小异。数据库里出现多行内容几乎一样但url不同的记录占用存储空间下游做检索和推荐时也会把它们当作不同内容。原因scrapy-redis默认的RFPDupeFilter基于request_fingerprint函数这个函数对URL、请求方法、请求体计算SHA1指纹。两个URL只要有一点不同——哪怕只是末尾多了?fromsohu或?_f258——指纹就完全不同去重器认为是两个新请求。这不算去重器失效而是它的粒度不够贴合“新闻去重”需求。解决在写列表页解析时就过滤掉URL里的跟踪参数和来源参数。做法是定制request_fingerprint对查询字符串里的spm、from、from_source、uid等参数做剔除后再计算指纹。这需要替换scrapy-redis的去重器# dupefilter.py import hashlib from urllib.parse import urlparse, parse_qsl, urlencode from scrapy.utils.request import request_fingerprint from scrapy_redis.dupefilter import RFPDupeFilter # 这些参数在不同新闻站点的URL里大量出现属于平台附加的追踪参数 TRACKING_PARAMS frozenset([ spm, from, from_source, uid, source, refer, ]) def news_fingerprint(request): parsed urlparse(request.url) filtered_query [ (k, v) for k, v in parse_qsl(parsed.query) if k not in TRACKING_PARAMS and not k.startswith(utm_) ] clean_url parsed._replace(queryurlencode(filtered_query)).geturl() return request_fingerprint(request.replace(urlclean_url)) class NewsRFPDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): return news_fingerprint(request)然后在settings.py里替换DUPEFILTER_CLASS dupefilter.NewsRFPDupeFilter注意这一层解决的是“同一个URL去掉追踪参数后”的去重它不能解决“URL完全不同但内容一样”的去重比如新浪写了一条完全原创的新闻搜狐改个标题转载。要做到内容级去重需要在process_item阶段对正文做simhash或MD5然后以内容指纹为唯一键入库。在新闻采集系统里这两层都该有URL指纹负责请求级去重内容指纹负责入库级去重。只做一层早晚要补第二层。4.3 Redis里的脏任务把下一个抓取周期堵死现象爬虫运行中途被强制终止或者反爬策略把Spider打得异常退出。第二天启动爬虫日志显示一上来就在抓一堆已经过期的URL而且这些URL大部分返回404或跳转到网站首页。队列被这些无效URL占满新注入的新闻URL迟迟得不到处理。原因SCHEDULER_PERSIST True会把爬虫终止时队列里的请求原样保留。如果终止发生在列表页解析完成、但详情页请求还没消费完的时候队列里堆积的大多是详情页URL这些URL隔了几个小时后要么被新闻站下线要么被反爬策略拦截全部变成了脏任务。解决给队列加时效性不要让请求在队列里躺太久。最简单的方式是启动爬虫前清空上次遗留的待抓取队列保留去重指纹集合# 保留去重结果清空待抓取队列 redis-cli DEL news:requests这里news:requests是scrapy-redis默认的请求队列Key实际名称取决于爬虫的name和队列类型配置可以在Redis里用KEYS *requests*查出来。另一个更稳妥的做法是把“清空队列”写进爬虫启动脚本python -c import redis r redis.Redis.from_url(redis://:yourpassword127.0.0.1:6379/1) for key in r.scan_iter(news:requests:*): r.delete(key) scrapy crawl sina_news注意只清理请求队列不要动news:dupefilter那个集合否则历史抓取过的URL会被重新抓一遍。如果你确实希望整个爬虫周期完全从头开始才需要连同指纹集合一起清空。4.4 动态加载的新闻正文抓不全列表页和详情页分开调度现象部分新闻站点改版后列表页的新闻链接和正文内容全部由JavaScript渲染用普通scrapy.Request抓取时Response里只有页面的HTML骨架新闻链接一个都提取不到Item产出为零。原因scrapy-redis本身不解决渲染问题它只负责调度。新闻站点的反爬策略和前端框架升级后服务端渲染变成了客户端渲染抓取的目标从“服务端返回的HTML”变成了“浏览器执行JS后的DOM”。如果还用Scrapy默认的Downloader直接发HTTP请求拿到的自然不是渲染后的内容。解决常见做法是引入渲染中间件比如scrapy-splash或selenium但这两个方案在分布式环境下都面临新问题——渲染服务需要独立部署且并发能力远低于普通HTTP请求。我实际的思路是分开调度列表页因为需要渲染走一个渲染节点渲染完成后再把详情页URL提取出来注入Redis队列详情页大多是服务端渲染的老式新闻模板直接走普通下载器抓取。这样渲染服务的压力只集中在列表页不会被详情页的并发冲垮。在scrapy-redis里这实现起来很自然# 列表页渲染Spider执行完毕后把详情页URL直接推到Redis而不是交给下载器 r.sadd(news:start_urls:details, detail_url)这里的取舍是渲染服务只做一件事——把列表页的URL解析出来它不抓详情页。详情页的高并发抓取任务交给scrapy-redis的普通调度器Redis队列在这里充当了渲染系统和抓取系统的解耦缓冲区。5. 新闻采集系统的参数调优去重指纹、调度间隔和平台优先级5.1 定制请求指纹spm、from这类跟踪参数是去重失效的第一根源上一章提到新闻站点的URL里充斥着spm、from、uid这类平台附加的跟踪参数。这些参数的共同特点是同一篇新闻在页面上的多个入口URL参数各不相同新闻内容却完全相同。如果不去掉它们去重指纹会把同一篇新闻的各入口版本当作不同URL导致详情页被重复抓取多次。这不仅是浪费带宽更危险的是同一篇新闻被高频率重复请求会触发新闻站点的频率限制甚至封禁IP。定制指纹的正则里有一个普遍规律spm、from这类中文互联网站点特有的跟踪参数可以统一剔除utm_开头的参数谷歌分析标准参数也可以剔除但id、news_id、article_id这类参数不能剔——它们往往是URL里真正标识新闻身份的部分。一个粗略的判断方法是把URL里的查询参数全部去掉后访问是否还能定位到同一篇新闻。如果能说明参数是纯跟踪用途可以安全剔除。这个判断方法不保证100%准确但覆盖了大多数新闻站点的情况。上一章给的NewsRFPDupeFilter在线上运行一段时间后你还会发现一个新问题有些站点即使去掉了跟踪参数URL里还带着一个时间戳参数比如?_t1693441234这个参数每次页面加载都会变。要彻底解决这个还得在去重正则里加上对时间戳参数的识别TRACKING_PARAMS frozenset([ spm, from, from_source, uid, source, refer, _t, timestamp, ])处理好URL指纹之后再做一层内容指纹入库去重才算完整闭环。内容指纹我通常用正文前200个字符的MD5或者simhash的64位值入库时配合UNIQUE KEY防止重复插入。5.2 DOWNLOAD_DELAY和并发量用两个参数控制抓取节奏别让新闻站在日志里记你一笔很多人把DOWNLOAD_DELAY理解成“抓慢一点”这个理解不全面。DOWNLOAD_DELAY是每个请求之间的最小间隔它和CONCURRENT_REQUESTS共同决定抓取速率速率大概等于请求数除以总耗时也就是CONCURRENT_REQUESTS / (平均响应时间 DOWNLOAD_DELAY)。调大并发量而不调DOWNLOAD_DELAY抓取速率可能迅速翻倍快得让新闻站点的安全策略立刻警觉。我常见的配置组合是这样的CONCURRENT_REQUESTS 16 DOWNLOAD_DELAY 0.5 CONCURRENT_REQUESTS_PER_DOMAIN 8CONCURRENT_REQUESTS_PER_DOMAIN这个参数值得单独说。多平台采集系统里有多个站点同时抓取如果不限制单站点的并发单个请求慢的站点会占掉大量并发配额快站反而饿死。设成8意思是一个域名下同时进行的请求最多8个总并发16两台机器各跑一半配额整体对新闻站点来说是一个既不慢也不激进的速度。新闻采集里还有一个小技巧对每个平台分别设置DOWNLOAD_DELAY。Scrapy原生不支持per-domain延迟但可以通过Spider里重写start_requests来实现class SinaNewsSpider(NewsBaseSpider): name sina_news custom_settings { DOWNLOAD_DELAY: 0.3, CONCURRENT_REQUESTS_PER_DOMAIN: 6, }5.3 平台优先级用PriorityQueue插队让突发新闻先被采集新闻资讯场景有一个区别于通用爬虫的需求突发新闻需要优先采集。比如某地发生突发事件澎湃、新浪在几分钟内连发多条报道这些URL如果排在队列尾部等平凡的更新先抓完突发新闻就失去了时效性。解决这个问题的标准方案是启用PriorityQueueSCHEDULER_QUEUE_CLASS scrapy_redis.queue.PriorityQueue启用后往Redis里注入URL时可以带一个优先级分数。scrapy-redis的PriorityQueue基于ZSet分数越小优先级越高。在Spider代码里列表页解析出新闻链接时可以给疑似突发新闻的链接更高的优先级def parse(self, response): for link in self.extract_news_links(response): priority 10 # 标题里含有突发和快讯字样的链接赋予更高优先级 if any(keyword in link.text for keyword in (突发, 快讯, 刚刚)): priority 1 yield scrapy.Request( urljoin(response.url, link), callbackself.parse_detail, prioritypriority, )要确认Scrapy的优先级确实传给了scrapy-redis的队列有个验证方法在高优先级任务注入后用ZRANGE查看Redis里的ZSetredis-cli ZRANGE news:requests 0 10 WITHSCORESScore最小的请求应该就是你刚注入的突发新闻链接。这一招在生产环境里排障很有用它能直观确认调度器有没有按照预期工作。如果没有启用PriorityQueuescrapy.Request(priority10)不会产生任何效果——队列还是FIFO。5.4 持久化和指纹集合的清理策略哪些Redis数据能留哪些不能留SCHEDULER_PERSIST True让去重指纹集合永久保留在Redis里。这个集合越大每次请求去重时判断“是否见过”的开销就越高但更关键的问题是它会让爬虫漏抓——如果一篇新闻的URL在昨天被抓过今天新闻站把这篇新闻重新编辑上线URL没变但内容变化了去重指纹会直接阻止重新抓取。应对新闻站内容更新的办法是给指纹集合加TTL。问题是scrapy-redis的RFPDupeFilter不会自动给指纹键设过期时间需要手动处理。我常用的做法是在每天的任务结束后对指纹集合里超过N天的指纹做清理# 每天凌晨执行清理指定爬虫在Redis中的指纹集合 python -c import redis, time r redis.Redis.from_url(redis://:yourpassword127.0.0.1:6379/1) # 指纹集合的key格式是 爬虫name:dupefilter:哈希值 # 这里以新浪新闻爬虫为例 pattern sina_news:dupefilter:* for key in r.scan_iter(pattern): # 直接删除整个指纹集合让爬虫重新开始去重 # 如果你希望保留一部分历史指纹需要更精细的手段 r.delete(key) 清理的粒度很关键。整个指纹集合连同历史记录全部删掉意味着爬虫会把所有新闻站点的历史链接重新抓一遍这通常不是你想要的。更合理的思路是新闻爬虫只关心最近24小时到72小时内的更新那么指纹集合的保留时间也应该设在这个范围。实际上很多新闻资讯系统直接采用“隔天清空指纹”的策略——因为新闻站点的首页和栏目页权重集中在新内容上旧链接很少再次出现清空指纹带来的重复抓取成本远低于漏抓成本。6. 从测试到部署单机多Spider验证再到多机扩展和每日定时采集6.1 先在单机上跑通两个Spider确认Redis队列和数据库写入正常多机扩展之前先在单机上跑通最小闭环。启动两个平台的Spider观察三个关键指标Redis队列长度是否增长后回落、MySQL里是否出现两个来源的新闻记录、日志里有没有Redis超时或连接异常。队列长度的检查命令是redis-cli LLEN news:requests如果队列先增后减说明消费正常如果只增不减说明Spider的解析逻辑出了问题——大概率是extract_news_links提取不到任何新闻链接或提取到的链接都是无效的。此时不要急着调参数先单独curl一个列表页确认选择器没有失效。6.2 定时采集除了crontab还要盯住队列空转和重复启动新闻采集是持续任务一般用crontab定时拉起爬虫。这里有一个新手常踩的坑用scrapy crawl sina_news拉起爬虫任务结束后进程退出Redis里的队列却还在。下一次crontab拉起时如果上一次的脏队列没有清理新闻站点的更新会被脏URL挤占。我在生产里用的是一个包装脚本#!/bin/bash # crawl_news.sh cd /opt/news_crawler source venv/bin/activate # 启动前清理上次遗留的请求队列保留去重指纹 python -c import redis r redis.Redis.from_url(redis://:yourpassword127.0.0.1:6379/1) for key in r.scan_iter(news:requests:*): r.delete(key) print(queue cleaned) # 后台运行爬虫日志落盘 nohup scrapy crawl sina_news logs/sina.log 21 脚本里的nohup是为了防止crontab环境里进程被SIGHUP信号杀掉。日志落盘后排查问题不用登录机器翻终端输出直接tail日志即可这在多机部署时几乎是必须的。6.3 多机扩展第二台机器怎么接入同一个Redis第二台机器接入时只需要把代码部署过去settings.py里的REDIS_URL指向同一个Redis实例然后直接启动爬虫即可。两台机器从同一个Redis队列里取任务天然不冲突。需要注意的唯一问题是scrapy-redis的指纹集合是全局的去重器基于同一个Redis去重不会因为你加了机器就重复抓取。加机器能提速的前提是Redis实例所在服务器的带宽和连接数足够如果Redis部署在云上注意它的最大连接数限制按CONCURRENT_REQUESTS乘以爬虫实例数估算留出30%的余量。6.4 日常运维还要看什么我在维护这套系统时有一个习惯每天只看两个数字——入库新闻总数和队列积压数。入库总数可以直接查MySQLSELECT source, COUNT(*) FROM news WHERE crawl_time CURDATE() GROUP BY source;队列积压数就查Redis。如果某天某个source的入库数为零几乎可以断定这个平台的页面结构变了去爬虫日志里找解析异常。如果队列积压一直增长说明下游消费速度跟不上生产速度需要加爬虫节点或者降低产出端的并发。这套系统我跑了大半年踩过最深的坑反而是入库那步的数据库锁——多个爬虫并发写同一张MySQL表时InnoDB的行锁竞争会导致写入延迟变高建议把Pipeline的批量提交改为每攒50条一次commit或者直接换MongoDB。希望这些从队列、指纹到入库的细节能帮你在搭建自己的新闻采集系统时少走几步弯路。本文还有配套的精品资源点击获取