ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Scrapy分布式爬虫实战:全国历史天气数据采集与断点续爬

Scrapy分布式爬虫实战:全国历史天气数据采集与断点续爬

简介:这份资源是面向Python爬虫学习者与毕业设计选题学生的Scrapy分布式爬虫实战项目,聚焦全国历史天气数据的大规模抓取场景。项目基于Twisted异步网络库,涵盖Spiders、Item Pipeline、Downloader Middleware等核心组件,并借助Scrapy Cluster或ScrapyRT等扩展实现多机任务分发,以提升抓取速度与稳定性;数据经清洗、验证后存入MySQL、MongoDB或CSV、JSON等存储介质,涉及日期、温度、湿度、风向风力等字段的解析。压缩包共9个文件,以7个py源码为主,辅以1个md说明文档和1个gitignore配置,整体约8KB,已在Windows 10/11环境完成测试。资源内含爬取示例图片与部署教程,可帮助读者理解分布式爬虫的架构设计、数据存储结构与本地部署流程,适合作为爬虫技能进阶与毕业设计参考。目前已有232人学习。

1. 从一份全国历史天气数据包说起:它到底能跑出什么结果

如果你正在做毕业设计,选题又恰好落在“分布式爬虫”这个方向,大概率会遇到一个尴尬:单机 Scrapy 跑个几百页还行,一旦要覆盖全国几百个城市、几十年的历史天气,队列、去重、限速、断点续爬全都会变成拦路虎。这份scrapy分布式爬虫爬取全国历史天气.zip就是冲着这个场景来的——它把 Scrapy 的分布式改造、天气站点解析、数据落库这几件事打包成了一个能直接跑的项目骨架。

它解决的不是“Scrapy 怎么入门”这种问题,而是“怎么让多个爬虫节点同时干活、还不互相踩脚”的工程问题。适合两类人:一类是毕业设计需要完整可演示系统的同学,另一类是想把单机爬虫升级成分布式、但不想从零搭调度中心的一线开发者。下面我按“先看懂结构、再动手跑通、最后避开坑”的顺序拆一遍,中间会给出关键配置和代码片段,你照着改参数就能复现。

2. 分布式爬虫的骨架:Scrapy-Redis 队列与去重是怎么接上的

2.1 为什么单机 Scrapy 撑不住全国天气这种量级

全国历史天气的数据源通常是按城市、按月组织的页面,一个城市一年 12 个月,300 个城市 10 年就是 36000 个请求起步。单机 Scrapy 的默认调度器把请求队列放在内存里,去重指纹也存在内存的set中,进程一挂,队列全丢,重启就得从头再来。更麻烦的是,单机只能靠CONCURRENT_REQUESTS调并发,调到 32 以上对目标站点压力大,调低了又跑不完。

分布式要解决的核心就两件事:请求队列共享和去重集合共享。Scrapy-Redis 的做法是把这两样都挪到 Redis 里,每个爬虫节点从同一个 Redis 列表里取请求,取走就没了,天然实现任务分发;去重指纹写进 Redis 的 set,所有节点查同一份,不会重复抓同一个 URL。

常见做法是:一台机器只跑 Redis 和调度,另外几台跑 Scrapy worker,worker 数量按你手头机器算。我一般会先用两台 worker 验证队列分发是否正常,再往上加。

2.2 改造 Scrapy 项目的四个关键改动点

把普通 Scrapy 项目改成分布式,不需要重写爬虫逻辑,改的是调度器和去重类。下面这段是settings.py里必须动的部分:

# settings.py # 1. 启用 Scrapy-Redis 调度器,替换默认的内存调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 2. 去重类换成 Redis 版,所有节点共享同一份指纹集合 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 3. 队列是否持久化:True 表示爬完不清空,方便断点续爬 SCHEDULER_PERSIST = True # 4. 请求队列的排序方式,按优先级排队 SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.PriorityQueue" # Redis 连接信息,worker 节点填调度机的内网 IP REDIS_URL = "redis://192.168.1.10:6379" # 分布式下建议关掉自带的去重中间件,避免双重过滤 DUPEFILTER_DEBUG = False

