Scrapy-Redis分布式爬虫:全国历史天气数据采集实战
发布时间:2026/10/26 20:43:50 锦皓数字建站

简介这份资源是面向Python爬虫学习者与毕业设计选题学生的Scrapy分布式爬虫实战项目聚焦全国历史天气数据的大规模抓取场景。项目基于Twisted异步网络库涵盖Spiders、Item Pipeline、Downloader Middleware等核心组件并借助分布式扩展将抓取任务分散到多台机器提升速度与稳定性同时降低对目标站点的访问压力。抓取字段涉及日期、温度、湿度、风向风力等数据经清洗验证后存入数据库或CSV、JSON文件。压缩包共9个文件以7个py源码为主体另含1个md说明文档与1个gitignore配置整体约8KB已在Windows 10/11环境测试通过。资源内附示例图片与部署教程可帮助读者理解分布式爬虫的搭建、运行与数据存储结构适合作为爬虫技能进阶与毕业设计参考。目前已有232人学习下载。1. 全国历史天气数据怎么爬从单机 Scrapy 到分布式采集的落地判断做气象数据分析、农业模型、能耗预测的团队几乎都绕不开一份长周期、覆盖全国站点的历史天气数据。单机 Scrapy 爬虫跑一个城市一年数据可能只要几分钟但把时间轴拉到十年、把空间轴铺到全国三百多个城市站点请求量会瞬间膨胀到几十万甚至上百万级单机 IP 被限流、任务中断无法续跑、去重逻辑拖垮内存这些问题会集中爆发。这正是「scrapy 分布式爬虫爬取全国历史天气」这个标题真正要解决的事不是教你写一个爬虫而是把一份全国范围的历史气象数据用可横向扩展、可断点续爬、可去重的方式稳定拿下来。适合已经写过基础 Scrapy 爬虫、准备把采集规模从单机推到多机的工程师也适合需要评估这套方案值不值得投入的技术负责人。下面按选型、实现、调度、避坑、验证的顺序讲透。2. 分布式爬虫的选型与数据源拆解为什么是 Scrapy-Redis 而不是多进程2.1 全国历史天气的数据源特征决定了架构历史天气数据源通常分两类一类是公开气象接口按城市和月份返回结构化 JSON字段稳定但请求频率受限另一类是网页版历史天气页面按城市、按年月分页展示需要解析 HTML 表格。全国范围意味着城市维度约 300 到 400 个时间维度如果取近十年就是 300 × 10 × 12 ≈ 36000 个「城市-月」组合每个组合再分页实际请求量在十万到百万级。这个量级下单机 Scrapy 的三个瓶颈会依次出现。第一是请求队列在内存里进程一挂全部丢失没有后悔药第二是去重靠内存里的指纹集合几十万 URL 之后内存占用明显上升第三是单 IP 并发受限调度器再快也卡在网络层。分布式要解决的核心就是把这三点从单机内存里搬出来交给一个共享的中间层。常见做法是用 Scrapy-Redis 做分布式调度Redis 承担请求队列和去重指纹集合多个 Scrapy 爬虫实例连同一个 Redis谁空闲谁取任务。选它而不是自己写多进程是因为 Scrapy-Redis 直接替换了 Scrapy 原生的调度器和去重类改动量小且天然支持断点续爬——Redis 里的队列没清空重启爬虫就能接着跑。2.2 Scrapy-Redis 的核心组件与数据流理解这套方案要先看清四个角色。Redis 里的requests列表或有序集合是待抓取请求队列dupefilter集合是已抓取 URL 指纹start_urls是初始种子。每个爬虫实例启动后从requests取请求抓完把新请求 push 回队列同时把指纹写进dupefilter。调度器不再用 Scrapy 默认的Scheduler而是scrapy_redis.scheduler.Scheduler去重类换成scrapy_redis.dupefilter.RFPDupeFilter。这里有个容易忽略的点分布式去重是全局的意味着多个实例不会重复抓同一个 URL但代价是每次请求都要和 Redis 通信一次。如果 Redis 网络延迟高去重开销会拖慢整体速度。所以 Redis 最好和爬虫实例在同一内网别跨公网。2.3 环境搭建与依赖安装先装基础依赖。Scrapy 版本建议 2.8 以上Scrapy-Redis 用社区维护的版本Redis 服务端 6.x 或 7.x 都行。# 创建虚拟环境避免污染系统 Python python -m venv venv source venv/bin/activate # 安装核心依赖 pip install scrapy scrapy-redis redis # 本地起一个 Redis 用于开发调试 redis-server --port 6379 --daemonize yes redis-cli ping # 返回 PONG 说明服务正常逻辑说明虚拟环境隔离是为了多机部署时依赖版本一致避免 A 机器能跑 B 机器报错。Redis 用默认端口 6379生产环境要改端口并加密码。redis-cli ping是最快的连通性验证比直接跑爬虫排错成本低得多。参数说明--daemonize yes让 Redis 后台运行开发时方便生产环境建议用 systemd 或容器管理不要裸跑。2.4 数据源字段与抓取粒度设计在写爬虫前先把数据模型定下来否则后期补字段要重爬。历史天气通常需要的字段包括城市名、城市编码、日期、最高温、最低温、天气现象、风向、风力。抓取粒度建议按「城市-月」作为任务单元而不是按「城市-年」原因是单页返回的数据量有限按月切分能让每个请求的解析量可控也方便失败重试时只重跑某个月。字段类型说明citystring城市名如「北京」city_codestring数据源内部编码用于拼 URLdatedate具体日期temp_highint最高温摄氏度temp_lowint最低温摄氏度weatherstring天气现象windstring风向风力这张表的作用是让 Item 定义和后续入库字段对齐避免解析时临时决定字段名导致不同月份数据字段不一致。3. 分布式爬虫的代码实现从 Item 定义到 Redis 调度接入3.1 定义 Item 与 Spider 骨架先定义 Item字段和上面的表对齐。然后写 Spider继承RedisSpider而不是普通Spider这样启动时从 Redis 的start_urls读种子而不是从代码里的start_urls列表读。import scrapy from scrapy_redis.spiders import RedisSpider class WeatherItem(scrapy.Item): city scrapy.Field() city_code scrapy.Field() date scrapy.Field() temp_high scrapy.Field() temp_low scrapy.Field() weather scrapy.Field() wind scrapy.Field() class HistoryWeatherSpider(RedisSpider): name history_weather # 关键redis_key 是种子队列的键名多机共用同一个键 redis_key history_weather:start_urls def parse(self, response): # 解析表格行逐行产出 Item for row in response.css(table.weather-table tr): item WeatherItem() item[city] response.meta[city] item[city_code] response.meta[city_code] item[date] row.css(td:nth-child(1)::text).get() item[temp_high] row.css(td:nth-child(2)::text).get() item[temp_low] row.css(td:nth-child(3)::text).get() item[weather] row.css(td:nth-child(4)::text).get() item[wind] row.css(td:nth-child(5)::text).get() yield item逻辑说明RedisSpider的redis_key是分布式调度的入口所有机器监听同一个键。parse里用response.meta取城市信息是因为 URL 里可能只带了城市编码城市名需要在生成请求时通过 meta 传进来。逐行 yield Item 而不是攒成列表是为了让 pipeline 尽早处理降低内存峰值。参数说明redis_key命名建议带项目前缀避免多个项目共用 Redis 时键冲突。response.css的选择器要按实际页面结构调整这里只是示例结构。3.2 settings.py 的分布式配置这是整个方案最关键的一步配置错了爬虫要么不分布式要么直接卡死。# settings.py # 启用 Redis 调度器替换默认调度器 SCHEDULER scrapy_redis.scheduler.Scheduler # 启用 Redis 去重全局去重靠它 DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter # 允许暂停和恢复队列不清空 SCHEDULER_PERSIST True # 请求队列用有序集合支持优先级 SCHEDULER_QUEUE_CLASS scrapy_redis.queue.PriorityQueue # Redis 连接信息 REDIS_URL redis://:yourpassword127.0.0.1:6379/0 # 并发和延迟按目标站点承受能力调 CONCURRENT_REQUESTS 16 DOWNLOAD_DELAY 0.5 # 关闭默认的 start_urls 机制改用 redis_key # 不要设置 start_urls否则会走单机逻辑逻辑说明SCHEDULER_PERSIST True是断点续爬的前提设为 False 时爬虫关闭会清空队列重启就从头来。PriorityQueue让重要请求优先比如失败重试的请求可以设更高优先级。REDIS_URL里带密码是生产环境必须的开发环境可以省略密码段。参数说明CONCURRENT_REQUESTS是单机并发多机部署时总并发是机器数乘以这个值要按目标站点限流阈值反推。DOWNLOAD_DELAY是每个请求之间的间隔设太小容易触发封禁设太大采集周期拉长0.5 到 1 秒是常见折中。3.3 种子 URL 的生成与入队分布式爬虫不靠代码里的start_urls而是靠往 Redis 里 push 种子。写一个独立的脚本生成「城市-月」组合的 URL 并批量入队。import redis r redis.Redis(host127.0.0.1, port6379, passwordyourpassword, db0) cities [(北京, 101010100), (上海, 101020100), (广州, 101280101)] years range(2014, 2024) months range(1, 13) count 0 for city_name, city_code in cities: for year in years: for month in months: url fhttps://example.com/weather/{city_code}/{year}{month:02d}.html # 把城市信息编码进 URL 或单独存 meta这里用查询参数简化 r.lpush(history_weather:start_urls, url) count 1 print(f入队 {count} 条种子)逻辑说明用lpush而不是rpush是因为 Scrapy-Redis 默认从列表左侧取lpush入队后取出的顺序和入队顺序相反对采集顺序无影响时可忽略。城市信息如果 URL 里带不全可以在入队时用 JSON 序列化整个请求对象但简单场景下把城市编码编进 URL 更省事。参数说明db0是 Redis 的逻辑库多项目共用时建议分库。种子数量大时不要一次性全 push可以分批避免 Redis 内存瞬时冲高。3.4 多机启动与任务分发验证代码和配置就绪后在多台机器上分别启动爬虫它们会自动连同一个 Redis 抢任务。# 每台机器上执行启动爬虫 scrapy crawl history_weather # 在 Redis 里查看队列剩余量和已抓取指纹数 redis-cli llen history_weather:start_urls redis-cli scard history_weather:dupefilter逻辑说明llen返回待抓取队列长度随着多机消费会持续下降scard返回已去重指纹数量持续上升。两个数字一降一升说明分布式调度正常工作。如果llen不降检查爬虫是否真的连上了 Redis如果scard不涨检查去重类配置是否生效。参数说明dupefilter的键名默认是项目名:dupefilter可以在 settings 里用DUPEFILTER_KEY自定义。指纹集合会随抓取量增长长期运行要关注 Redis 内存。4. 调度、去重与存储的工程化处理让百万级请求不翻车4.1 去重指纹的内存控制Scrapy-Redis 默认用 SHA1 对请求方法、URL、请求体做哈希存进 Redis 的 set。百万级 URL 的指纹集合每个指纹 40 字节左右加上 Redis set 的底层开销内存占用在百 MB 级别单机 Redis 扛得住。但如果 URL 里带了随机参数或时间戳去重会失效同一个页面被反复抓。解决办法是在生成 URL 时规范化去掉无意义的查询参数统一大小写统一末尾斜杠。这一步在种子生成脚本里做比在爬虫里做更省事。另外可以给dupefilter设置过期时间历史指纹超过一定天数自动清理避免无限增长。4.2 失败重试与死信队列分布式环境下网络抖动是常态请求失败要有重试机制。Scrapy 自带RetryMiddleware但默认重试次数是 2 次对不稳定的数据源不够。可以在 settings 里调高同时把重试失败的请求单独记录形成死信队列事后人工排查。# settings.py 重试配置 RETRY_ENABLED True RETRY_TIMES 3 RETRY_HTTP_CODES [500, 502, 503, 504, 408, 429] # 自定义中间件把最终失败的请求写进 Redis 死信队列 class DeadLetterMiddleware: def process_response(self, request, response, spider): if response.status in [500, 502, 503, 504]: spider.redis_client.lpush(history_weather:dead_letter, request.url) return response逻辑说明RETRY_HTTP_CODES里加上 429 是因为限流返回码需要退避重试。死信队列用单独的 Redis 键和正常队列隔离方便统计失败率。中间件里拿spider.redis_client需要在 Spider 初始化时建立连接。参数说明RETRY_TIMES设 3 次是平衡再多会拖慢整体进度。死信队列要定期清理否则同样占内存。4.3 数据入库的批量写入抓下来的 Item 如果逐条写数据库百万级数据会产生百万次 IO数据库压力大。常见做法是在 pipeline 里攒批每 500 到 1000 条写一次。用 MySQL 的话可以用executemany用 MongoDB 可以用insert_many。class BatchMySQLPipeline: def __init__(self): self.buffer [] self.batch_size 500 def process_item(self, item, spider): self.buffer.append(dict(item)) if len(self.buffer) self.batch_size: self.flush() return item def flush(self): if not self.buffer: return # 批量插入具体 SQL 按表结构写 self.cursor.executemany( INSERT INTO weather_history (city, date, temp_high, temp_low) VALUES (%s, %s, %s, %s), [(i[city], i[date], i[temp_high], i[temp_low]) for i in self.buffer] ) self.conn.commit() self.buffer.clear() def close_spider(self, spider): self.flush() # 爬虫结束时把剩余数据写完逻辑说明close_spider里必须再 flush 一次否则最后不足一批的数据会丢这是血泪经验。批量大小 500 是折中太大内存占用高太小失去批量意义。参数说明batch_size按数据库承受能力调MySQL 单次executemany几千条也可以但要注意max_allowed_packet限制。4.4 监控指标与进度可视化分布式爬虫跑起来后最怕的是「不知道跑到哪了」。至少要看四个指标队列剩余量、已抓取指纹数、死信队列长度、入库行数。前三个从 Redis 直接读第四个从数据库查。可以写一个简单的监控脚本定时打印。# 每 30 秒打印一次进度 while true; do echo 待抓取: $(redis-cli llen history_weather:start_urls) echo 已去重: $(redis-cli scard history_weather:dupefilter) echo 死信: $(redis-cli llen history_weather:dead_letter) sleep 30 done逻辑说明队列剩余量持续下降且死信不暴涨说明采集健康。如果队列不降但指纹也不涨多半是爬虫卡在某个请求上要查网络或目标站点是否变更。参数说明sleep 30是监控频率生产环境可以接入 Prometheus 做长期趋势。5. 全国历史天气采集的避坑清单五个真实翻车现场5.1 现象多机启动后数据重复入库原因去重类没生效或者多台机器用了不同的 Redis 库。常见是 settings 里DUPEFILTER_CLASS拼写错误Scrapy 静默回退到默认去重每台机器各自去重跨机器就重复了。解决启动后立刻用redis-cli scard看指纹集合是否在涨不涨就是配置没生效。检查DUPEFILTER_CLASS的值是否精确等于scrapy_redis.dupefilter.RFPDupeFilter以及所有机器REDIS_URL是否指向同一个库。5.2 现象爬虫跑一段时间后卡死日志无报错原因Redis 连接池耗尽或者某个请求一直不返回导致调度器阻塞。Scrapy-Redis 默认连接池大小有限多机高并发时容易打满。解决在 settings 里调大REDIS_PARAMS的连接池配置同时给DOWNLOAD_TIMEOUT设一个合理值比如 15 秒避免单个请求无限等待。卡死时用redis-cli client list看连接数超过预期就是连接泄漏。5.3 现象目标站点返回 403 或验证码原因并发太高或请求头太假。分布式多机总并发是机器数乘以单机并发很容易超过站点阈值。解决降低CONCURRENT_REQUESTS加DOWNLOAD_DELAY并配置真实的User-Agent池。如果出现验证码说明已经触发风控要暂停一段时间再降速重试硬刚只会让 IP 被封更久。5.4 现象历史数据字段缺失或错位原因目标页面结构在不同年份不一致早期页面可能少列或列顺序不同。用固定nth-child选择器就会错位。解决解析时不要死用列序号改用表头文字匹配列或者按年份分支处理。入库前加字段校验温度字段解析不出数字就记入异常表别直接写 NULL 污染数据。5.5 现象断点续爬后重复抓取已完成部分原因SCHEDULER_PERSIST设成了 False或者手动清过 Redis 队列。另一个隐蔽原因是爬虫用了start_urls而不是redis_key重启后从代码里的种子重新开始。解决确认SCHEDULER_PERSIST True且 Spider 继承的是RedisSpider。重启前用llen确认队列还有剩余重启后观察指纹数是否接着涨而不是归零。6. 用少量种子验证全链路分布式爬虫上线前的自检技巧整套方案写完别急着把几万条种子全 push 进去。我一般会先用 3 个城市、1 个年份、2 个月也就是 6 条种子跑一遍全链路确认从入队、抓取、去重、入库到监控都正常再放量。这个习惯帮我省过好几次「跑了一夜发现字段全错」的后悔药。验证分四步。第一步只 push 6 条种子启动单机爬虫看llen是否从 6 降到 0scard是否涨到 6。第二步查数据库确认入库行数和预期一致字段没有 NULL 或错位。第三步故意把 Redis 停掉再启动看爬虫是否报错退出而不是静默卡死这验证的是异常处理。第四步启动第二台机器连同一个 Redispush 新种子确认两台机器都能抢到任务且不重复。这里有个具体技巧给种子 URL 加一个调试标记比如?debug1在 pipeline 里遇到这个标记就打印完整 Item 而不入库。这样验证阶段不会污染正式表确认无误后去掉标记再正式跑。验证项命令/方法通过标准队列消费redis-cli llen从种子数降到 0全局去重redis-cli scard等于种子数多机不翻倍入库正确查数据库行数与预期条数一致异常恢复停 Redis 再启动爬虫报错可感知不静默丢任务多机协同两台机器同 Redis任务不重复队列共享放量之后我习惯每天看一眼死信队列长度。如果死信突然涨多半是目标站点改版或限流升级这时候停下来查比硬跑划算。全国历史天气这种长周期采集拼的不是单次速度而是稳定跑完不返工。把种子生成、去重、重试、入库、监控这五块都做成可重复执行的脚本下次换数据源或换年份改几个参数就能复用这才是分布式爬虫真正的价值。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。