简介:本资源是一个基于Scrapy-Redis构建的分布式多平台新闻资讯采集系统,面向Python爬虫开发者、数据采集工程师及大数据初学者,旨在解决单机Scrapy在高并发、跨平台、去重与任务调度方面的瓶颈问题。系统完整集成Redis作为调度中心与去重中间件,支持对多个新闻网站的并行抓取、动态解析、结构化清洗及多后端存储(含MySQL写入脚本),兼顾反爬适配与可扩展架构设计。压缩包共41个文件,以27个Python核心模块(如spiders、pipelines、middlewares、push.py等)为主体,辅以6个XML配置、2个CFG/YML环境配置、Dockerfile与docker-compose.yml容器化部署支持,整体仅32KB,轻量但结构完整。目前已有41人学习下载,提供开箱即用的工程目录、清晰的模块划分(如test.py用于快速验证、process_items.py专注数据处理)、真实可用的headers与存储适配逻辑,是理解分布式爬虫原理与落地实践的优质参考样本。
1. 这不是个“爬虫脚本”,而是一套可落地的新闻资讯数据管道
你搜“scrapy-redis 新闻采集”,刷出来的大多是零散代码片段、过时的教程,或者直接甩个 GitHub 链接让你自己啃。但真正跑起来一个能扛住多平台、不崩、不丢数据、还能随时加节点的新闻采集系统,光靠抄几行start_urls和parse()根本不行。我去年帮一家区域媒体做内容聚合中台,从零搭起这套基于 scrapy-redis 的多平台新闻资讯采集系统,现在每天稳定抓取 37 家主流媒体、21 个地方政务站、8 类垂直行业门户的首页、栏目页和详情页,峰值并发 1200+ 请求,Redis 队列积压始终控制在 300 条以内——这不是理论模型,是实打实压在生产环境跑了一年半的系统。核心关键词就五个:scrapy-redis、新闻资讯采集系统、scrapy.cfg、docker-compose.yml、Dockerfile。它解决的不是“能不能抓到”,而是“能不能持续、可控、可扩展地抓到”。适合三类人:一是想把爬虫从玩具级升级成工程级的 Python 工程师;二是需要对接内容中台、做舆情监控或竞品分析的产品/运营同学;三是技术负责人,要评估这套架构能否放进现有 CI/CD 流水线、是否兼容 MySQL 数据库生态。它不教你怎么写 XPath,而是告诉你:当搜狐首页刷新频率变成每 90 秒一次,当澎湃新闻的反爬策略突然升级 JS 渲染,当本地测试跑得好好的,一上 Docker 就报ConnectionRefusedError: [Errno 111] Connection refused——你该动哪根线、改哪行配置、查哪个日志段。
这套系统真正的价值,不在“采集”本身,而在“管道化”:URL 入队、去重、调度、下载、解析、存储、监控,每个环节都解耦、可替换、可度量。比如你今天用 MySQL 存正文,明天想换 Elasticsearch 做全文检索,只需改pipelines.py里两行代码,不用碰调度器、不用重写中间件。再比如,某家媒体突然加了滑动验证,你只需要在middlewares.py里新增一个SeleniumDownloaderMiddleware,把它插进下载器中间件栈,其他所有站点照常运行。这才是 scrapy-redis 给你的底气——它不是让你写更多代码,而是让你少写重复代码,把精力聚焦在业务逻辑上:怎么定义“有效新闻”?标题含“突发”“快讯”的优先级加权多少?同一事件不同信源的去重粒度设为“标题+发布时间±5分钟”还是“正文前200字哈希”?这些才是真实世界里的决策点,而不是卡在pipelines.py里纠结item['content'] = response.css('div#article p::text').getall()抓不到数据。
2. 整体架构设计:为什么必须用 scrapy-redis,而不是单机 Scrapy?
2.1 单机 Scrapy 的天花板在哪?
先说清楚我们放弃单机 Scrapy 的具体原因。不是它不好,而是新闻采集场景天然踩中它的三个硬伤:
第一,调度器(Scheduler)内存绑定。Scrapy 默认调度器把待爬 URL 存在 Python 的set或deque里,进程一挂,队列全丢。你凌晨三点跑着采集,服务器重启一下,几百个未抓链接就永远消失了。更糟的是,它无法跨进程共享——你想开两个scrapy crawl news_spider实例并行跑?不行,因为两个实例的调度器互不知情,会重复抓同一 URL,或者漏掉某些 URL。这在新闻场景下是致命的:热点事件爆发时,每分钟都有新链接产生,漏抓一条可能就是漏掉关键信源。
第二,去重(DupeFilter)不可持久化。默认RFPDupeFilter把指纹存在内存里,重启即清空。这意味着系统重启后,昨天刚抓过的人民网首页,今天又会当作新 URL 再抓一遍。新闻站首页更新频繁,但内容主体变化不大,反复抓不仅浪费带宽和 IP,更会导致下游存储层写入大量重复数据,清洗成本飙升。
第三,扩展性为零。想提升吞吐量?只能堆机器,但每台机器都要独立维护一套 Scrapy 环境、独立配置代理池、独立管理 Cookies。运维复杂度呈指数增长。我们试过用scrapyd部署多个单机实例,结果发现:当某个媒体反爬变严,所有实例的请求都被限速,而你根本不知道是哪个实例触发了风控,排查像大海捞针。
2.2 scrapy-redis 如何破局:把调度器和去重器“搬出进程”
scrapy-redis 的核心思想非常朴素:把 Scrapy 最脆弱的两个组件——调度器和去重器——从 Python 进程里剥离出来,放到 Redis 这个外部、持久化、支持多客户端共享的存储里。它不是重写 Scrapy,而是给 Scrapy “装上 Redis 插件”。
具体怎么装?看这张简化的数据流图(文字描述):
[新闻源列表] → [Spider 生成初始URL] → [Redis DUPEFILTER 检查指纹] → [若未重复,存入 Redis SCHEDULER QUEUE] → [多个 Scrapy Worker 同时监听该 Queue] → [Worker 取出 URL → 下载 → 解析 → Pipeline 存 MySQL] → [Pipeline 成功后,通知 Redis 更新状态]这里的关键跃迁有三点:
调度器 Redis 化:所有 Spider 生成的 URL,不再塞进本地内存队列,而是统一推到 Redis 的 List 或 Sorted Set(我们用
zset,便于按优先级排序)。任何数量的 Worker(Scrapy 进程)都可以BRPOP或ZPOPMIN从同一个队列取任务。Worker 挂了?没关系,URL 还在 Redis 里,其他 Worker 接着干。这就是真正的“任务队列高可用”。去重器 Redis 化:
RFPDupeFilter被替换为RedisDupeFilter,它计算 URL 指纹(默认是scrapy.utils.request.request_fingerprint)后,存到 Redis 的set结构里。所有 Worker 共享同一个set,自然实现全局去重。而且 Redis 的set是持久化的(开启 AOF 或 RDB),重启不丢数据。Request 对象序列化:Scrapy 的
Request对象不能直接存 Redis,scrapy-redis 提供了pickle序列化方案(默认)或json方案(需自定义)。我们选pickle,因为它能完整保留Request的meta、cookies、headers等所有属性,这对需要携带登录态或 Referer 的新闻站至关重要。
提示:不要迷信“JSON 更轻量”。新闻采集常需传递复杂上下文,比如
meta={'source': 'people', 'category': 'politics', 'priority': 10},pickle一行搞定,json得自己写default函数处理datetime、bytes等类型,反而增加出错概率。实测下来,pickle在千级 QPS 下序列化开销可忽略。
2.3 为什么选择 Redis 而不是 Kafka 或 RabbitMQ?
有人问:消息队列不是更专业吗?Kafka 吞吐无敌,RabbitMQ 可靠性高。但在新闻采集场景,Redis 是更优解,理由很实在:
延迟极低:Redis 是内存数据库,
LPUSH+BRPOP的 P99 延迟在 0.5ms 以内。Kafka 单次写入延迟通常在 5~20ms,对毫秒级响应的新闻抓取来说,积压风险更高。我们曾用 Kafka 替代 Redis 做调度,结果在突发流量下,Worker 拿到 URL 的平均延迟从 1.2ms 涨到 18ms,导致部分时效性强的快讯抓取超时。运维简单:一个
redis-server进程,配好maxmemory和maxmemory-policy(我们用allkeys-lru),基本不用管。Kafka 需要 ZooKeeper、Broker 集群、Topic 分区管理,运维成本翻倍。对中小团队,省下的运维时间够你多优化两个解析规则。功能刚好够用:新闻采集不需要 Kafka 的分区重平衡、精确一次语义(Exactly-Once)。我们需要的是:URL 不丢、不重复、能按优先级取、能快速查看队列长度。Redis 的
List/ZSet/Set原语完美覆盖。ZSet的score字段天然支持动态优先级——比如把“突发”类新闻的score设为当前时间戳的负值,保证最新事件永远排在队首。与 Scrapy 生态无缝集成:scrapy-redis 本身就是为 Redis 量身定制,文档、社区、坑都明明白白。换成 Kafka,得自己写
KafkaScheduler、KafkaDupeFilter,调试成本远高于收益。
3. 核心细节解析:从 scrapy.cfg 到 docker-compose.yml 的每一行都在解决什么问题
3.1 scrapy.cfg:不只是配置文件,它是部署契约
scrapy.cfg常被当成启动参数的集合,但它实际是 Scrapy 项目的“部署契约”,定义了项目如何被scrapyd或scrapyCLI 识别和加载。我们的scrapy.cfg长这样:
[settings] default = news_spider.settings [deploy] # scrapyd 部署用,但我们不用 scrapyd,所以注释掉 # project = news_spider [deploy:prod] # 指向生产环境的 scrapyd server,同样不用 # url = http://prod-scrapyd:6800/ # username = user # password = pass # 关键:自定义命令入口 [commands] crawl = news_spider.commands.crawl_command:CrawlCommand重点在最后三行。我们没用scrapyd,而是自己写了CrawlCommand,原因很现实:scrapy crawl spider_name启动时,会加载settings.py,但settings.py里REDIS_URL等敏感配置如果写死,就无法适配不同环境(开发/测试/生产)。CrawlCommand让我们能在启动时动态注入配置:
# news_spider/commands/crawl_command.py from scrapy.cmdline import execute import os import sys class CrawlCommand: def run(self, args, opts): # 从环境变量读取配置,而非 settings.py 硬编码 os.environ.setdefault('REDIS_URL', 'redis://redis:6379/0') os.environ.setdefault('MYSQL_URL', 'mysql+pymysql://user:pass@mysql:3306/news_db') os.environ.setdefault('SCRAPY_SETTINGS_MODULE', 'news_spider.settings') # 构建 scrapy 命令 cmd = ['scrapy', 'crawl', args[0]] + args[1:] execute(cmd)这样,启动命令就变成python -m scrapy crawl people_spider --set REDIS_URL=redis://prod-redis:6379/1,完全绕过settings.py的硬编码。scrapy.cfg里的[commands]就是告诉 Scrapy:“当你看到scrapy crawl,请调用我写的这个类,而不是默认逻辑。”
注意:
scrapy.cfg必须放在项目根目录,且文件名不能改。Scrapy 启动时会向上遍历目录找它,找不到就报Not a Scrapy project。我们吃过亏:有次把项目打包进 Docker,忘了把scrapy.cfg放进WORKDIR,容器启动直接失败,日志只显示ImportError: No module named 'news_spider',查了两小时才发现是scrapy.cfg缺失。
3.2 settings.py:scrapy-redis 的灵魂开关
settings.py是整个系统的“神经中枢”,80% 的稳定性问题都源于这里配置错误。我们只列出最关键的 12 行,并解释每行背后的战场经验:
# 1. 启用 scrapy-redis 调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 2. 启用 scrapy-redis 去重器 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 3. 持久化开关:True=Redis队列不自动清空,False=爬完就删(慎用!) SCHEDULER_PERSIST = True # 4. Redis连接URL,必须指向Docker网络内的服务名 REDIS_URL = os.getenv('REDIS_URL', 'redis://redis:6379/0') # 5. 调度队列类型:ZSet支持优先级,List是FIFO SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue' # 6. 并发数:不是越大越好,要匹配目标网站承受力 CONCURRENT_REQUESTS = 32 # 7. 下载延迟:新闻站首页通常允许1s,详情页可设0.5s DOWNLOAD_DELAY = 1 # 8. 自动限速:根据响应时间动态调整,并发数,比固定DELAY更智能 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 1 AUTOTHROTTLE_MAX_DELAY = 3 # 9. User-Agent轮换:避免被识别为爬虫 DOWNLOADER_MIDDLEWARES = { 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, 'news_spider.middlewares.RandomUserAgentMiddleware': 400, } # 10. 代理中间件:新闻站对IP要求严,必须用 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 90, 'news_spider.middlewares.ProxyMiddleware': 100, # 11. Pipeline链:先去重清洗,再存MySQL,最后发消息 ITEM_PIPELINES = { 'news_spider.pipelines.DeduplicatePipeline': 200, 'news_spider.pipelines.MySQLPipeline': 300, 'news_spider.pipelines.KafkaPipeline': 400, # 可选 } # 12. 日志级别:生产环境用INFO,DEBUG只在本地调试开 LOG_LEVEL = 'INFO'逐条拆解:
SCHEDULER_PERSIST = True是生命线。设为False,爬虫结束时 Redis 队列自动清空。线上环境绝对禁止!我们曾因误设此值,导致一次全站重抓,MySQL 写入压力暴增,主库 CPU 100%,差点引发雪崩。SCHEDULER_QUEUE_CLASS选SpiderPriorityQueue(基于 ZSet),不是FifoQueue。新闻采集必须优先级:人民网突发新闻 > 地方日报常规报道 > 行业论坛转载帖。我们在start_requests()里给Request加priority参数:yield scrapy.Request(url, priority=100),priority值越大,越早被取出。CONCURRENT_REQUESTS = 32是经过压测的平衡点。设太高,目标站返回 429;设太低,吞吐跟不上。我们用ab工具对目标站做压力测试:ab -n 1000 -c 50 http://example.com/,观察响应时间拐点。32 是多数新闻站的甜蜜点。AUTOTHROTTLE_ENABLED = True比DOWNLOAD_DELAY更可靠。它根据Response.time动态调整并发,遇到慢响应自动降并发,快响应自动提并发。START_DELAY和MAX_DELAY控制调整范围,避免抖动过大。DOWNLOADER_MIDDLEWARES的顺序是关键。ProxyMiddleware必须在RetryMiddleware之后(序号 100 > 90),否则重试时不会走代理,IP 被封风险大增。
3.3 Dockerfile:不是“把代码扔进容器”,而是构建可复现的运行时环境
Dockerfile的使命是:让任何人在任何机器上,执行docker build,得到和你生产环境一模一样的 Python 运行时。我们的Dockerfile没用FROM python:3.9-slim,而是选FROM continuumio/miniconda3:4.12.0,原因有三:
依赖隔离强:Conda 的
environment.yml比requirements.txt更精准控制包版本,尤其对lxml、cryptography这类 C 扩展,pip install常因系统库版本不匹配编译失败,Conda 直接装预编译二进制。科学计算友好:后续可能加 NLP 清洗(如标题分类、情感分析),Conda 的
pytorch、transformers一键安装,pip得折腾 CUDA 版本。镜像体积可控:
miniconda3基础镜像 350MB,比python:3.9-slim(120MB)稍大,但省去apt-get install build-essential libxml2-dev libxslt-dev等编译依赖,最终镜像大小反而小 15%。
FROM continuumio/miniconda3:4.12.0 # 设置工作目录 WORKDIR /app # 复制环境文件,先装依赖(利用Docker layer cache) COPY environment.yml . RUN conda env create -f environment.yml && \ conda clean --all -f -y # 激活环境 SHELL ["conda", "run", "-n", "news_env", "bash", "-c"] # 复制代码 COPY . . # 创建非root用户(安全强制要求) RUN useradd -m -u 1001 -G root -d /home/newsuser newsuser && \ chown -R newsuser:root /app && \ chmod -R 775 /app USER newsuser # 暴露端口(Scrapy本身不占端口,但健康检查需要) EXPOSE 6800 # 启动命令 CMD ["conda", "run", "-n", "news_env", "python", "-m", "scrapy", "crawl", "news_spider"]关键细节:
environment.yml显式声明scrapy-redis==0.7.2(不是>=0.7.0),因为 0.7.3 有个 Redis 连接池 bug,会导致高并发下ConnectionResetError。我们踩过这个坑,回滚到 0.7.2 后稳定运行。USER newsuser强制非 root 运行。Docker 安全最佳实践,避免容器内提权攻击。chown确保用户有/app目录权限。CMD用conda run而不是python,确保激活news_env环境。python命令可能调用系统 Python,而非 Conda 环境。
3.4 docker-compose.yml:定义服务间的“外交关系”
docker-compose.yml不是简单的服务列表,它是定义redis、mysql、scrapy-worker之间网络、依赖、健康检查的“外交协议”。我们的版本:
version: '3.8' services: # Redis:调度中心 redis: image: redis:7.2-alpine container_name: news-redis restart: always command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru ports: - "6379:6379" volumes: - ./redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 # MySQL:存储中心 mysql: image: mysql:8.0-oracle container_name: news-mysql restart: always environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: news_db MYSQL_USER: news_user MYSQL_PASSWORD: news_pass ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"] interval: 10s timeout: 5s retries: 5 depends_on: redis: condition: service_healthy # Scrapy Worker:采集引擎 worker: build: . container_name: news-worker restart: always environment: - REDIS_URL=redis://redis:6379/0 - MYSQL_URL=mysql+pymysql://news_user:news_pass@mysql:3306/news_db - SCRAPY_SETTINGS_MODULE=news_spider.settings volumes: - ./logs:/app/logs depends_on: redis: condition: service_healthy mysql: condition: service_healthy # 关键:健康检查,确保Worker能连通Redis和MySQL healthcheck: test: ["CMD", "python", "-c", "import redis; r=redis.Redis(host='redis', port=6379); r.ping(); import pymysql; pymysql.connect(host='mysql', user='news_user', password='news_pass', database='news_db')"] interval: 30s timeout: 10s retries: 3核心设计点:
healthcheck是灵魂。redis和mysql的健康检查确保它们启动完成才启动worker;worker的健康检查则验证它能否连通两个依赖服务。没有它,worker可能因 Redis 未就绪而启动失败,Docker 会不断重启,日志刷屏。depends_on的condition: service_healthy比condition: service_started严格得多。后者只等容器启动,前者等健康检查通过。我们曾用started,结果worker启动时mysql还在初始化,报pymysql.err.OperationalError: (1045, "Access denied"),查了半小时才发现是依赖条件太松。volumes映射./logs:/app/logs,让日志落到宿主机,方便docker logs news-worker查看,也方便 ELK 收集。command里--appendonly yes开启 AOF 持久化,--maxmemory 2gb限制内存,--maxmemory-policy allkeys-lru防止 OOM。新闻采集队列可能堆积,必须防止单个 Redis 实例吃光内存。
4. 实操过程:从本地调试到生产部署的七步通关
4.1 第一步:本地最小闭环验证(5分钟)
别急着写 Spider,先验证 scrapy-redis 能否跑通。创建最简test_spider.py:
import scrapy from scrapy_redis.spiders import RedisSpider class TestSpider(RedisSpider): name = 'test_spider' def parse(self, response): self.logger.info(f"Success! Status: {response.status}, URL: {response.url}") yield {'url': response.url, 'status': response.status}然后,在本地启动 Redis(redis-server),执行:
# 1. 启动Scrapy Worker,监听Redis队列 scrapy crawl test_spider # 2. 另开终端,向Redis队列推一个测试URL echo "http://httpbin.org/get" | redis-cli -x lpush test_spider:start_urls如果worker终端打印Success! Status: 200...,说明 scrapy-redis 基础链路通了。这步必须做,90% 的线上问题都能在本地复现。
实操心得:
lpush命令里的test_spider:start_urls必须和 Spider 的name一致。我们曾把name写成testspider(少下划线),队列名变成testspider:start_urls,worker一直空转,查日志全是DEBUG: Crawled 0 pages,折腾半天才发现队列名不匹配。
4.2 第二步:编写新闻 Spider,聚焦“可维护性”
新闻站结构千差万别,但 Spider 设计有通用模式。以“人民日报”为例,我们不写死 XPath,而是用Selector+css方法链:
class PeopleSpider(scrapy.Spider): name = 'people' allowed_domains = ['people.cn'] def start_requests(self): # 首页新闻列表 yield scrapy.Request( 'http://www.people.cn/', callback=self.parse_index, meta={'source': 'people', 'category': 'index'} ) def parse_index(self, response): # 提取所有新闻链接,带优先级 for href in response.css('div#main-content a::attr(href)').getall(): if href.startswith('http'): # 突发新闻优先级+50 priority = 50 if '突发' in response.css('a::text').get() else 0 yield scrapy.Request( href, callback=self.parse_article, meta={'source': 'people', 'category': 'article', 'priority': priority} ) def parse_article(self, response): item = {} item['title'] = response.css('h1::text').get(default='').strip() item['content'] = '\n'.join(response.css('div#p_content p::text').getall()).strip() item['publish_time'] = response.css('meta[name="publishdate"]::attr(content)').get() item['url'] = response.url item['source'] = response.meta['source'] yield item关键设计:
meta传递上下文:source、category、priority,让 Pipeline 能做差异化处理。callback明确分离:parse_index只负责找链接,parse_article只负责提正文,职责单一,改一个站不影响其他。get(default='')避免NoneType错误,新闻站 HTML 结构常有变动,容错必须强。
4.3 第三步:Pipeline 实现去重与存储(MySQL)
pipelines.py是数据落库前的最后一道闸门。我们的DeduplicatePipeline不是简单if item in seen,而是用 MySQL 的INSERT IGNORE:
class DeduplicatePipeline: def __init__(self, mysql_url): self.mysql_url = mysql_url @classmethod def from_crawler(cls, crawler): return cls(crawler.settings.get('MYSQL_URL')) def open_spider(self, spider): # 连接MySQL,用连接池 self.engine = create_engine(self.mysql_url, pool_pre_ping=True) self.Session = sessionmaker(bind=self.engine) def process_item(self, item, spider): session = self.Session() try: # 基于URL和标题哈希去重 url_hash = hashlib.md5(item['url'].encode()).hexdigest() title_hash = hashlib.md5(item['title'].encode()).hexdigest() # INSERT IGNORE,冲突则跳过 sql = """ INSERT IGNORE INTO news_articles (url_hash, title_hash, title, content, publish_time, source, url) VALUES (:url_hash, :title_hash, :title, :content, :publish_time, :source, :url) """ session.execute(text(sql), { 'url_hash': url_hash, 'title_hash': title_hash, 'title': item['title'], 'content': item['content'], 'publish_time': item['publish_time'], 'source': item['source'], 'url': item['url'] }) session.commit() except Exception as e: session.rollback() spider.logger.error(f"Dedup error: {e}") finally: session.close() return item为什么用INSERT IGNORE而不是先SELECT再INSERT?因为高并发下,两次查询间可能有其他 Worker 插入同一条,导致重复。INSERT IGNORE是原子操作,MySQL 层面保证唯一性。
4.4 第四步:Docker 构建与本地测试
在项目根目录执行:
# 构建镜像 docker build -t news-worker . # 启动全套服务(后台) docker-compose up -d # 查看worker日志 docker logs -f news-worker # 手动推一个测试URL到Redis echo "http://www.people.cn/" | docker exec -i news-redis redis-cli -x lpush people:start_urls如果日志出现Crawled (200)和Scraped from <200 http://www.people.cn/>,说明 Docker 环境跑通。此时docker ps应看到news-redis、news-mysql、news-worker三个容器都在Up状态。
注意:
docker-compose.yml里worker的environment必须用redis和mysql作为 host 名,这是 Docker 内置 DNS 解析的 service name,不是localhost。本地测试时,localhost指向宿主机,而容器内localhost指向自己,必须用 service name。
4.5 第五步:生产环境部署(阿里云 ECS)
生产环境不用docker-compose up,而是用docker stack deploy(Swarm 模式)或 Kubernetes。我们用 Swarm,因为轻量:
# 初始化Swarm docker swarm init # 创建 overlay 网络 docker network create --driver overlay --attachable news-net # 部署stack docker stack deploy -c docker-compose-prod.yml newsdocker-compose-prod.yml和本地版区别在于:
redis和mysql改为外部云服务(阿里云 Redis、RDS),worker只连它们的内网地址。worker增加deploy配置:deploy: replicas: 3 resources: limits: memory: 2G cpus: '1.0' restart_policy: condition: on-failure delay: 5s max_attempts: 3- 移除
volumes,日志用logging驱动发到阿里云 SLS。
这样,3 个worker实例自动负载均衡,一个挂了,Swarm 30 秒内拉起新实例。
4.6 第六步:监控与告警(Prometheus + Grafana)
没有监控的采集系统等于裸奔。我们在worker容器里加 Prometheus Exporter:
# 在settings.py里加 EXTENSIONS = { 'scrapy_prometheus.PrometheusMetrics': 100, } # docker-compose.yml里暴露/metrics端口 worker: ports: - "9000:9000" # Prometheus抓取端口Grafana 看板监控 5 个黄金指标:
| 指标 | 查询语句 | 告警阈值 | 说明 |
|---|---|---|---|
| 队列积压 | redis_queue_length{queue="people:start_urls"} | > 5000 | 说明Worker处理不过来,需扩容 |
| 抓取成功率 | rate(scrapy_response_status_count{status="200"}[1h]) / rate(scrapy_response_status_count[1h]) | < 0.95 | 网络或反爬问题 |
| MySQL写入延迟 | mysql_up{job="mysql"} == 0 | 1 | MySQL宕机 |
| Redis内存使用率 | redis_memory_used_bytes{instance="redis:6379"} / redis_memory_max_bytes{instance="redis:6379"} | > 0.85 | 需清理或扩容 |
| Worker存活数 | count(container_state{state="running", name=~"news-worker.*"}) | < 3 | Swarm调度异常 |
4.7 第七步:日常运维:扩缩容与故障恢复
- 扩容 Worker:
docker service scale news_worker=5,Swarm 自动分配任务。 - 清理 Redis 队列:
docker exec news-redis redis-cli DEL people:start_urls(慎用!先确认无重要任务)。 - 故障恢复:若
worker全挂,先docker service logs news_worker查错;常见是 MySQL 连接超时,执行docker service update --env-add MYSQL_URL=mysql://new-host:3306/news_db news_worker动态更新配置。
实操心得:我们给每个 Spider 配置独立队列(
people:start_urls,xinhua:start_urls),这样停掉某个站的采集,只DEL对应队列,不影响其他。曾经澎湃站点升级反爬,我们DEL pengpai:start_urls,修好后再推新 URL,全程其他站无感知。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
worker启动后立即退出,日志无输出 | Dockerfile中USER权限不足 | docker exec -it news-worker ls -l /app | chown -R newsuser:root /app,确保用户有读权限 |
redis-cli lrange people:start_urls 0 -1返回空,但worker日志显示Crawled 0 pages | SCHEDULER_QUEUE_CLASS配置错误,或name不匹配 | docker exec news-redis redis-cli keys "*" | 检查keys输出 |
本文还有配套的精品资源,点击获取