逻辑说明:SCHEDULER和DUPEFILTER_CLASS是分布式改造的开关,不换这两个,多个节点各跑各的,等于白搭。SCHEDULER_PERSIST=True是毕业设计演示时的后悔药——中途 Ctrl+C 或者机器重启,队列还在 Redis 里,重新启动 worker 会接着爬。REDIS_URL里的 IP 必须是所有 worker 都能访问到的地址,本机测试写127.0.0.1,多机就写调度机内网 IP。

参数怎么改:PriorityQueue适合按城市优先级抓取;如果你不关心顺序,换成scrapy_redis.queue.FifoQueue更省内存。SCHEDULER_PERSIST在正式跑数据时建议设False,避免 Redis 里堆积几十万条历史请求把内存吃满。

2.3 爬虫类里怎么把起始 URL 交给 Redis

普通 Scrapy 用start_urls列表,分布式下要改成从 Redis 键读取,这样你才能动态往队列里塞城市列表。改造点在爬虫类的start_requests:

# weather_spider.py import scrapy from scrapy_redis.spiders import RedisSpider class WeatherSpider(RedisSpider): # Redis 里存放起始 URL 的键名,所有 worker 监听同一个键 redis_key = "weather:start_urls" name = "weather" def parse(self, response): # 解析页面中的月份链接,交给调度器继续排队 for month_link in response.css("a.month-link::attr(href)").getall(): yield response.follow(month_link, callback=self.parse_month) def parse_month(self, response): # 逐行提取日期、最高温、最低温、天气状况 for row in response.css("table.history-table tr"): yield { "city": response.meta.get("city"), "date": row.css("td.date::text").get(), "high": row.css("td.high::text").get(), "low": row.css("td.low::text").get(), "weather": row.css("td.weather::text").get(), }

逻辑说明:继承RedisSpider后,爬虫启动时不会立刻发请求,而是阻塞等待weather:start_urls这个键有数据。你可以在任意一台机器上用redis-cli lpush weather:start_urls "https://example.com/city/beijing"推入起始地址,所有 worker 会竞争消费。parse_month里的字段名和 CSS 选择器要按你实际抓的站点结构调整,这里给的是通用写法。

参数说明:redis_key的命名建议带项目前缀,避免和别的爬虫冲突。response.follow会自动补全相对 URL,比手写urljoin省事。如果目标页面是 JS 渲染的,CSS 选择器拿不到数据,那就得换 Playwright 或 Selenium 中间件,这是另一个话题,后面避坑章节会提。

3. 从零跑通:环境、Redis 配置与多节点启动顺序

3.1 依赖安装与 Redis 服务端配置

拿到压缩包后,先看requirements.txt,通常包含scrapy、scrapy-redis、redis、pymysql或pymongo。安装命令:

# 建议在虚拟环境里装,避免和系统包冲突 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt # 如果 requirements 里没锁版本,手动补一个兼容组合 pip install scrapy==2.11.0 scrapy-redis==0.7.3 redis==5.0.1

Redis 服务端的配置重点改两处:bind和protected-mode。多机部署时,调度机的redis.conf里把bind 127.0.0.1改成bind 0.0.0.0,同时设protected-mode no,否则 worker 连不上。改完重启 Redis:

redis-server /etc/redis/redis.conf redis-cli ping # 返回 PONG 说明服务正常

逻辑说明:protected-mode no只在可信内网环境用,公网机器千万别这么设,否则 Redis 裸奔会被扫。毕业设计一般在本机或实验室内网跑,问题不大。redis-cli ping是最快的连通性检查,连不上先看防火墙和 bind 地址。

3.2 多节点启动顺序与验证队列分发

