
简介本资源是一套面向数据科学初学者与Python爬虫实践者的全国天气数据采集解决方案聚焦2011–2024年长期历史气象数据的获取与结构化处理。资源包含300个文件261个CSV格式城市天气数据表、29个Python脚本用于数据抓取与清洗、2个Jupyter Notebook提供分析示例、6个XML配置及元信息文件压缩包仅1.23MB轻量易部署适配本地快速复现与教学演示。已有163人学习下载覆盖农业气候分析、城市热岛研究、时序预测建模等实际场景。用户可直接调用已封装的爬虫模块含地区-时间参数化设计、加载2290个区县中部分代表性城市如北京、上海、成都、武汉等的完整字段天气记录并基于附带的数据处理脚本完成缺失值填充、指数计算与格式标准化显著降低从零构建气象数据集的技术门槛。1. 全国天气数据采集源代码不是“一键爬完就跑”而是能稳定跑满365天的生产级采集链路你是不是也试过用几行 requests BeautifulSoup 抓个天气网站跑两天就 403改个 User-Agent 又撑三天最后发现页面结构月更、反爬策略周变、IP 封禁无日志、数据字段错位不报错这不是你代码写得差是绝大多数公开“天气爬虫教程”根本没碰过真实场景——它默认你只采一天、只跑一台机器、不关心字段一致性、不验证数据时效性、不处理断点续采。而这份「全国天气数据采集源代码」是我去年在某高校气象数据中台项目里实际落地的完整采集系统覆盖全国2800区县支持按行政区划/经纬度双模式调度内置动态 UA 池、代理轮转适配器、HTML 结构容错解析器、JSON Schema 校验层以及最关键的——带时间戳快照的增量采集状态机。它不承诺“全自动零维护”但把所有已知翻车点都做成可配置开关和可观测日志。适合需要长期、合规、可审计获取气象基础数据的开发者、地信专业学生、环境建模研究者而不是想凑个 demo 交作业的人。2. 为什么选这个架构不是为炫技是为扛住真实天气网站的三重绞杀天气类网站的反爬强度在垂直领域里属于“中高危”梯队它不像电商有强登录态也不像学术库有 API 限流而是靠“页面结构模糊化 静态资源混淆 行为指纹轻量检测”组合拳。我见过太多人栽在同一个地方以为抓到div classweather-temp25℃/div就万事大吉结果下个月 class 名变成temp-val-2再下个月温度值被拆成span2/spanspan5/spanspan℃/span。所以本采集系统没走“硬解析 HTML 树”的老路而是分三层解耦2.1 数据源抽象层把“网站”变成“协议契约”核心不是写死某个网址而是定义WeatherDataSource接口from abc import ABC, abstractmethod from typing import Dict, List, Optional class WeatherDataSource(ABC): abstractmethod def get_city_list(self) - List[Dict[str, str]]: 返回标准城市编码表{ code: 101010100, name: 北京, province: 北京 } pass abstractmethod def fetch_daily_forecast(self, city_code: str, date: str) - Optional[Dict]: 按城市编码日期拉取单日预报返回标准化字典 pass abstractmethod def get_supported_dates(self) - List[str]: 返回该源支持的历史/未来日期范围如 [2023-01-01, 2023-01-02] pass当前实现包含三个具体源CNWeatherGovSource: 对接某国家级气象服务中心公开页面非 API纯 HTMLThirdPartyWeatherSource: 适配某商业天气服务的公开前端接口带 Referer 和简单 tokenMockLocalSource: 本地 JSON 文件模拟器用于离线调试和单元测试提示所有源都强制实现get_supported_dates()这是避免“爬到不存在的日期页导致全链路中断”的第一道保险。你不需要知道它怎么算的但必须信任它返回的日期列表是安全的。2.2 解析引擎层用 CSS 选择器 正则兜底拒绝 XPath 玄学很多人一上来就写response.xpath(//div[classtemp]/text())结果网站把 class 改成temp-value就全崩。我们换思路先定位容器区块再从区块内提取目标字段。以某.gov 站点为例其每日预报结构是div classforecast-day>from bs4 import BeautifulSoup import re def parse_gov_forecast(html: str, target_date: str) - dict: soup BeautifulSoup(html, lxml) # 第一步用># 创建虚拟环境推荐 python -m venv weather_env source weather_env/bin/activate # Linux/macOS # weather_env\Scripts\activate # Windows # 安装核心包无冗余依赖 pip install requests beautifulsoup4 lxml pandas apscheduler pyyaml依赖说明requests: 发起 HTTP 请求配合urllib3自动处理连接池复用beautifulsoup4 lxml: 解析 HTMLlxml比html.parser快 3 倍且容错更强pandas: 最终数据清洗与导出支持直接to_csv()和to_parquet()apscheduler: 定时任务调度比cron更 Pythonic支持内存/数据库后端pyyaml: 读取配置文件比 JSON 更易写注释、支持锚点复用。注意不安装 selenium、playwright 或任何浏览器自动化工具。本系统坚持“纯 HTTP 解析”路线因为真实天气站极少需要 JS 渲染且浏览器方案在服务器上资源消耗大、稳定性差、日志难追踪。3.2 配置你的采集策略一份 YAML 说清所有事编辑config.yaml这是你控制全局行为的唯一入口# config.yaml 采集源配置: active_source: CNWeatherGovSource # 可选: CNWeatherGovSource, ThirdPartyWeatherSource, MockLocalSource proxy_enabled: true # 是否启用代理需配合下方 proxy_pool retry_times: 3 # 单请求最大重试次数 timeout: 15 # 请求超时秒数 代理池配置: type: file # 可选: file, api file_path: ./proxies.txt # 当 typefile 时每行一个 proxy: http://user:passhost:port api_url: # 当 typeapi 时调用此 URL 获取代理返回 JSON: {proxy: http://...}) 调度策略: mode: incremental # 可选: incremental增量, full全量 start_date: 2024-03-01 # 增量模式下从该日期开始采集 end_date: 2024-03-05 # 增量模式下采集到该日期含 cities: # 指定城市列表留空则采集全部 - code: 101010100 name: 北京 - code: 101020100 name: 上海 输出配置: output_format: csv # 可选: csv, parquet, jsonl output_dir: ./output daily_file: true # true: 每天一个文件false: 所有数据一个文件 include_metadata: true # 是否在 CSV 中添加 source、fetch_time 等元数据参数说明mode: incremental是默认且推荐模式它会读取state.db中每个城市的next_fetch_date然后按配置的start_date/end_date截断确保不越界cities字段留空时系统自动调用get_city_list()加载全部城市但首次全量采集建议先注释掉用 2~3 个城市测试通路daily_file: true生成./output/2024-03-01.csv这样的文件方便后续按日期切片分析也利于增量备份。3.3 启动采集一条命令后台静默运行# 方式一前台运行适合调试看实时日志 python run.py --config config.yaml # 方式二后台守护进程生产推荐 nohup python run.py --config config.yaml --daemon run.log 21 run.py主逻辑精简如下# run.py 核心片段 if __name__ __main__: args parse_args() config load_config(args.config) # 初始化采集器注入源、解析器、状态库 collector WeatherCollector( source_classget_source_class(config[采集源配置][active_source]), parserparse_gov_forecast if config[采集源配置][active_source] CNWeatherGovSource else parse_third_party, state_dbWeatherStateDB(state.db) ) # 构建任务队列 tasks collector.build_task_queue( citiesconfig[调度策略].get(cities, []), start_dateconfig[调度策略][start_date], end_dateconfig[调度策略][end_date], modeconfig[调度策略][mode] ) # 执行采集支持并发但默认单线程保稳定 results collector.run_batch(tasks, max_workers1) # 导出结果 exporter DataExporter(config[输出配置]) exporter.export(results) print(f✅ 采集完成共处理 {len(results)} 条记录输出至 {config[输出配置][output_dir]})执行后你会看到类似日志[2024-03-15 10:22:03] INFO Starting incremental collection for 101010100 (北京) [2024-03-15 10:22:05] DEBUG Fetched HTML for 101010100 on 2024-03-01 (2042 bytes) [2024-03-15 10:22:05] DEBUG Parsed: {date: 2024-03-01, weather: 晴, high_temp: 18, low_temp: 5} [2024-03-15 10:22:05] INFO ✅ Saved 101010100_2024-03-01.csv3.4 验证输出用 pandas 三行代码确认数据质量采集完成后进入./output目录用以下代码快速检查import pandas as pd # 读取最新一天的 CSV df pd.read_csv(./output/2024-03-01.csv) # 检查关键字段是否齐全且无空值 required_cols [city_code, city_name, date, weather, high_temp, low_temp] print(缺失字段:, [col for col in required_cols if col not in df.columns]) print(空值统计:\n, df[required_cols].isnull().sum()) # 检查温度是否为数值类型 print(high_temp 类型:, df[high_temp].dtype) print(异常高温样本45℃:, df[df[high_temp] 45][[city_name, high_temp]])预期输出缺失字段: [] 空值统计: city_code 0 city_name 0 date 0 weather 0 high_temp 0 low_temp 0 dtype: int64 high_temp 类型: int64 异常高温样本45℃: Empty DataFrame如果看到high_temp是object类型或存在大量NaN说明解析器没正确提取数字需回查parse_gov_forecast()中的正则表达式是否匹配当前页面结构。4. 避坑指南那些让我凌晨三点改代码的真实翻车现场别信“一次配置永久运行”。天气网站的变动是常态下面这些坑都是我在某跨平台气象系统上线前两周内踩出来的血泪经验每一条都附带可复现现象、根因和修复动作。4.1 现象采集突然卡在某个城市日志停在Fetching...再无响应原因目标网站对单 IP 的并发连接数做了限制非封禁requests.Session()默认复用连接但当多个请求排队等待同一个 TCP 连接释放时会无限期 hang 住。解决在WeatherCollector.fetch_page()中显式设置timeout并禁用连接池复用# 错误写法复用连接易卡死 response session.get(url, headersheaders) # 正确写法每次新建连接加硬超时 response requests.get( url, headersheaders, timeout(3.05, 27), # connect_timeout3.05s, read_timeout27s3.05是经典防卡死值 streamFalse )提示connect_timeout设为 3.05 秒是行业惯例能有效规避 SYN Flood 检测read_timeout设为 27 秒非整数可绕过某些服务器的超时检测逻辑。4.2 现象CSV 里出现大量12℃ ~ ℃或℃ ~ 24℃高低温只有一个数字原因网站把温度范围拆成两个独立span但我们的正则r(\d)℃\s*~\s*(\d)℃只匹配两个数字都存在的场景。当某天只报高温如“24℃”或只报低温如“5℃”时正则完全失效。解决重构温度提取逻辑分三步先找所有含℃的文本节点对每个节点用r(\d)℃提取数字按位置左/右或上下文如含“最高”、“最低”字样归类。def extract_temps_from_block(block): all_temp_spans block.find_all(stringre.compile(r\d℃)) temps [int(re.search(r(\d)℃, s).group(1)) for s in all_temp_spans if re.search(r(\d)℃, s)] # 简单启发式取最大值为高温最小值为低温多数情况成立 return max(temps) if temps else None, min(temps) if temps else None4.3 现象state.db里error_count突然暴涨到 100但日志里没报错原因SQLite 在 NFS 或某些云盘上不支持 WAL 模式UPDATE语句在并发写入时触发锁等待最终超时抛OperationalError: database is locked但错误被上层try/except吞掉只默默累加error_count。解决强制 SQLite 使用journal_modeDELETE并关闭synchronousNORMAL# 在 WeatherStateDB.__init__() 中 self.conn sqlite3.connect(state.db, check_same_threadFalse) self.conn.execute(PRAGMA journal_mode DELETE) self.conn.execute(PRAGMA synchronous NORMAL) self.conn.execute(PRAGMA cache_size 10000)注意synchronousNORMAL是安全的它只牺牲毫秒级崩溃恢复能力换取 3 倍写入速度对采集系统完全可接受。4.4 现象用ThirdPartyWeatherSource时所有请求返回 403但浏览器能正常访问原因该商业源校验Referer头且要求必须是其官网域名如https://weather.example.com/而我们发请求时Referer是空或http://localhost。解决在ThirdPartyWeatherSource.fetch_daily_forecast()中硬编码 Refererheaders { User-Agent: self._get_ua(), Referer: https://weather.example.com/forecast/101010100.html, # 必须是真实路径 X-Requested-With: XMLHttpRequest }同时在config.yaml中增加referer_template字段让 Referer 可配置避免硬编码。4.5 现象MockLocalSource测试通过但切到CNWeatherGovSource后解析全错原因Mock 数据用的是理想化 JSON而真实 HTML 包含大量无意义空格、换行、注释节点BeautifulSoup默认解析会把它们当作NavigableString导致find()找不到目标元素。解决初始化BeautifulSoup时指定formatterNone并预处理 HTML# 预处理移除注释和多余空白 def clean_html(html: str) - str: html re.sub(r!--.*?--, , html, flagsre.DOTALL) # 移除注释 html re.sub(r\s, , html) # 多空格变单空格 return html.strip() soup BeautifulSoup(clean_html(html), lxml, formatterNone)5. 进阶技巧用 SQL 直查 state.db把“采集健康度”变成可量化指标当你把采集系统跑上一周state.db就不再是个状态记录器而是一份活的“采集健康度仪表盘”。SQLite 原生支持复杂查询无需导出就能回答关键问题。以下是我每天晨会必跑的三条 SQL它们直接决定当天要不要介入。5.1 查看“最脆弱城市”哪些城市连续失败最多SELECT city_code, city_name, error_count, last_success_date, julianday(now) - julianday(last_success_date) AS days_since_last_success FROM city_state WHERE status error AND error_count 3 ORDER BY error_count DESC, days_since_last_success DESC LIMIT 10;这张表告诉我error_count 3是熔断阈值系统会自动暂停这些城市days_since_last_success超过 7 天说明该城市源可能已下线或改版需人工核查如果同一省份多个城市上榜大概率是省级页面结构变更不是单个城市问题。5.2 统计“采集时效性”昨天该采的城市今天完成率多少-- 计算昨日2024-03-14应采但未完成的城市数 SELECT (SELECT COUNT(*) FROM city_state WHERE next_fetch_date 2024-03-14) AS should_collect, (SELECT COUNT(*) FROM city_state WHERE last_success_date 2024-03-14) AS actually_collected, ROUND( 100.0 * (SELECT COUNT(*) FROM city_state WHERE last_success_date 2024-03-14) / NULLIF((SELECT COUNT(*) FROM city_state WHERE next_fetch_date 2024-03-14), 0), 2 ) AS completion_rate_percent;这个查询的结果是硬指标completion_rate_percent 95%触发告警检查网络、代理池、源站可用性should_collect为 0说明next_fetch_date没推进可能是状态库写入失败或日期逻辑 bugactually_collected为 0 但should_collect很大典型代理全挂或 UA 被集体封禁。5.3 定位“解析器瓶颈”哪个城市解析耗时最长-- 假设你在采集日志里记录了每个城市的解析耗时单位毫秒存于 log_table SELECT city_code, city_name, AVG(parse_time_ms) as avg_parse_time, COUNT(*) as sample_count FROM log_table WHERE date(fetch_time) 2024-03-14 GROUP BY city_code, city_name HAVING sample_count 5 -- 过滤掉采样少的噪声 ORDER BY avg_parse_time DESC LIMIT 5;这条 SQL 揭示的是“隐性技术债”如果某城市avg_parse_time 500ms说明它的 HTML 结构特别混乱比如嵌套 10 层 div解析器正则在反复回溯此时不应优化正则而应为该城市单独写一个parse_special_city_xxx()函数用更暴力的字符串切片替代通用解析sample_count少但avg_parse_time高往往是该城市页面加载慢网络抖动不是解析问题。从那以后我每次上线新解析规则都强制走一遍这三条 SQL —— 不是为炫技是让“数据采集”这件事从玄学变成可测量、可归因、可改进的工程实践。希望帮到你。本文还有配套的精品资源点击获取