我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

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 workerworker 数量按你手头机器算。我一般会先用两台 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_PERSISTTrue是毕业设计演示时的后悔药——中途 CtrlC 或者机器重启队列还在 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, callbackself.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 scrapy2.11.0 scrapy-redis0.7.3 redis5.0.1Redis 服务端的配置重点改两处bind和protected-mode。多机部署时调度机的redis.conf里把bind 127.0.0.1改成bind 0.0.0.0同时设protected-mode no否则 worker 连不上。改完重启 Redisredis-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( host127.0.0.1, userroot, passwordyourpass, databaseweather, charsetutf8mb4 ) 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 highVALUES(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加upsertTrue即可字段结构不用改。参数说明charsetutf8mb4是为了兼容天气描述里可能出现的特殊符号。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_PERSISTTrue且去重集合从不清理几十万条指纹一直堆着。或者请求队列里的 URL 被反复推入列表无限增长。解决正式跑数据时设SCHEDULER_PERSISTFalse爬完自动清空。定期用redis-cli scard weather:dupefilter监控集合大小超过预期就手动del掉重建。给 Redis 设maxmemory和maxmemory-policy allkeys-lru作为兜底但别依赖它主动清理更稳。5. 进阶技巧用 Redis 做断点续爬与增量更新分布式爬虫跑长任务最怕中途断掉重来。Scrapy-Redis 的SCHEDULER_PERSISTTrue只是保住了队列但去重指纹也在重启后已抓过的 URL 不会重抓这是断点续爬的基础。真正实用的是增量更新只抓新增月份的数据而不是每次全量重跑。做法是在推起始 URL 之前先查数据库里每个城市已抓到的最大日期只把该日期之后的月份链接推入 Redis。下面这段脚本可以放在项目根目录跑一次就更新队列# incremental_push.py import pymysql import redis # 连数据库查每个城市的最新日期 conn pymysql.connect(host127.0.0.1, userroot, passwordyourpass, databaseweather) cursor conn.cursor() cursor.execute(SELECT city, MAX(date) FROM history_weather GROUP BY city) latest dict(cursor.fetchall()) # 连 Redis推入需要补抓的月份 URL r redis.Redis(host192.168.1.10, port6379, db0) 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为空可以单独写一个初始化脚本把所有城市所有月份推一遍。参数说明db0要和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看几秒队列消费情况确认分发正常再离开。增量更新这个习惯帮我省掉了大量重复抓取的时间也降低了被目标站点限流的概率。希望帮到你。本文还有配套的精品资源点击获取
返回列表