启动顺序有讲究:先起 Redis,再起任意多个 worker,最后推起始 URL。顺序反了,worker 会空等,你以为是代码问题,其实是队列里没数据。

# 在调度机上启动 Redis 后,分别在每台 worker 上执行 scrapy crawl weather # 此时 worker 会阻塞,等待 redis_key 有数据 # 在任意一台能连 Redis 的机器上推入起始 URL redis-cli lpush weather:start_urls "https://example.com/city/beijing" redis-cli lpush weather:start_urls "https://example.com/city/shanghai"

验证分发是否正常,看 Redis 里的队列长度和去重集合大小:

redis-cli llen weather:requests # 待处理请求数 redis-cli scard weather:dupefilter # 已去重指纹数 redis-cli llen weather:start_urls # 起始队列剩余

逻辑说明:如果weather:requests一直为 0 而dupefilter在涨,说明请求被消费完了,正常。如果多个 worker 的日志里出现相同的 URL 被抓,检查DUPEFILTER_CLASS是否真的换成了 Redis 版。scard的数字应该约等于已发请求数,偏差太大说明有重复入队。

3.3 数据落库:MySQL 与 MongoDB 两种接法

天气数据字段固定,用 MySQL 存方便做统计查询。在pipelines.py里写一个管道:

# pipelines.py import pymysql class WeatherPipeline: def open_spider(self, spider): # 爬虫启动时建连接,避免每条数据都重连 self.conn = pymysql.connect( host="127.0.0.1", user="root", password="yourpass", database="weather", charset="utf8mb4" ) self.cursor = self.conn.cursor() def process_item(self, item, spider): sql = """INSERT INTO history_weather (city, date, high, low, weather) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE high=VALUES(high)""" self.cursor.execute(sql, ( item["city"], item["date"], item["high"], item["low"], item["weather"] )) self.conn.commit() return item def close_spider(self, spider): self.cursor.close() self.conn.close()

逻辑说明:ON DUPLICATE KEY UPDATE要求(city, date)建唯一索引,这样重复抓到的同一天数据不会报错,只会更新。open_spider里建连接是标准做法,别在process_item里反复connect,分布式下并发一高连接数会爆。如果偏好文档型存储,把管道换成pymongo的insert_one加upsert=True即可,字段结构不用改。

参数说明:charset="utf8mb4"是为了兼容天气描述里可能出现的特殊符号。commit每条一次在高速抓取下会成为瓶颈,可以改成批量提交,但毕业设计量级下没必要过度优化。

4. 避坑与排查:分布式爬虫最容易翻车的五个地方

4.1 现象:多个 worker 抓到完全相同的 URL

原因:DUPEFILTER_CLASS没改,或者改成了 Redis 版但REDIS_URL指向了不同的 Redis 实例。每个 worker 连自己的本地 Redis,去重集合不共享,自然各抓各的。

解决:确认所有 worker 的REDIS_URL指向同一个 IP 和端口,用redis-cli monitor观察是否有多个客户端在写同一个dupefilter键。如果用了 Docker,注意容器网络里的127.0.0.1是容器自己,要写宿主机 IP 或服务名。

4.2 现象:爬虫启动后一直不动,日志停在 “Waiting for start urls”

原因:redis_key对应的列表为空,或者 worker 连的 Redis 里根本没有这个键。常见于先起了 worker 但忘了推起始 URL,或者推到了错误的数据库编号(Redis 默认 16 个库,REDIS_URL没指定/0可能连到别的库)。

解决:redis-cli -n 0 llen weather:start_urls确认键在哪个库。REDIS_URL写成redis://IP:6379/0明确指定库号。推 URL 时用redis-cli -n 0 lpush保持一致。

4.3 现象:请求队列越积越多,但 worker 不消费

原因:SCHEDULER_QUEUE_CLASS配成了PriorityQueue,而推入的请求没有优先级字段,Scrapy-Redis 在排序时卡住。或者 worker 的CONCURRENT_REQUESTS设成了 0。

