简介本资源面向气象数据处理与GIS分析人员提供中国地面气候资料日值数据集V3.0的配套处理工具及全国气象站点矢量数据。处理软件基于C#编写需VS2019及以上环境当前版本支持气温与降水量数据的月、年平均或总量统计源码开放便于二次开发与功能扩展站点矢量数据则覆盖数据集全部站点可直接用于空间制图与位置分析。压缩包共41个文件约239KB包含cs源码、sln解决方案、exe可执行程序、config配置、shp/shx/dbf/prj等Shapefile组件、sqlite数据库及docx使用说明兼顾编译运行与直接使用。已有1056人学习下载。使用前需注意输入路径设置若不想修改代码可将站点数据文件夹命名为《2014-2019》或《2000-2018》并置于指定目录具体操作参见使用说明文档适合具备一定C#或GIS基础的用户快速上手。1. 从一份日值表到一张站点图气象数据处理链路到底卡在哪如果你手头拿到过中国地面气候资料日值数据集V3.0的原始压缩包第一反应大概率不是兴奋而是懵。一个年份目录下动辄几百个站点文本每个文件里是按月排列的宽表列头还带着台站号、纬度、经度、海拔和一堆气象要素缺测值用一串固定数字占位。想把它们变成能画图、能入库、能做空间插值的干净数据中间隔着格式解析、质量控制、站点匹配三道坎。这篇笔记就围绕这条链路展开用一套需要 VS2019 编译的桌面处理软件把 V3.0 日值数据读进来、清洗掉、导出成规整表格再和全国气象站点矢量数据做空间关联最终得到一份能直接进 GIS 或数据库的成果。适合手里有原始数据、想自己搭一套可复现流程的气象、农业、水文方向从业者也适合需要把站点观测落到地图上的数据分析同学。2. 先搞清楚 V3.0 日值文件和站点矢量的数据结构2.1 日值数据集的文件组织与字段含义中国地面气候资料日值数据集V3.0的常见分发形式是按年份分目录目录内每个站点一个文本文件文件名通常就是区站号。文件内部不是标准 CSV而是带说明行的定宽或逗号分隔文本前几行是站号、经纬度、海拔、数据年份等元信息之后才是逐日记录。逐日记录里一般包含平均气温、最高最低气温、平均水汽压、平均相对湿度、降水量、平均风速、日照时数等要素每个要素还带一个质量控制码字段。缺测值在不同要素上可能用不同数字表示比如气温常用 999999降水常用 32700 这类约定值处理前必须先把这些占位符识别出来否则后面算平均值会被拉爆。理解字段含义比写代码更重要。举个例子降水量字段里 32700 通常表示“微量降水”和真正的缺测不是一回事如果统一当缺测丢掉统计降水日数就会偏少。再比如质量控制码0 表示正确其他值代表可疑、错误或修改过做科研级分析时一般只保留 0 和部分可接受码。这些规则在数据集附带的说明文档里有但很多人拿到数据直接跳过文档后面发现结果对不上再回头翻血泪经验就是先读说明再动手。2.2 全国气象站点矢量数据的属性与坐标全国气象站点矢量数据一般是点图层常见格式是 Shapefile 或 GeoJSON属性表里至少包含区站号、站名、经度、纬度、海拔、省份等字段。它的价值在于把纯文本的站点观测变成带空间位置的对象方便做地图展示、区域统计和空间插值。需要注意的是站点矢量里的区站号编码规则要和日值文件里的站号对得上有些版本会带前导零有些会去掉直接按字符串匹配容易漏。坐标系也要确认常见是 WGS84 地理坐标如果后续要算距离或面积得先投影到合适的投影坐标系不然按经纬度算出来的距离单位是度没有物理意义。站点矢量还有一个隐藏问题同一站号在不同年份可能发生迁站经纬度有微小变化。如果做长时间序列分析最好按年份分别匹配或者至少记录下迁站信息别拿一套固定坐标套几十年数据。2.3 为什么选桌面软件而不是纯脚本纯 Python 或 R 脚本当然能处理 V3.0但为什么标题里强调一套需要 VS2019 的桌面处理软件实际场景里很多气象业务单位的内网环境不允许随意装 Python 包或者数据量到了几十 GB 级别脚本的内存管理和解析速度会成为瓶颈。用 C 配合 VS2019 编译的桌面工具优势在于解析定宽文本快、内存可控、能打包成单个 exe 发给同事直接用不需要对方配环境。而且 VS2019 对 C17 支持完整文件流和字符串处理性能足够调试也方便。如果你所在团队有类似约束走这条路是合理的如果纯研究环境脚本也够用但下面的解析逻辑和参数思路是通用的。3. 用 VS2019 搭起处理软件的最小可运行框架3.1 工程创建与依赖配置打开 VS2019新建一个 C 控制台项目或者用空项目手动加文件。项目属性里把 C 语言标准设为 ISO C17字符集用 Unicode 或多字节都行但读中文路径建议用宽字符或多字节并处理好编码。如果要用到文件系统遍历直接包含filesystem即可VS2019 原生支持。不需要额外第三方库就能完成文本解析和 CSV 导出如果后面要写 Shapefile可以引入 GDAL但为了最小可运行先不引入把站点矢量当成 CSV 读进来做匹配。// pch.h 或直接在主文件包含 #include iostream #include fstream #include sstream #include string #include vector #include filesystem #include map #include algorithm namespace fs std::filesystem;这段头文件覆盖了文件遍历、字符串流解析、容器存储和排序。filesystem用来递归扫描年份目录sstream用来按分隔符切分每行map用来按站号索引站点属性。逻辑上先把依赖收窄避免一上来就引入重型库导致编译配置复杂。3.2 读取单个站点日值文件并解析元信息V3.0 单个站点文件的前若干行是元信息常见格式是“站号 纬度 经度 海拔 年份”或者带标签的键值对。不同分发批次可能有差异所以解析时要容错先读前 10 行找到包含站号和经纬度的行提取数字。下面是一个简化但可用的解析函数。struct StationMeta { std::string stationId; double lat 0.0; double lon 0.0; double elev 0.0; int year 0; }; StationMeta parseMeta(const std::string filePath) { StationMeta meta; std::ifstream fin(filePath); if (!fin.is_open()) return meta; std::string line; int lineCount 0; while (std::getline(fin, line) lineCount 10) { lineCount; // 跳过空行和纯说明行 if (line.empty() || line.find(站号) std::string::npos) continue; std::istringstream iss(line); // 假设格式站号 纬度 经度 海拔 年份 iss meta.stationId meta.lat meta.lon meta.elev meta.year; break; } return meta; }这里的关键参数是lineCount 10因为元信息一般在前几行但不同版本可能多几行说明给 10 行余量足够。iss 按空白切分如果实际文件用逗号分隔需要先替换或改用getline(iss, token, ,)。解析失败时返回默认值后续通过站号是否为空来判断是否跳过。注意站号可能带字母或前导零所以用std::string而不是整数。3.3 逐日记录解析与缺测值处理逐日记录通常每行一天字段顺序固定。假设列顺序是日期、平均气温、最高气温、最低气温、降水量、平均风速、日照时数每个要素后面跟一个质控码。解析时按位置取值遇到缺测值就标记为无效。struct DailyRecord { std::string date; double avgTemp 0.0; double precip 0.0; double windSpeed 0.0; double sunshine 0.0; bool valid true; }; const double MISSING_TEMP 999999.0; const double MISSING_PRECIP 32700.0; DailyRecord parseDaily(const std::string line) { DailyRecord rec; std::istringstream iss(line); std::string token; std::vectorstd::string fields; while (std::getline(iss, token, ,)) { fields.push_back(token); } if (fields.size() 8) { rec.valid false; return rec; } rec.date fields[0]; double t std::stod(fields[1]); double p std::stod(fields[4]); if (t MISSING_TEMP || p MISSING_PRECIP) { rec.valid false; return rec; } rec.avgTemp t; rec.precip p; rec.windSpeed std::stod(fields[5]); rec.sunshine std::stod(fields[6]); return rec; }缺测值判断放在赋值之前一旦命中就整条记录标记无效。实际业务里可能希望部分要素有效时仍保留那就把valid拆成每个要素一个标志位。std::stod在遇到非数字时会抛异常生产代码里应该用try/catch包住或者先判断字符串是否全为数字。这里为了简洁没加但你在自己工程里一定要补上否则一个脏行就能让程序崩掉。3.4 批量遍历年份目录并导出 CSV把单文件解析串起来用fs::recursive_directory_iterator遍历根目录对每个.txt文件调用解析把有效记录写入一个总 CSV。为了避免内存爆掉采用边读边写的方式。void processAll(const std::string rootDir, const std::string outCsv) { std::ofstream fout(outCsv); fout stationId,lat,lon,elev,date,avgTemp,precip,windSpeed,sunshine\n; for (const auto entry : fs::recursive_directory_iterator(rootDir)) { if (entry.path().extension() ! .txt) continue; StationMeta meta parseMeta(entry.path().string()); if (meta.stationId.empty()) continue; std::ifstream fin(entry.path()); std::string line; bool dataStart false; while (std::getline(fin, line)) { if (!dataStart) { if (line.find(日期) ! std::string::npos) dataStart true; continue; } DailyRecord rec parseDaily(line); if (!rec.valid) continue; fout meta.stationId , meta.lat , meta.lon , meta.elev , rec.date , rec.avgTemp , rec.precip , rec.windSpeed , rec.sunshine \n; } } }dataStart标志用来跳过表头之前的说明行找到包含“日期”的行后才开始解析数据。输出 CSV 用逗号分隔字段顺序和表头一致。如果数据量特别大可以按年份或按站点分文件输出避免单个 CSV 过大导致后续打开困难。这个函数就是整个软件的核心编译成 exe 后命令行传入根目录和输出路径即可运行。4. 把站点矢量接进来匹配、校验与空间落图4.1 读取站点矢量属性并建立站号索引站点矢量如果是 Shapefile用 GDAL 读最稳如果只是临时用可以先导出成 CSV用标准库读。假设已经有一份stations.csv包含stationId,stationName,lat,lon,province读进来建一个mapstring, StationInfo。struct StationInfo { std::string name; double lat 0.0; double lon 0.0; std::string province; }; std::mapstd::string, StationInfo loadStations(const std::string csvPath) { std::mapstd::string, StationInfo stations; std::ifstream fin(csvPath); std::string line; std::getline(fin, line); // 跳过表头 while (std::getline(fin, line)) { std::istringstream iss(line); std::string token; std::vectorstd::string fields; while (std::getline(iss, token, ,)) fields.push_back(token); if (fields.size() 5) continue; StationInfo info; info.name fields[1]; info.lat std::stod(fields[2]); info.lon std::stod(fields[3]); info.province fields[4]; stations[fields[0]] info; } return stations; }用map而不是unordered_map是因为站号数量通常几千到几万map的有序性在调试时方便查看性能差异可以忽略。站号作为 key 必须完全一致如果日值文件里的站号有前导零而矢量里没有需要统一格式化比如都补齐到 5 位。4.2 站号匹配与经纬度偏差检查匹配时遍历日值 CSV 的每一行用站号去stations里查。如果查不到记录下来如果查到比较日值文件里的经纬度和矢量里的经纬度偏差超过阈值就告警。void matchAndCheck(const std::string dailyCsv, const std::string stationCsv) { auto stations loadStations(stationCsv); std::ifstream fin(dailyCsv); std::string line; std::getline(fin, line); int matched 0, unmatched 0, coordWarn 0; while (std::getline(fin, line)) { std::istringstream iss(line); std::string token; std::vectorstd::string fields; while (std::getline(iss, token, ,)) fields.push_back(token); if (fields.size() 5) continue; std::string sid fields[0]; auto it stations.find(sid); if (it stations.end()) { unmatched; continue; } matched; double lat std::stod(fields[1]); double lon std::stod(fields[2]); if (std::abs(lat - it-second.lat) 0.1 || std::abs(lon - it-second.lon) 0.1) { coordWarn; } } std::cout matched matched unmatched unmatched coordWarn coordWarn std::endl; }阈值 0.1 度大约是 11 公里对于站点迁站或坐标记录差异来说足够宽松超过这个值基本可以认定是站号错配或坐标错误。输出统计能帮你快速判断数据质量如果 unmatched 占比很高先检查站号格式而不是怀疑数据本身。4.3 导出带站点属性的成果表匹配完成后把站点名称、省份等属性拼回日值记录输出最终成果表。这一步可以直接在匹配循环里写文件也可以先存内存再统一写。为了简单直接在循环里写。void exportEnriched(const std::string dailyCsv, const std::string stationCsv, const std::string outCsv) { auto stations loadStations(stationCsv); std::ifstream fin(dailyCsv); std::ofstream fout(outCsv); fout stationId,stationName,province,lat,lon,date,avgTemp,precip,windSpeed,sunshine\n; std::string line; std::getline(fin, line); while (std::getline(fin, line)) { std::istringstream iss(line); std::string token; std::vectorstd::string fields; while (std::getline(iss, token, ,)) fields.push_back(token); if (fields.size() 9) continue; auto it stations.find(fields[0]); if (it stations.end()) continue; fout fields[0] , it-second.name , it-second.province , fields[1] , fields[2] , fields[3] , fields[4] , fields[5] , fields[6] , fields[7] \n; } }输出表可以直接拖进 QGIS 做点图层或者导入数据库做后续统计。字段顺序和表头对应经纬度保留原始精度。如果要做空间插值这个表就是输入源。5. 避坑与排查那些让结果对不上的细节5.1 缺测值当有效值参与计算现象算出来的月平均气温明显偏低或偏高或者降水总量出现极端值。原因把 999999 或 32700 这类缺测占位符直接当数值参与平均。解决在解析阶段就判断并剔除或者标记为无效后跳过。不同要素的缺测值可能不同最好从说明文档里抄一份对照表写进代码常量。5.2 站号前导零丢失导致匹配率低现象站点矢量匹配时大量站号找不到unmatched 数量异常高。原因日值文件里站号是00123矢量 CSV 里被 Excel 打开后变成123。解决统一站号格式读入后补齐到固定长度或者两边都转成整数再比较。最稳的是在数据准备阶段就用文本编辑器检查别让 Excel 自动转换。5.3 文件编码不一致导致中文站名乱码现象导出的 CSV 里站名是乱码或者程序读站点 CSV 时解析失败。原因日值文件可能是 GBK 编码站点 CSV 是 UTF-8C 标准库按字节读不自动转换。解决统一转成 UTF-8 再处理或者在 Windows 下用MultiByteToWideChar转换。VS2019 项目里可以设置/utf-8编译选项但读文件时仍要确认实际编码。5.4 逐日记录行数对不上月份天数现象某站点某月只有 28 天记录但该月有 31 天。原因原始文件里缺测日可能直接不写行而不是写一行缺测值。解决解析时按日期连续性检查缺失的日期补空或标记不要默认文件里每天都有。做时间序列分析时缺失日期要显式处理否则移位错位。5.5 经纬度偏差告警被忽略现象匹配统计里 coordWarn 有几十条但没管后面画图发现站点跑到海里。原因站号匹配上了但坐标是错的可能是迁站或数据录入错误。解决把 coordWarn 的记录单独导出人工核对确认是迁站就按年份用不同坐标确认是错误就修正或剔除。6. 进阶技巧把处理链路做成可复用的批处理工具走到这里核心功能已经跑通但每次改参数都重新编译不现实。我的习惯是把根目录、输出路径、缺测值阈值、坐标偏差阈值做成命令行参数或者配置文件用main函数接收。这样编译一次后面换数据只需要改参数。int main(int argc, char* argv[]) { if (argc 4) { std::cerr Usage: processor rootDir stationCsv outCsv\n; return 1; } std::string rootDir argv[1]; std::string stationCsv argv[2]; std::string outCsv argv[3]; std::string tempCsv temp_daily.csv; processAll(rootDir, tempCsv); exportEnriched(tempCsv, stationCsv, outCsv); std::cout Done. Output: outCsv std::endl; return 0; }参数说明rootDir是 V3.0 数据根目录stationCsv是站点矢量导出的属性表outCsv是最终成果路径。中间文件temp_daily.csv可以保留用于排查也可以处理完删除。如果数据量很大可以把processAll改成多线程按年份目录并行但要注意磁盘 IO 瓶颈别开太多线程。另一个实用技巧是加一个校验模式不输出成果只统计每个站点的记录数、缺测比例、坐标偏差生成一份质量报告。这样在正式处理前就能发现数据问题避免跑完几个小时才发现某年目录是空的。我一般会在批处理脚本里先跑校验确认无误再跑正式导出。这个习惯帮我省过很多次后悔药。最后说一个验证方法拿一个你熟悉的站点手动翻原始文件数几天记录和成果表里的值对一遍。如果对得上说明解析逻辑没问题对不上就从缺测值、字段顺序、日期格式三个方向查。别小看这一步很多玄学问题都是字段顺序在不同年份文件里变了导致的。希望帮到你。本文还有配套的精品资源点击获取