2. 环保数据文件批量获取与组织
在真正开始清洗数据之前,最容易被低估、却最影响后续效率的一步,是把散落在各处的原始文件收拢成一批“可被程序稳定识别”的输入。环保数据尤其如此:同一个排污口,监测平台导出的可能是按小时命名的 CSV,企业报送文件夹里是按月份堆放的 Excel,邮件附件里又夹着带日期的压缩包。如果直接进入清洗环节,脚本往往要先花大量精力去猜“这个文件是什么、属于谁、对应哪段时间”。本节要解决的,就是把这堆杂乱文件整理成结构清晰、命名统一、可追溯的输入集合。
一、先看清三类来源的差异
环保数据的批量获取通常来自三条通道,它们的文件特征差别很大,处理策略也应不同。
监测平台导出:一般由在线监控系统按查询条件生成,文件名常带站点编号、污染物因子和时间范围,格式多为 CSV 或 xlsx,字段结构相对固定,但导出时可能一次只给一个因子,也可能把多个因子横向拼在一张表里。这类文件的关键是保留导出时的查询条件,否则事后无法判断某列数据对应哪个时间段。
企业报送文件夹:企业按环保部门要求定期上报,常见形式是“企业名+年月”的文件夹,里面放自行监测报告、在线数据导出表、手工监测记录等。这类文件命名随意、格式混杂,甚至同一企业不同月份的表头都不一致,是整理工作的重点。
邮件附件:多来自第三方运维单位或企业环保专员,附件名常带“最终版”“修改版”“以此为准”等字样,容易重复和覆盖。批量收集时要特别关注版本与时间戳,避免把旧版本当成新数据。
二、统一命名规则:让文件名自己说明身份
命名规则的目标不是“好看”,而是让文件名成为可解析的元数据。推荐采用下划线分隔的固定字段顺序,例如:
区域_企业简称_数据类型_起始日期_结束日期_版本.扩展名
示例:
城东区_华兴化工_在线监测_20240101_20240131_v1.csv
城东区_华兴化工_自行监测_20240101_20240131_v2.xlsx
几个实践要点:日期统一用 YYYYMMDD,避免 2024.1.1 与 2024-01-01 混用导致排序错乱;版本号用 v1、v2 递增,而不是“最终版”“最终版2”;企业简称要建立一张对照表,防止“华兴化工”“华兴化工有限公司”“华兴”三种写法并存。命名一旦确定,就应写成规则文档,后续所有来源的文件在入库前都先重命名,而不是保留原名再另建索引。
三、目录结构设计:按查询维度分层
目录结构应围绕“以后怎么找数据”来设计。环保数据最常用的检索维度是时间、企业、数据类型,因此可以采用三级结构:
环保数据/
├── 2024/
│ ├── 华兴化工/
│ │ ├── 在线监测/
│ │ │ ├── 城东区_华兴化工_在线监测_20240101_20240131_v1.csv
│ │ │ └── ...
│ │ └── 自行监测/
│ └── 其他企业/
└── 2023/
这样分层的好处是:按年归档便于跨年对比,按企业聚合便于单企业全量核查,按数据类型区分则避免在线数据与手工监测数据混在同一目录。需要注意的是,原始文件与清洗后文件必须分开放置,例如在根目录下另设 raw/ 与 clean/,原始文件只读不改,所有清洗结果写入 clean 目录,保证任何时候都能回溯到源头。
四、文件清单:批量处理的“账本”
文件收拢、重命名、归档之后,必须生成一份文件清单。清单不是可有可无的附属品,它是后续批量清洗的驱动表:脚本读取清单,逐行处理文件,处理结果再回写到清单的状态列。一份实用的清单至少包含以下字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| file_name | 统一命名后的文件名 | 城东区_华兴化工_在线监测_20240101_20240131_v1.csv |
| source_type | 来源通道 | 平台导出 / 企业报送 / 邮件附件 |
| enterprise | 企业名称 | 华兴化工 |
| period_start / period_end | 数据覆盖时段 | 2024-01-01 / 2024-01-31 |
| size_bytes | 文件大小 | 184320 |
| md5 | 内容校验值,用于识别重复文件 | a3f5...(示例) |
| status | 处理状态 | 待清洗 / 已清洗 / 异常 |
清单可以用 Python 的 pathlib 遍历目录自动生成,核心思路是:遍历目标目录下所有 CSV 与 Excel 文件,提取路径、大小、修改时间,再按命名规则用正则解析出企业、数据类型和时段,写入一张总表。下面是一段可直接改造使用的示例代码:
from pathlib import Path
import hashlib, re, csv
root = Path("环保数据/2024")
rows = []
pattern = re.compile(r"(?P<area>[^_]+)_(?P<ent>[^_]+)_(?P<dtype>[^_]+)_"
r"(?P<start>\d{8})_(?P<end>\d{8})_v(?P<ver>\d+)")
for f in root.rglob("*"):
if f.suffix.lower() not in (".csv", ".xlsx", ".xls"):
continue
m = pattern.search(f.stem)
md5 = hashlib.md5(f.read_bytes()).hexdigest()
rows.append({
"file_name": f.name,
"source_type": "企业报送" if "报送" in str(f) else "平台导出",
"enterprise": m.group("ent") if m else "",
"period_start": m.group("start") if m else "",
"period_end": m.group("end") if m else "",
"size_bytes": f.stat().st_size,
"md5": md5,
"status": "待清洗",
})
with open("文件清单.csv", "w", newline="", encoding="utf-8-sig") as fp:
writer = csv.DictWriter(fp, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
运行后得到的清单就是后续清洗的入口。这里有两个值得观察的判断点:一是正则未匹配的文件(enterprise 为空),说明命名不合规,需要人工回看并重命名;二是md5 重复的行,往往意味着同一文件被重复收集(例如邮件里既发了附件又发了压缩包),应在清洗前去重,避免同一批数据被计算两次。
original_path,记录文件重命名前的原始路径。这样即使命名规则调整,也能追溯每个文件从哪来、由谁报送,满足环保数据可审计的要求。五、批量收集时的常见坑
第一,编码问题。监测平台导出的 CSV 常见 GBK 编码,而企业报送的可能是 UTF-8,直接读取会出现中文乱码。收集阶段可以先统一转存为 UTF-8,或在清单中记录编码,清洗时按编码读取。第二,表头行位置不固定。部分企业报表前几行是标题和说明,真正表头在第 3 行甚至第 5 行,收集时不必强行改表,但要在清单或备注中标记,供清洗阶段跳过。第三,同名覆盖。邮件附件下载到同一目录时,同名文件会被自动加“(1)”,这类文件在重命名前要先比对 md5,确认是否真的不同。
把这三类来源收拢、按规则重命名、按维度归档、再生成一份带校验值的清单,原始数据就从“一堆文件”变成了“一批可被程序稳定处理的输入”。这一步做扎实,后面的批量清洗才能少走弯路。