解决:检查settings.py里CONCURRENT_REQUESTS是否大于 0,默认 16 就够。如果不需要优先级,把队列类换成FifoQueue或LifoQueue,避免排序开销。

4.4 现象:目标页面返回的数据全是空值

原因:天气站点改版,CSS 选择器失效;或者页面是 JS 动态渲染,Scrapy 拿到的 HTML 里根本没有表格数据。热词里提到的scrapy playwright 动态 iframe就是这类问题的典型场景。

解决:先用scrapy shell "URL"手动测选择器,response.css("table.history-table")返回空列表就说明选择器不对。如果是动态渲染,接入scrapy-playwright,在DOWNLOAD_HANDLERS里注册,并在请求的meta里加"playwright": True。注意 Playwright 会显著降低抓取速度,只在必要页面开启。

4.5 现象:Redis 内存持续上涨,最后 OOM

原因:SCHEDULER_PERSIST=True且去重集合从不清理,几十万条指纹一直堆着。或者请求队列里的 URL 被反复推入,列表无限增长。

解决:正式跑数据时设SCHEDULER_PERSIST=False,爬完自动清空。定期用redis-cli scard weather:dupefilter监控集合大小,超过预期就手动del掉重建。给 Redis 设maxmemory和maxmemory-policy allkeys-lru作为兜底,但别依赖它,主动清理更稳。

5. 进阶技巧:用 Redis 做断点续爬与增量更新

分布式爬虫跑长任务,最怕中途断掉重来。Scrapy-Redis 的SCHEDULER_PERSIST=True只是保住了队列,但去重指纹也在,重启后已抓过的 URL 不会重抓,这是断点续爬的基础。真正实用的是增量更新:只抓新增月份的数据,而不是每次全量重跑。

做法是在推起始 URL 之前,先查数据库里每个城市已抓到的最大日期,只把该日期之后的月份链接推入 Redis。下面这段脚本可以放在项目根目录,跑一次就更新队列:

# incremental_push.py import pymysql import redis # 连数据库查每个城市的最新日期 conn = pymysql.connect(host="127.0.0.1", user="root", password="yourpass", database="weather") cursor = conn.cursor() cursor.execute("SELECT city, MAX(date) FROM history_weather GROUP BY city") latest = dict(cursor.fetchall()) # 连 Redis,推入需要补抓的月份 URL r = redis.Redis(host="192.168.1.10", port=6379, db=0) for city, last_date in latest.items(): # 根据 last_date 计算下一个月的 URL,这里用占位函数示意 next_url = build_next_month_url(city, last_date) if next_url: r.lpush("weather:start_urls", next_url) print(f"已推入 {len(latest)} 个城市的增量任务")

逻辑说明:latest字典拿到每个城市已入库的最新日期,build_next_month_url按你的站点 URL 规则拼出下一个月。只推增量,Redis 队列短,跑得快,也不容易触发目标站点的频率限制。第一次跑全量时,latest为空,可以单独写一个初始化脚本把所有城市所有月份推一遍。

参数说明:db=0要和settings.py里的REDIS_URL库号一致。lpush是左推,配合PriorityQueue时注意优先级字段,用FifoQueue则无所谓顺序。增量脚本建议加个--dry-run参数先打印要推的 URL,确认无误再实际推入。

验证增量是否生效,跑完一轮后对比数据库行数和 Redis 队列长度:

检查项命令预期
数据库总行数SELECT COUNT(*) FROM history_weather只增不减
去重指纹数redis-cli scard weather:dupefilter与新增请求数接近
待处理队列redis-cli llen weather:requests跑完后归零
起始队列redis-cli llen weather:start_urls跑完后归零

从那以后我每次跑分布式爬虫,都会先redis-cli monitor看几秒队列消费情况,确认分发正常再离开。增量更新这个习惯帮我省掉了大量重复抓取的时间,也降低了被目标站点限流的概率。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进