简介:一套覆盖全国2290个地区、时间跨度2011至2024年的天气数据采集与处理资料包,面向爬虫开发、数据分析及气象应用的学习者与从业者。数据源取自天气网历史天气频道,包含历史气温、风向、风力、生活指数、健康指数、旅游指数等维度,可支撑农业、交通、旅游等场景的基础数据需求。资源配有Python爬虫源码、XML配置文件和Jupyter Notebook分析示例,能清晰还原从目标选择、参数配置、请求解析到CSV存储的多城市批量采集流程。包内共300个文件,以261个CSV天气数据为主体,29个PY脚本实现抓取逻辑,另有6个XML配置文件与2个IPYNB演示文档,压缩包整体约1.23MB,结构简洁便于定位。目前已有163人学习下载;借助该包可直接获得2011至2024年多个城市的标准化历史天气数据集,并参考源码学习如何处理分页、数据清洗与字段整理,非常适合需要夯实爬虫和数据处理技能的初学者。
1. 全国天气数据采集:把零散网页变成结构化天气数据集
写了三年采集脚本,最怕的不是被限流,而是数据拿到手发现字段对不上:昨天还是"晴",今天变成"晴转多云",温度和风力各说各话。这套全国天气数据采集源码,把从请求调度、页面解析、数据清洗到落库的完整链路拆成了一个个可替换的模块,拿到手改一改配置就能跑出按城市和日期组织的结构化天气数据集。它适合三类人:做数据分析但苦于没有稳定数据源的研究者,刚学 Scrapy 想找个完整工程参考的开发者,以及需要批量历史天气数据做模型训练的算法同学。项目的核心是一套轻量级采集框架:调度器维护城市队列,下载器处理重试与限速,解析层把天气状况、温度、风力等字段统一格式,清洗后按日期分表写入 SQLite。整条链路不需要额外部署数据库服务,一台普通开发机就能跑完全国城市的数据采集。
2. 架构设计:轻量级采集框架如何拆解数据链路
在打开代码之前,先理解整个工程的骨架。这里讲清三件事:技术栈为什么这么选、目录结构怎么组织、数据模型怎么设计。弄懂之后,后面读代码就是按图索骥。
2.1 技术选型:Scrapy 主采、Requests 补采、SQLite 落库
先说结论:这套项目没有用 Airflow 或分布式调度的全家桶,而是用 Scrapy 作为批量采集引擎、Requests 作为单点补充工具、SQLite 作为最终存储。选型的理由要从数据量级说起。全国天气历史数据即便全量累积跑一年,也就是百来万行的量级,单机完全能扛住。用分布式框架属于杀鸡用牛刀,调度、监控、容错全都要额外配置,运维成本反而拖慢开发进度。
Scrapy 承担的是每天定时批量跑的城市队列。每个城市的天气页面是一个独立 Request,交给下载器并发请求,项目默认把并发数设为 8,并且开启了下载延迟。这个组合让采集节奏对目标站点相对友好,不至于单个 IP 的请求频率把数据源打挂。Requests 则用在两类场景:一是 Scrapy 解析失败后的单条重试,二是调试某个城市页面时手动拉取 HTML 看结构。两个 HTTP 客户端分工明确,前者跑批量,后者做兜底。
有人可能会问,既然 Requests 也能批量跑,为什么还要引入 Scrapy?答案是 Scrapy 自带了两样 Requests 需要手写的东西:并发调度和下载中间件。Requests 的并发需要自己用线程池或协程管理,而 Scrapy 的引擎天然处理并发请求的调度、去重和异常恢复。另一个实际体验是 Scrapy 的日志自带采集统计,item_scraped_count、request_count 这些指标在排查问题时比自定义日志直观得多。
SQLite 作为存储层是刻意做的取舍。很多爬虫项目一上来就上 MySQL,但天气数据是典型的按城市、按日期查询模型,SQLite 的 B+ 树索引在这种场景下查询性能完全够用,而且部署时不用额外维护数据库服务。数据库文件就是一个独立的 .db 文件,备份直接拷文件,迁移也简单。项目里存储层做了接口抽象,如果后续数据量真的大到单机 SQLite 扛不住,可以把写入逻辑替换成 MySQL 版本,清洗层和解析层完全不用动。
这三个选型合起来看,核心思路是"能单机解决的问题不引入分布式"。对学习者和中小型数据分析任务来说,这套组合的性价比是最高的。
2.2 目录结构:每个目录在采集链路中的位置
项目的目录结构是典型的 Scrapy 工程风格,但在默认模板基础上增加了清洗和存储两个自定义层:
weather_spider/ ├── scrapy.cfg ├── requirements.txt ├── config/ │ ├── settings.py # 并发数、限速、User-Agent 轮换 │ └── cities.json # 城市编码与名称映射表 ├── spiders/ │ ├── __init__.py │ └── weather.py # 核心爬虫,定义请求与解析逻辑 ├── pipelines/ │ ├── __init__.py │ ├── cleaner.py # 数据清洗与去重 │ └── storage.py # SQLite 写入与分表 ├── utils/ │ ├── logger.py # 日志封装 │ └── retry.py # 请求重试与退避 ├── db/ │ └── weather.db # 生成后的数据库文件 └── scripts/ ├── init_db.py # 建表与索引 └── export_csv.py # 导出为 CSV 数据集每个目录的职责边界都很清晰。config 目录集中管理城市清单和运行参数,意味着换数据源或者增删城市时不需要改代码,只改 JSON。比如想把采集范围从省会城市扩展到地级市,只要往 cities.json 里追加城市对象就行;想把采集频率从每天一次改成每小时一次,改 settings.py 里的调度配置即可。
spiders 目录只负责"请求加解析",拿到结构化字段后直接 yield 给 pipelines。pipelines 目录里 cleaner 和 storage 是两个独立类,前者处理脏数据,后者处理写入逻辑。这种三段式分离最大的好处是变更隔离:目标站页面改版时,通常只动 spiders 里的解析函数;存储结构调整时,只动 storage 类;采集规则变化时,只动 cleaner。项目大量设计都是围绕"让改动落在一个目录里"来做。
utils 目录放跨层复用的工具。logger 做了日志分级和滚动写入,把正常采集日志和丢弃记录分开存,排查问题时有迹可循。retry 封装了带退避的重试逻辑。scripts 目录则是给运维用的辅助脚本,init_db 负责建表,export_csv 负责把数据导出成数据集,便于外部工具直接使用。
2.3 数据模型:按城市与日期组织的气象字段
落库之前,采集到的每条原始记录需要映射成固定的表结构。项目的核心表名为 daily_weather,在设计上直接对应分析场景的查询习惯:
daily_weather( id INTEGER PRIMARY KEY AUTOINCREMENT, city_code TEXT NOT NULL, -- 城市编码,如 010101 city_name TEXT NOT NULL, -- 城市名称 date TEXT NOT NULL, -- 日期,格式 YYYY-MM-DD weather TEXT, -- 天气状况,如 晴、多云、小雨 max_temp INTEGER, -- 最高气温,整数摄氏度 min_temp INTEGER, -- 最低气温 wind_direction TEXT, -- 风向,如 东南风 wind_level TEXT, -- 风力等级 humidity INTEGER, -- 相对湿度百分比 updated_at TEXT -- 采集时间戳 )主键用自增 id 是为了保证写入顺序和唯一性,但实际查询最常用的条件是 city_code 加 date 的组合,所以项目在建表时给这两个字段建了联合索引。weather 和 wind_direction 这类文本字段没有强制枚举约束,因为不同地区的天气描述存在差异,"晴"和"少云"这类原始文本需要保留,枚举约束反而会把合法数据卡掉。updated_at 记录采集时间,用来排查某条数据是不是隔了很久的过期数据。
这里还要提一下 city_code 的设计。项目使用数据源自带的城市编码而不是城市名称作为关联键,好处是编码稳定且不会重名,坏处是拿到数据集的人需要额外对照城市映射。好在导出的 CSV 里同时带 code 和 name 两列,数据分析时直接按 name 看就行了,不需要再查表。这套数据模型在"查询够快"和"保留原始语义"之间取了平衡。max_temp 和 min_temp 用整数而不是浮点,因为天气页面上的温度通常已经是整数,用浮点只会引入无意义的精度问题。湿度字段用 INTEGER 存百分数数值,不做归一化,由下游分析工具自行换算。
3. 核心模块拆解:从请求进入内存到数据落库的完整链路
从城市队列生成第一个请求,到数据库里出现一条完整记录,整个链路要经过调度、下载、解析、清洗、存储五层。下面沿着代码往下走,每一层都有对应的 Python 文件,代码和说明对照着看。
3.1 请求调度:城市队列、限速与重试
所有采集动作的起点是调度器。项目在 spiders/weather.py 里用 start_requests 方法把 config/cities.json 中的每个城市编码转成一个独立的 Request,并附带当天的日期参数:
# spiders/weather.py 核心调度逻辑 import json import scrapy from utils.retry import make_retry_request class WeatherSpider(scrapy.Spider): name = "weather" def start_requests(self): with open("config/cities.json", "r", encoding="utf-8") as f: cities = json.load(f) for city in cities: request = make_retry_request( url=self.build_url(city["code"]), callback=self.parse_weather, meta={"city": city}, errback=self.handle_error, ) yield request def build_url(self, city_code): # 拼接目标平台的城市天气页地址 return f"https://weather.example.com/{city_code}.html"这段代码有两个细节值得注意。make_retry_request 是 utils/retry.py 里的封装,它在 Request 对象上增加了重试次数和退避策略,默认失败后等 5 秒再重试,最多三次。Scrapy 自带的 retry middleware 虽然能做重试,但不带退避间隔,短时间密集重试反而更容易触发数据源的限流。另一个细节是 meta 参数的传递:把整个 city 字典挂在 meta 上,回调函数 parse_weather 从 response.meta 里取出来,这样即使并发请求乱序返回,每条响应也能正确对应到自己的城市信息,不会串数据。
控制整体节奏的开关在 config/settings.py 里。项目把 CONCURRENT_REQUESTS 设为 8,DOWNLOAD_DELAY 设为 0.8 秒,这两个参数配合的效果是:最多 8 个请求同时在飞,每个请求之间至少间隔 0.8 秒。如果目标站点响应明显变慢,应该优先把并发降到 4 或 2,而不是去改代码。这里有个经验:请求调度层的调参顺序,永远是先降并发、再升延迟、最后才考虑加代理。
为了把调度逻辑从具体业务里解耦,项目单独维护了一个城市队列。cities.json 中的城市顺序决定了初始请求的入队顺序,但 Scrapy 的调度器会按优先级和去重队列重新组织实际的发送顺序。城市对象通常几百条,直接用 JSON 加载比引入外部队列服务省事得多。后续如果要做全国几千个区县级采集,再考虑把城市队列拆成独立文件按区块分批加载,避免启动时一次性加载太多内容进内存。关于重试这里还有一个细节:make_retry_request 里除了重试次数,还设置了 download_timeout 参数,默认 10 秒。这个值不是随便定的,它要求单次请求在 10 秒内必须有响应,超时就触发重试。天气页面通常很轻,10 秒已经足够宽裕,如果目标站响应超过这个值,说明数据源本身有问题,重试也没用。调这个参数时要注意,设得太短会误杀正常请求,设得太长会让整批采集卡在慢请求上。
3.2 页面解析:从 HTML 结构到结构化字段的转换
解析层是这套项目里最容易受数据源改版影响的环节。parse_weather 收到响应后,先用 XPath 提取天气信息块,再把原始字符串清洗成统一格式:
# spiders/weather.py 解析逻辑 def parse_weather(self, response): city = response.meta["city"] # 页面中的天气信息被包裹在 class 为 weather-info 的 div 里 weather_block = response.xpath("//div[contains(@class, 'weather-info')]") if not weather_block: self.logger.warning(f"page structure changed: {city['code']}") return None item = { "city_code": city["code"], "city_name": city["name"], "date": response.meta.get("date", self.today()), "weather": weather_block.xpath( "./div[contains(@class, 'weather-text')]/text()" ).get(default=""), "max_temp": self._parse_temp( weather_block.xpath( "./div[contains(@class, 'temp-range')]/span[1]/text()" ).get(default="") ), "min_temp": self._parse_temp( weather_block.xpath( "./div[contains(@class, 'temp-range')]/span[2]/text()" ).get(default="") ), "wind_direction": weather_block.xpath( "./div[contains(@class, 'wind')]/text()" ).get(default="").strip(), "wind_level": weather_block.xpath( "./div[contains(@class, 'wind-level')]/text()" ).get(default="").strip(), "humidity": self._parse_humidity( weather_block.xpath( "./div[contains(@class, 'humidity')]/text()" ).get(default="") ), } yield item这段代码最核心的设计是 _parse_temp 和 _parse_humidity 两个私有解析方法。_parse_temp 接收页面上"12℃~23℃"这类原始文本,内部用正则提取数字,遇到"暂无"或空字符串时返回 None 而不是 0,因为 0 是有效气温,None 才是缺失值。_parse_humidity 先把"湿度78%"里的百分号去掉,再转成整数存储。
解析层和清洗层的边界也在这段代码里体现:解析层只做格式转换,不做业务判断。比如某个城市页面返回的气温明显异常(最高温 60 度),解析层照样提取出来,交给 cleaner 去判断剔除。这样划分的原因是解析层要保持纯粹,任何业务规则的变化都不应该触发解析代码的改动。反之,如果数据源调整了页面结构,也只是改 XPath 表达式,清洗规则不需要动。
3.3 数据处理与清洗:空值、异常值与重复记录的处理
数据采集只是第一步,数据处理才是决定数据集质量的关键。清洗层在 pipelines/cleaner.py 中实现,它接收爬虫 yield 出来的 item,逐字段做校验。项目里清洗规则分三类:空值处理、合理范围校验、重复记录去重。
# pipelines/cleaner.py 清洗逻辑 from scrapy.exceptions import DropItem class CleanerPipeline: def process_item(self, item, spider): # 1. 关键字段缺失直接丢弃 if not item.get("city_code") or not item.get("date"): raise DropItem(f"missing key field: {item}") # 2. 温度合理范围校验,常见事故:页面露出单位符号或乱码 for field in ("max_temp", "min_temp"): value = item.get(field) if value is not None and not (-50 <= value <= 50): raise DropItem(f"temperature out of range: {field}={value}") # 3. 湿度范围校验 humidity = item.get("humidity") if humidity is not None and not (0 <= humidity <= 100): raise DropItem(f"humidity out of range: {humidity}") # 4. 重复数据去重:同城市同日期只留一条 seen_keys = spider.state.setdefault("seen_keys", set()) dedup_key = f"{item['city_code']}_{item['date']}" if dedup_key in seen_keys: raise DropItem(f"duplicate record: {dedup_key}") seen_keys.add(dedup_key) return item这段代码展示了清洗层的三个原则。原则一:校验失败用 DropItem 异常直接丢弃,而不是修改数据后放行,避免脏数据进入存储层。原则二:温度范围设置得比较宽(-50 到 50),因为全国范围包含高海拔地区和夏季高温,范围太窄会误杀真实数据。原则三:去重放在爬虫进程的 state 里做,同一轮采集内维护一个 seen_keys 集合,重复数据会在内存里被拦截。
这套去重策略是"够用但不过度"的设计。它解决的是同一轮采集因为重试或页面跳转导致的重复写入,适合单机运行的场景。如果项目后续改成多机分布式采集,就要把这套内存去重换成 Redis 去重,set 的 key 结构可以原样迁移,逻辑几乎不用变。从数据结构上看,city_code 加 date 作为聚合键,天然就是天气数据的业务主键。
清洗层还有一个容易被忽略的职责:日志记录。每条被丢弃的数据都要能追溯原因,否则数据缺失时无法判断是没采集到还是被规则过滤了。项目在 cleaner 里通过 logger 记录 drop 原因,这份日志在后续数据校验阶段非常重要。实际跑批时,DropItem 数量占采集总量的比例应当很低,如果超过 5% 就需要回头检查解析逻辑是否出了问题。
3.4 数据落库:按日期分表与增量写入
清洗完成的数据进入 storage pipeline,写入 SQLite。项目的存储策略是按日期分表:表名格式为 daily_weather_20250615,也就是一个日期一张表。分表的直接好处是历史数据归档方便,删除过期数据时直接 DROP 表,不用执行 DELETE 语句逐行删。
# pipelines/storage.py 写入逻辑 import sqlite3 from datetime import datetime class StoragePipeline: def open_spider(self, spider): self.conn = sqlite3.connect("db/weather.db") self.conn.execute("PRAGMA journal_mode=WAL") def process_item(self, item, spider): table_name = self._table_for(item["date"]) self.conn.execute( f""" INSERT OR REPLACE INTO {table_name} (city_code, city_name, date, weather, max_temp, min_temp, wind_direction, wind_level, humidity, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( item["city_code"], item["city_name"], item["date"], item["weather"], item["max_temp"], item["min_temp"], item["wind_direction"], item["wind_level"], item["humidity"], datetime.now().isoformat(), ), ) self.conn.commit() return item写入时使用 INSERT OR REPLACE 而不是普通 INSERT,这等于在数据库层面做了第二道去重:同一城市同一天的数据如果重复采集,旧记录被新记录覆盖。前面 cleaner 做的是内存级去重,这里做的是落地级保护,两道防线防止数据膨胀和主键冲突。
PRAGMA journal_mode=WAL 是一个容易被忽略但很关键的配置。它启用了 SQLite 的预写日志模式,读写操作之间互不阻塞,适合采集任务一边写入一边被查询的场景。注意 WAL 模式会额外生成 -wal 和 -shm 两个文件,备份数据库时要把这三个文件一起拷贝,只拷 .db 文件会丢失未合并的日志数据。
4. 运行实战:从环境搭建到批量采集的完整流程
代码读通了只是第一步,真正跑起来才会遇到各种环境问题和数据源的变化。下面按照从零搭建到批量采集的操作顺序来写,结尾附上实际运行中踩过的高频问题。
4.1 环境准备与依赖安装
项目运行在 Python 3.8 以上版本,依赖集中在 requirements.txt 里。先用虚拟环境隔离依赖是最稳妥的方式,避免和系统 Python 的其他包冲突:
python -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt 中的核心依赖是 Scrapy、Requests、APScheduler 和 lxml。Scrapy 自带 lxml 解析器,所以大多数情况下不需要单独安装 lxml。APScheduler 用于定时采集任务的调度,如果只跑一次性数据采集,这个依赖可以从清单里去掉。安装完成后,用 pip list 确认一下关键包版本,Scrapy 主版本差异有时会影响 middleware 的写法。
安装完依赖,需要检查 config/cities.json 里的城市列表是否有实际数据。文件是一个 JSON 数组,每个元素包含 code 和 name 两个字段。code 是目标数据源的城市编码,通常是一串数字;name 是对应的中文名称。如果只想采集部分城市做测试,直接删掉不需要的城市对象即可。这里有个建议:第一次跑通前不要贪多,选两三个城市先验证链路,全部跑通了再放开全国城市列表,这样排查问题时影响面更小。
运行前还需要确认三个关键参数,它们都在 config/settings.py 里:
| 参数 | 默认值 | 作用 |
|---|---|---|
| CONCURRENT_REQUESTS | 8 | 最大并发请求数 |
| DOWNLOAD_DELAY | 0.8 | 请求间隔秒数 |
| RETRY_TIMES | 3 | 单条请求最大重试次数 |
这三个参数是采集节奏的油门和刹车。并发太大或延迟太短,容易触发数据源限流;并发太小,全国城市跑完全程要很久。初次运行时建议保持默认值,观察日志里 request 和 item 的比例,如果 request 数量涨得飞快但 item 产出很少,优先检查解析层而不是调参。
4.2 初始化数据库并启动采集
库表结构需要先初始化。项目提供 scripts/init_db.py,它会读取建表 SQL,创建 daily_weather 分表和联合索引:
python scripts/init_db.py python -m scrapy crawl weather第二条命令是启动采集的主命令。Scrapy 会先执行 start_requests,读取城市列表生成请求队列,然后按 settings.py 里的并发配置开始抓取。日志里如果看到 item_scraped_count 持续增长,说明采集链路正常;如果这个数字一直为 0,多半是解析层没有产出可用的 item,要回到第三章的解析逻辑去排查。
采集完成后可以用 export 脚本快速验证数据集:
python scripts/export_csv.py --date 2025-06-15这个命令会把指定日期的所有城市天气记录导出成一个 CSV 文件,列与数据库表字段一一对应,方便导入 Excel 或 Pandas。第一次跑全量数据时,建议先只导一天的数据检查字段内容,确认没有解析错位后再放开来跑全量。这里我有过一次教训:拿到新数据源后直接全量跑了一晚上,第二天发现温度和风向两列对调了,重新采集浪费了不少时间。从那以后,每次改完解析逻辑我都先跑一个最小城市集验证。
关于增量更新,项目的 scripts 目录里还有一个 update_daily.py 脚本,作用是把调度器接入 APScheduler,每天固定时间触发一次 crawl。实际使用时需要先确认数据源的更新时段,天气数据一般在凌晨更新当天预报,早上 7 点前采集是比较稳妥的窗口期。定时任务的参数要按目标站的更新规律来调,建议先观察一周更新时间再设置固定的调度时间,避免空跑。
4.3 避坑手记:5 个真实踩过的运行问题
先说第一个高频问题:启动后一个请求都没发出去,日志停在 start_requests 就没动静了。原因多半是 cities.json 的编码格式问题,文件被编辑器保存成了 UTF-8 with BOM,json.load 解析出的第一个城市 code 前面多了不可见字符,目标站返回 404 导致整个队列卡住。解决方法是把 cities.json 重新保存为无 BOM 的 UTF-8,或者在代码里用 utf-8-sig 编码打开文件,一劳永逸。
第二个问题是页面能打开但解析字段全为空。常见原因是目标站返回的不是完整 HTML,而是一段被压缩过的内容,XPath 匹配不到节点。现象是日志里出现 page structure changed 的警告。解决方法是先确认响应头里的 Content-Encoding,Scrapy 的 downloader middleware 通常会自动解压 gzip,但遇到 br 压缩格式时需要额外处理。最直接的排查动作是写个临时脚本,用 Requests 拉一次响应头看编码,再对照调整 settings 里的 headers 配置。
第三个问题与反爬限流有关:采集到一半开始大量超时,日志里频繁出现 ResponseNeverReceived。原因通常是目标站对单 IP 的请求频率设了阈值,触发后直接断开连接。解决方法是把 CONCURRENT_REQUESTS 从 8 降到 4,DOWNLOAD_DELAY 从 0.8 提到 2 秒以上。这类问题本质上不是代码 bug,是节奏问题,调参优先级始终高于改代码。有些超时现象看起来像玄学,换个时间段再跑就恢复了,实际还是频率问题。如果调参后仍然被限流,再考虑轮换 User-Agent,不建议一上来就上代理池,后者引入的变量更多。
第四个问题比较隐蔽:SQLite 数据库文件在采集过程中越来越大,但逻辑数据量没涨多少。原因是 WAL 模式下频繁的更新操作会产生大量碎片页,文件实际占用远大于有效数据量。解决方法是采集任务结束后执行一次 VACUUM 命令压缩文件。项目里可以在 export_csv.py 里加一个 --vacuum 参数,每次导出前自动执行,效果很明显。
第五个问题涉及数据校验:某城市在数据库里缺记录,第一反应是采集没爬到,实际很可能是清洗层按规则把记录丢弃了。排查时先看日志里的 DropItem 记录,再看 cleaner 的校验规则是否合理。项目里 logger.py 专门把 dropped items 写进单独的日志文件,就是为了让这类排查有迹可循。结合排查经验,我的习惯是每次跑完采集后先看一眼 dropped 日志,如果丢弃率异常升高,优先怀疑解析层而非清洗层。
5. 二次开发:把天气数据集改造成自己的数据服务
5.1 新增采集字段:以空气质量为例
加一个 AQI 字段需要走完整链路:解析层提取、清洗层校验、存储层建列。先分析目标页面上 AQI 数值所在的节点,然后在 parse_weather 里增加提取逻辑,接着在 cleaner 里增加范围校验,最后在建表语句中增加一列。
# 在 parse_weather 中追加 AQI 提取 "aqi": self._parse_int( weather_block.xpath( "./div[contains(@class, 'aqi')]/text()" ).get(default="") ),改造时最容易漏的是 init_db.py,很多人加了字段却忘记更新建表语句,导致新字段没有写入位置。我的习惯是每加一个字段就同步改三处:解析、清洗、建表。如果数据量后续上涨明显,storage pipeline 的 SQL 可以改成批量插入,把逐条 commit 改为攒批后一次性 commit,写入速度能提升一个量级。
5.2 把 SQLite 数据导出为 REST API
天气数据集更大的价值在于服务下游应用。可以基于 Flask 写一个最小的只读 API,让前端或定时任务通过 HTTP 获取数据:
# app.py 简易天气数据接口 from flask import Flask, jsonify, request import sqlite3 app = Flask(__name__) DB_PATH = "db/weather.db" @app.route("/api/weather") def get_weather(): city = request.args.get("city") date = request.args.get("date") conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute( """ SELECT * FROM daily_weather WHERE city_code = ? AND date = ? """, (city, date), ).fetchall() return jsonify([dict(row) for row in rows]) if __name__ == "__main__": app.run(port=8000)这个接口把数据库查询能力直接暴露给外部,city 参数传城市编码,date 参数传日期,返回该城市当天的天气记录。实际部署时可以改成支持日期区间查询,加一个分页参数,就能支撑前端图表展示的需求。写接口时用参数化查询避免 SQL 注入,另外要加一层访问控制,比如白名单 IP 或请求头校验,防止数据库文件被外部大量拉取。
运行这套采集脚本半年后,我最大的体会是数据采集项目的核心不是爬虫本身,而是变更管理:页面改版时怎么快速定位解析层,数据量增长时怎么平滑迁移存储层,新数据源接入时怎么复用清洗规则。从那以后我每次接手类似的采集工程,都会强制自己先画一遍请求到落库的数据流图,确认每一层的数据格式契约再动手改代码,这套项目最值得借鉴的正是这种拆分思路。希望帮到你。
本文还有配套的精品资源,点击获取