资讯动态

生产级气象数据采集系统:Python构建高可靠天气数据管道

发布时间:2026/10/21 14:11:58 来源:尧图企业网站定制
简介本资源是一套面向数据科学初学者与Python爬虫实践者的全国天气数据采集解决方案聚焦2011–2024年长期历史气象数据的获取与结构化处理。资源包含300个文件261个CSV格式城市天气数据表、29个Python脚本、6个XML配置/元数据文件、2个Jupyter Notebook分析示例总大小仅1.23MB轻量易部署其中CSV文件覆盖绵阳、上海、雅安、成都、北京等2290个地区字段涵盖气温、风向、生活指数及天气预警等实用维度Python脚本提供数据抓取逻辑、清洗流程与基础分析框架Notebook则演示典型可视化与时间序列探索路径。目前已有163人学习下载适合需快速构建气象分析基线、理解多源异构数据整合流程、或开展农业/交通领域小规模实证研究的学习者。1. 全国天气数据采集源代码不是“爬完就跑”的玩具项目而是能直接嵌入气象类业务系统的生产级数据管道某高校实验室做城市热岛效应建模时卡在了数据环节——第三方API调用频次受限、历史数据缺失、接口字段不一致最后靠人工导出Excel再清洗两周只凑齐3个城市的月度数据。他们后来换上了这个「全国天气数据采集源代码」包三天内拉齐了2023全年334个地级市的逐日气温、湿度、气压、风速、降水、能见度、紫外线强度共7类核心指标原始数据量达127万条全部存为结构化CSVSQLite双格式且自带时间对齐、异常值标记、空值插补逻辑。这不是一个教你怎么写requests.get()的入门Demo而是一套经过真实业务压力验证的采集-清洗-落库闭环它用动态UA池请求间隔抖动IP健康度反馈机制绕过基础反爬用pandasnumpy组合完成分钟级批量清洗非单行for循环用sqlite3WAL模式实现高并发写入不锁表更关键的是所有采集任务可配置化驱动——改一个JSON文件就能切到不同气象站点源、调整采集粒度逐日/逐小时/逐3小时、开关特定字段。如果你正被“数据拿不到”“拿到不能用”“用了不稳定”三座大山压着这个包就是你该拆开的第一块砖。2. 架构设计与模块职责从入口脚本到数据落地的五层流水线这个项目不是单个Python文件堆出来的而是按数据流严格分层的五层结构。我拆包后第一件事就是画清依赖链main.py调度中枢→collector/采集引擎→parser/解析器集群→cleaner/清洗流水线→storage/持久化层。每层都独立可测试且通过config.yaml统一注入参数避免硬编码污染。下面逐层拆解其设计意图和关键实现。2.1 主调度器main.py 的状态机式任务编排main.py不是简单循环调用采集函数而是实现了轻量状态机IDLE → FETCHING → PARSING → CLEANING → STORING → DONE。每个状态对应一个可插拔模块失败时自动回退并记录断点位置存于state_checkpoint.json下次启动直接从断点续采。这种设计让长周期采集如拉取十年历史数据不再怕中断。# main.py 核心调度逻辑节选 def run_pipeline(city_list: List[str], config: Dict): checkpoint load_checkpoint() # 加载上次断点 for city in city_list: if city in checkpoint.get(completed, []): continue # 已完成城市跳过 try: raw_data collector.fetch_weather_by_city(city, config) parsed parser.parse_raw_response(raw_data, city) cleaned cleaner.standardize(parsed, config[cleaning_rules]) storage.save_to_sqlite(cleaned, city) checkpoint[completed].append(city) save_checkpoint(checkpoint) except Exception as e: logger.error(fCity {city} failed: {str(e)}) checkpoint[failed].setdefault(city, []).append(str(e)) save_checkpoint(checkpoint)提示config[cleaning_rules]是一个嵌套字典定义了各字段的清洗规则例如temp: {min: -80, max: 60, method: interpolate}表示气温字段允许范围-80~60℃超限值用前后有效值线性插补。这个设计让清洗逻辑完全脱离代码运维人员改规则不用动Python。2.2 采集引擎collector/ 目录下的多源适配器模式collector/下不是一堆xxx_spider.py而是按数据源抽象出BaseCollector基类再派生出CMAWebCollector中国气象数据网、QWeatherCollector和风天气API、LocalStationCollector本地气象站HTTP接口三个子类。它们共享同一套请求中间件RetryMiddleware指数退避重试、RateLimiter动态QPS控制、UserAgentRotator从内置50个UA中随机轮换。关键在于所有子类必须实现fetch_weather_by_city()方法但内部实现自由——CMAWebCollector用requestsBeautifulSoup解析HTML表格QWeatherCollector用httpx调用REST APILocalStationCollector则用paramiko走SFTP拉取CSV。这种设计让新增数据源只需继承基类、实现一个方法无需动调度器。2.3 解析器集群parser/ 中的模板驱动式HTML解析parser/目录下没有if-else判断不同网站结构而是采用Jinja2模板驱动。每个数据源对应一个.j2模板文件例如cma_table.j2{# cma_table.j2 #} {% for row in table_rows %} { date: {{ row[0] | date_format(YYYY-MM-DD) }}, temp_max: {{ row[1] | float_or_none }}, temp_min: {{ row[2] | float_or_none }}, precipitation: {{ row[3] | float_or_none }}, wind_speed: {{ row[4] | float_or_none }} } {% endfor %}解析器加载模板后将原始HTML传入table_rows extract_table(html, selectortable.data-table tr)再用Jinja2渲染生成标准JSON列表。这样当CMA网站改版导致HTML结构调整时只需修改selector和模板字段索引无需重写Python解析逻辑——这是我在某次CMA页面重构后血泪验证过的方案。2.4 清洗流水线cleaner/ 的向量化操作而非逐行遍历cleaner/的核心是Standardizer类它不写for row in df.itertuples()而是用pandas原生向量化操作# cleaner/standardizer.py 关键清洗逻辑 def standardize(self, df: pd.DataFrame, rules: Dict) - pd.DataFrame: # 步骤1统一时间列格式向量化 df[date] pd.to_datetime(df[date], errorscoerce) # 步骤2批量字段校验向量化布尔索引 for field, rule in rules.items(): if min in rule and max in rule: mask (df[field] rule[min]) | (df[field] rule[max]) if rule[method] interpolate: df.loc[mask, field] np.nan df[field] df[field].interpolate(methodlinear) elif rule[method] drop: df df[~mask] # 步骤3缺失值填充向量化 df.fillna({ precipitation: 0.0, wind_speed: df[wind_speed].median() }, inplaceTrue) return df注意interpolate(methodlinear)对时间序列最友好它会按date索引顺序插值比fillna(methodffill)更符合气象数据规律。我曾因误用ffill导致某山区站点连续7天无降水记录被填成前一日值模型训练结果全偏移——这个坑得记牢。2.5 持久化层storage/ 的SQLite WAL模式高并发写入storage/用sqlite3而非MySQL/PostgreSQL不是因为轻量而是为离线部署和单机高性能。关键在开启WALWrite-Ahead Logging模式# storage/sqlite_writer.py def init_db(db_path: str): conn sqlite3.connect(db_path) conn.execute(PRAGMA journal_mode WAL;) # 启用WAL conn.execute(PRAGMA synchronous NORMAL;) # 平衡安全与速度 conn.execute( CREATE TABLE IF NOT EXISTS weather_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT NOT NULL, date DATE NOT NULL, temp_max REAL, temp_min REAL, precipitation REAL, wind_speed REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(city, date) -- 防止重复插入 ) ) return connWAL模式允许多个读进程同时进行写进程不阻塞读——这在批量清洗后集中写入时至关重要。实测10万条数据写入WAL比默认DELETE模式快3.2倍且不会因并发写入报database is locked错误。3. 运行全流程从环境准备到首次成功采集的七步实操别急着python main.py这个项目对运行环境有明确要求。我按新手第一次上手的真实路径把每一步命令、预期输出、常见卡点都列清楚。全程基于Ubuntu 22.04 Python 3.9Windows用户请将source venv/bin/activate换成venv\Scripts\activate.bat。3.1 环境初始化创建隔离虚拟环境并安装依赖项目依赖看似简单requests,pandas,beautifulsoup4,jinja2,pyyaml但版本冲突是高频翻车点。尤其pandas低于1.4会导致interpolate(methodtime)报错beautifulsoup4高于4.12可能解析CMA表格错位。所以必须用requirements.txt精确锁定# 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 安装指定版本依赖注意不要用pip install -r requirements.txt pip install pandas1.5.3 beautifulsoup44.11.2 jinja23.1.2 pyyaml6.0.1 requests2.28.2 # 验证安装应看到各包版本号 pip list | grep -E (pandas|bs4|jinja|PyYAML|requests)提示requirements.txt里没写版本号是作者疏忽我已根据git log回溯到2023年8月提交的setup.py确认上述版本组合最稳定。若跳过此步直接pip install -r requirements.txt大概率在parser阶段报AttributeError: Tag object has no attribute find_all——这是bs44.12废弃旧API导致的。3.2 配置文件定制config.yaml 的三项必改参数config.yaml是整个流程的总开关首次运行前必须修改以下三项其他参数可保持默认# config.yaml 关键配置段 collector: source: cma_web # 必改1数据源可选 cma_web / qweather_api / local_station max_retries: 3 # 建议保持3太多易触发风控 delay_range: [1.5, 3.0] # 请求间隔秒数范围防固定节奏被识别 cities: - beijing # 必改2城市编码用拼音小写支持334个地级市 - shanghai - guangzhou storage: db_path: ./data/weather.db # 必改3数据库路径确保data/目录存在 csv_output: true # 是否同时输出CSV调试时建议true注意城市编码必须用项目内置的city_codes.json里的标准码比如“深圳”是shenzhen而非sz或shenzhenshi。city_codes.json在resources/目录下打开即可查。输错会导致采集时返回404或空数据但日志里只报HTTP 404不提示城市码错误——这是新手最常踩的静默坑。3.3 数据目录预置手动创建data/并赋权项目默认将CSV和SQLite写入./data/但该目录不存在时程序不会自动创建而是直接报FileNotFoundError。必须手动创建并确保写权限mkdir -p data chmod 755 data # 验证ls -ld data 应显示 drwxr-xr-x3.4 首次采集执行带详细日志的后台运行用nohup后台运行并实时看日志避免SSH断连导致中断# 启动采集-u 强制未缓冲输出方便tail实时看 nohup python -u main.py logs/run.log 21 # 实时跟踪日志新开终端 tail -f logs/run.log预期日志流[INFO] Starting pipeline for beijing... [INFO] Fetching CMA data for beijing (2023-01-01 to 2023-12-31)... [INFO] Parsed 365 records from CMA HTML table [INFO] Cleaned data: dropped 2 outliers, interpolated 5 NaNs [INFO] Saved 365 records to SQLite (weather.db) [INFO] Pipeline completed for beijing若卡在Fetching...超2分钟大概率是网络或反爬触发见下一节排查。3.5 验证数据落库用sqlite3命令行快速核验别等全部城市跑完再验证先查第一个城市是否真入库# 进入SQLite命令行 sqlite3 data/weather.db # 查表结构确认字段名 .schema weather_daily # 查北京2023年1月1日数据应返回1行 SELECT * FROM weather_daily WHERE citybeijing AND date2023-01-01; # 退出 .quit提示若SELECT返回空先检查logs/run.log里是否有CMA Web returned empty table字样。这通常因CMA网站当日维护或IP被临时限制换时间重试即可不是代码问题。3.6 CSV同步验证检查data/下生成的CSV文件若config.yaml中storage.csv_output: true则data/下应有beijing_2023.csv等文件。用head看前几行是否结构正确head -n 5 data/beijing_2023.csv # 应输出类似 # city,date,temp_max,temp_min,precipitation,wind_speed # beijing,2023-01-01,2.5,-5.3,0.0,1.2 # beijing,2023-01-02,3.1,-4.8,0.0,1.5若CSV为空或只有表头说明cleaner阶段出错重点查logs/run.log中Cleaned data:行后的报错。3.7 日志分析技巧定位失败城市的三把钥匙当某个城市采集失败时logs/run.log里会有一段完整错误栈。我总结出三把快速定位钥匙找City [xxx] failed:—— 锁定失败城市名找Traceback (most recent call last):—— 错误栈起点看最后一行是什么异常找During handling of the above exception, another exception occurred:—— 若有这行说明是兜底异常要往上翻找第一个Traceback。例如City shenzhen failed: HTTPConnectionPool(hostdata.cma.cn, port80): Max retries exceeded...→ 这是网络层超时不是解析错应检查网络或调大delay_range。City chongqing failed: KeyError: temp_max→ 这是parser模板字段名和HTML实际内容不匹配需检查parser/cma_table.j2中row[1]是否真对应最高温。4. 常见问题排查五个真实翻车现场及根治方案这个项目在真实环境中跑过37个城市、11个月份的数据采集以下是高频、隐蔽、且文档没写的五大翻车点。每一条都是我亲手复现、定位、修复的血泪经验不是网上抄来的泛泛而谈。4.1 现象采集卡在Fetching...日志停住超过5分钟无任何报错原因CMA网站对IP有会话级限速连续请求超10次/分钟即触发503 Service Temporarily Unavailable但程序默认不捕获此HTTP状态码requests.get()抛出ConnectionError而非HTTPError导致异常被except Exception吞掉进入无限重试死循环。解决修改collector/base_collector.py在fetch_with_retry方法中显式检查响应状态# 在 requests.get() 调用后添加 if response.status_code 503: logger.warning(f503 received, sleeping 60s before retry...) time.sleep(60) # 强制休眠1分钟 raise ConnectionError(CMA 503 rate limit)然后在RetryMiddleware中增加对ConnectionError的重试逻辑。实测加此补丁后单IP可持续采集200城市无中断。4.2 现象CSV文件生成但SQLite中无数据weather_daily表为空原因storage/sqlite_writer.py中save_to_sqlite()函数使用executemany()批量插入但当df.to_dict(records)生成的字典列表中某条记录缺失city或date字段时SQLite的INSERT INTO ... VALUES (?, ?, ...)会因占位符数量不匹配直接报sqlite3.ProgrammingError而该异常未被try-except捕获导致后续插入全部跳过。解决在save_to_sqlite()开头增加字段校验# 检查每条记录是否含必需字段 required_fields [city, date] for i, record in enumerate(records): missing [f for f in required_fields if f not in record or not record[f]] if missing: logger.error(fRecord {i} missing fields {missing}: {record}) records.pop(i) # 移除问题记录 i - 1并确保logger.error后继续处理剩余记录而非return中断。4.3 现象parser阶段报UnicodeDecodeError: utf-8 codec cant decode byte 0xe8原因CMA部分页面用GBK编码但requests.get()默认按ISO-8859-1解码响应体response.text乱码导致BeautifulSoup解析失败。解决强制指定编码在collector/cma_web.py的fetch_weather_by_city()中response session.get(url, timeout30) response.encoding gbk # 关键必须设为gbk soup BeautifulSoup(response.text, html.parser)4.4 现象cleaner阶段interpolate()后出现NaN值未被填充最终写入SQLite为NULL原因pandas.interpolate()对非数值列如city、date会静默跳过但若date列因格式错误被转为object类型则interpolate()不生效后续fillna()也无效。解决在Standardizer.standardize()开头强制转换# 确保date列为datetime64 if not np.issubdtype(df[date].dtype, np.datetime64): df[date] pd.to_datetime(df[date], errorscoerce) # 确保数值列为float64 numeric_cols [temp_max, temp_min, precipitation, wind_speed] for col in numeric_cols: if col in df.columns: df[col] pd.to_numeric(df[col], errorscoerce)4.5 现象多城市并发采集时SQLite报database is locked原因虽然启用了WAL但storage/sqlite_writer.py中每个save_to_sqlite()都新建sqlite3.connect()而SQLite的WAL模式要求同一个连接对象复用否则仍会锁表。解决将数据库连接改为单例模式在storage/__init__.py中import sqlite3 _conn None def get_db_connection(db_path: str): global _conn if _conn is None: _conn sqlite3.connect(db_path) _conn.execute(PRAGMA journal_mode WAL;) return _conn然后所有save_to_sqlite()调用get_db_connection()获取连接不再自己connect()。5. 二次开发实战如何接入新数据源以和风天气API为例当你需要把项目从CMA网页抓取升级为调用和风天气APIQWeather时不是重写整个采集链而是精准替换collector/和parser/两个模块并微调配置。下面是以最小改动接入QWeather的完整路径包含密钥管理、字段映射、错误重试三重实战细节。5.1 新增采集器QWeatherCollector 继承 BaseCollector在collector/下新建qweather_api.py继承BaseCollector并实现fetch_weather_by_city()# collector/qweather_api.py import httpx from collector.base_collector import BaseCollector class QWeatherCollector(BaseCollector): def __init__(self, config: dict): super().__init__(config) self.api_key config.get(qweather_api_key, ) # 从config读密钥 self.base_url https://devapi.qweather.com/v7/weather/30d # 30天预报 def fetch_weather_by_city(self, city_code: str, config: dict) - dict: # QWeather城市编码需转换CMA拼音 → QWeather ID qweather_id self._map_city_code(city_code) # 映射逻辑见下文 params { location: qweather_id, key: self.api_key, gzip: true } # 使用httpx比requests更轻量支持异步 with httpx.Client(timeout30) as client: response client.get(self.base_url, paramsparams) response.raise_for_status() # 自动抛HTTPError return response.json() def _map_city_code(self, cma_code: str) - str: CMA城市码到QWeather城市ID的映射表 mapping { beijing: 101010100, shanghai: 101020100, guangzhou: 101280101, # ... 全部334个映射从QWeather官网下载CSV生成 } return mapping.get(cma_code, )注意_map_city_code()的映射表必须完整QWeather的location参数是数字ID不是城市名。我从QWeather开放平台下载了china-city-list.csv用Python脚本自动生成了334行映射字典避免手敲出错。5.2 新增解析器qweather_parser.py 的JSON路径提取QWeather返回的是标准JSON无需HTML解析新建parser/qweather_parser.py# parser/qweather_parser.py import json from typing import List, Dict def parse_qweather_response(data: dict, city: str) - List[Dict]: 将QWeather 30天预报JSON转为标准字典列表 records [] for day in data.get(daily, []): records.append({ city: city, date: day[fxDate], temp_max: float(day[tempMax]), temp_min: float(day[tempMin]), precipitation: float(day[precip]), # QWeather叫precip非precipitation wind_speed: float(day[windSpeedDay]) # 白天风速 }) return records提示QWeather字段名和CMA不一致如precipvsprecipitationparse_qweather_response()负责统一为项目标准字段名这样cleaner和storage层完全不用改。5.3 配置驱动切换修改 config.yaml 启用新源只需三处修改无需动一行主逻辑代码# config.yaml collector: source: qweather_api # 切换数据源 qweather_api_key: YOUR_QWEATHER_API_KEY_HERE # 新增密钥项 max_retries: 5 # API调用建议重试5次 cities: - beijing - shanghai # 城市码保持不变_map_city_code()自动转换 storage: db_path: ./data/weather_qweather.db # 建议新库避免混用5.4 密钥安全实践绝不硬编码用环境变量接管把qweather_api_key写在config.yaml里不安全应改用环境变量。修改main.py加载配置逻辑# main.py 中 config 加载部分 import os import yaml def load_config() - dict: with open(config.yaml) as f: config yaml.safe_load(f) # 从环境变量覆盖密钥 if QWEATHER_API_KEY in os.environ: config[collector][qweather_api_key] os.environ[QWEATHER_API_KEY] return config然后运行时export QWEATHER_API_KEYyour_actual_key_here python main.py注意QWeather免费版有1000次/日调用限制若采集334个城市每天最多跑3轮。我在collector/qweather_api.py的fetch_weather_by_city()里加了计数器和time.sleep(0.1)确保不超限。这个细节不写进文档但实际部署必须加。5.5 字段扩展如何新增“紫外线强度”字段而不破坏现有流程假设QWeather返回uv字段而CMA不提供你想在数据库里新增uv_index列并存入。三步走改SQLite表结构用ALTER TABLEALTER TABLE weather_daily ADD COLUMN uv_index REAL;改cleaner规则在config.yaml中cleaning_rules: uv_index: min: 0 max: 15 method: interpolate改parser逻辑在qweather_parser.py中# parser/qweather_parser.py records.append({ # ... 其他字段 uv_index: float(day[uv]) # QWeather的uv字段 })cleaner和storage层自动识别新字段并按规则处理无需修改。这就是良好分层设计的威力——新增字段只改三处且都在各自职责域内。从那以后我每次接入新数据源都强制走一遍“继承Collector→写Parser→配YAML→环境变量密钥”四步 checklist再没因漏改某处导致半夜告警。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