news 2026/10/26 20:43:41

Scrapy-Redis分布式爬虫:全国历史天气数据采集实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy-Redis分布式爬虫:全国历史天气数据采集实战

简介:这份资源是面向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数据源内部编码,用于拼 URL
datedate具体日期
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://:yourpassword@127.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(host="127.0.0.1", port=6379, password="yourpassword", db=0) 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 = f"https://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 更省事。

参数说明:db=0是 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 降到 0,scard是否涨到 6。第二步,查数据库,确认入库行数和预期一致,字段没有 NULL 或错位。第三步,故意把 Redis 停掉再启动,看爬虫是否报错退出而不是静默卡死,这验证的是异常处理。第四步,启动第二台机器连同一个 Redis,push 新种子,确认两台机器都能抢到任务且不重复。

这里有个具体技巧:给种子 URL 加一个调试标记,比如?debug=1,在 pipeline 里遇到这个标记就打印完整 Item 而不入库。这样验证阶段不会污染正式表,确认无误后去掉标记再正式跑。

验证项命令/方法通过标准
队列消费redis-cli llen从种子数降到 0
全局去重redis-cli scard等于种子数,多机不翻倍
入库正确查数据库行数与预期条数一致
异常恢复停 Redis 再启动爬虫报错可感知,不静默丢任务
多机协同两台机器同 Redis任务不重复,队列共享

放量之后,我习惯每天看一眼死信队列长度。如果死信突然涨,多半是目标站点改版或限流升级,这时候停下来查比硬跑划算。全国历史天气这种长周期采集,拼的不是单次速度,而是稳定跑完不返工。把种子生成、去重、重试、入库、监控这五块都做成可重复执行的脚本,下次换数据源或换年份,改几个参数就能复用,这才是分布式爬虫真正的价值。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 20:42:03

AI驱动数据价值提升:从数据治理到可视化落地的实战指南

7月3日,我照例翻自己上半年经手的数据项目,发现一个特别扎心的规律:凡是让业务方真正点头说"有用"的项目,都不是靠模型多炫,而是把数据从"躺在库里吃灰的数字"变成了"能直接指挥动作的判断&q…

作者头像 李华
网站建设 2026/10/26 20:41:18

BP神经网络与PID结合的永磁同步电机智能控制

1. 项目概述:当神经网络遇上电机控制在工业自动化领域,永磁同步电机(PMSM)因其高效率、高功率密度等优势,已成为伺服驱动系统的首选。但传统PID控制在面对非线性、时变系统时表现乏力,这正是我尝试将BP神经…

作者头像 李华
网站建设 2026/10/26 20:41:04

MCP协议实现AI代码助手的项目级记忆能力

1. 项目概述:让AI代码助手真正“记住你”“AI 代码助手每次开新对话都要重新介绍项目?这个工具给它装上了真正的记忆”——这句话不是营销话术,而是我在过去三个月里反复验证过的真实体验。作为一名每天和 TypeScript、Node.js、Playwright 打…

作者头像 李华
网站建设 2026/10/26 20:40:51

物业大模型落地:RAG知识库与混合检索工程实践

业内有句话我印象很深:物业公司的信息化预算,过去十年大半花在了“把纸搬到屏幕上”,OA、收费系统、工单系统、设备台账,一个个孤岛建起来,数据进去了,但知识没出来。现在数智化转型的口号一响,…

作者头像 李华
网站建设 2026/10/26 20:40:11

金融数据API设计与RESTful架构实践

1. 金融数据API的特殊性与挑战金融数据API与传统API相比存在显著差异,这源于金融行业特有的数据属性和业务需求。首先,金融数据具有极强的时效性,股价、汇率等市场数据每秒都在变化,这对API的实时性提出了极高要求。其次&#xff…

作者头像 李华
网站建设 2026/10/26 20:38:54

从零实现可调试DNN:NumPy手写卷积、BN与Dropout

1. 这不是“又一篇深度学习教程”,而是带你亲手拆开DNN的齿轮箱你可能已经看过太多标题里带“Python深度学习”“手把手”“零基础”的文章——它们要么堆砌公式让你望而生畏,要么只给你几行model.fit()就宣告成功。但真正卡住大多数人的,从来…

作者头像 李华