3. CSV与Excel文件格式解析与编码处理
环保监测数据在进入清洗流程之前,最先遇到的往往不是算法问题,而是“文件打不开、打开是乱码、列全挤在一格、只读到第一张表”这类格式层面的障碍。本节聚焦三类最常见的格式问题:字符编码(UTF-8与GBK)、分隔符差异(逗号、分号、制表符),以及Excel多工作表与合并单元格的读取策略。把这三件事处理正确,后续的缺失值填补、单位换算、异常值识别才有可靠的数据基础。
一、编码问题:乱码的本质是“字节相同、解释不同”
文本文件本身只存字节,编码决定这些字节如何映射为字符。中文Windows环境下,很多由环保部门业务系统、老版Excel“另存为CSV”导出的文件使用GBK(或GB2312、GB18030)编码;而网站下载、Python脚本生成、跨平台交换的文件多为UTF-8。用UTF-8去读GBK文件,典型表现是“监测点位”变成“鐩戞祴鐐逛綅”这类生僻字串;用GBK去读UTF-8文件,则常出现“锟斤拷”或大量问号。判断依据很直接:先看文件是否带BOM(UTF-8 BOM为字节 EF BB BF),再用小样本试读。
实操上建议采用“探测—试读—固定”的策略。Python中可用 charset-normalizer 或 chardet 探测,但探测结果不可全信,必须用业务字段验证:读入后检查表头是否包含“点位名称”“监测时间”“COD”等预期中文,若出现大量替换字符 � 则说明解码失败。更稳妥的做法是显式指定编码并捕获异常:
import pandas as pd
def read_csv_smart(path):
for enc in ("utf-8-sig", "gb18030", "utf-8"):
try:
df = pd.read_csv(path, encoding=enc, sep=None,
engine="python", nrows=50)
# 用业务字段做校验,而非只看是否抛异常
if any("点位" in str(c) or "监测" in str(c) for c in df.columns):
return pd.read_csv(path, encoding=enc, sep=None,
engine="python")
except (UnicodeDecodeError, pd.errors.ParserError):
continue
raise ValueError(f"无法确定编码或分隔符:{path}")
注意 gb18030 是GBK的超集,用它读取GBK文件不会出错,因此常作为中文环境的兜底编码;utf-8-sig 则能自动剥离BOM,避免第一列列名变成“\ufeff点位名称”这种隐蔽错误。输出环节同样要统一:建议中间结果一律写为 utf-8-sig,既保证Excel双击打开不乱码,也便于后续程序读取。
二、分隔符差异:不要假设一定是逗号
“CSV”字面是逗号分隔,但环保数据里分号(;)和制表符(\t)同样常见:欧洲区域设置的系统导出常用分号;从数据库查询结果或仪器软件复制出来的文本常是制表符;部分系统为避开字段内逗号,改用分号。若用默认逗号去读分号文件,整行会被当成一列,表现为“只有一列且列名很长”。
处理办法是让解析器自己判断:pandas 的 sep=None, engine="python" 会基于前若干行推断分隔符;也可用标准库 csv.Sniffer 先嗅探。但自动推断在字段内容含大量逗号(如“备注:pH 6.5, 温度 18℃”)时可能误判,因此推断后仍要检查列数是否与预期一致。对于结构固定的批量文件,最可靠的是在配置中显式声明分隔符,并对每个文件校验“解析出的列数 ≥ 预期关键列数”。此外要留意引号规则:字段内含分隔符时应有双引号包裹,quoting=csv.QUOTE_MINIMAL 是默认行为,遇到不规范文件可配合 on_bad_lines="warn" 定位问题行,而不是直接丢弃。
三、Excel多工作表:先看清结构,再决定读哪张
环保台账类Excel常把“点位基本信息”“月度监测记录”“排放量汇总”分放在不同工作表,甚至夹带“填报说明”“空白模板”等非数据页。直接用 pd.read_excel(path) 只会读第一张表,很容易读到说明页而误判“数据为空”。正确做法是先用 pd.ExcelFile 列出所有表名,再按业务规则筛选:
xl = pd.ExcelFile("排放汇总.xlsx")
print(xl.sheet_names) # ['填报说明', '点位信息', '监测记录', '排放汇总']
sheets = pd.read_excel(xl, sheet_name=None) # 全部读入为字典
data_sheets = {k: v for k, v in sheets.items()
if "监测时间" in v.columns or "点位名称" in v.columns}
筛选后还要逐表检查列结构:同一工作簿内不同表的列名可能不一致(如“COD”与“化学需氧量”),需要在读取后统一列名映射,而不是假设它们天然对齐。若某张表首行是标题、第二行才是表头,可用 header=1 指定;若表尾有“合计”“备注”行,应在读取后用条件过滤剔除,避免把汇总行当成监测记录参与统计。
四、合并单元格:读取后的NaN不是缺失值
合并单元格是Excel特有的视觉结构,底层只有左上角单元格存值,其余为空白。pandas读入后表现为:合并区域首行有值、后续行为 NaN。这与真正的数据缺失在表面上相同,但语义完全不同——前者应向下填充还原,后者不能随意填充。因此处理顺序必须是“先识别合并语义,再决定是否 ffill”。
典型场景是“点位名称”纵向合并覆盖多行监测记录:读入后对点位列执行 df["点位名称"] = df["点位名称"].ffill(),即可把每条记录归位到对应点位。但要注意两点:一是填充前应确认该列不存在真实缺失(可结合“监测时间”非空行判断);二是若合并发生在表头(如“第一季度”横跨“1月、2月、3月”三列),则需用 header=[0,1] 读取多级表头,再用列表推导展平为“第一季度_1月”这类唯一列名。对于横向合并的类别列,ffill(axis=1) 同样适用,但方向必须与合并方向一致,否则会把数据填错列。
把上述策略固化为读取函数后,输入端的格式差异就被收敛为统一的DataFrame:列名规范、中文正常、每条记录归属明确。此时再进入缺失值处理、单位统一与异常识别,数据质量才有可讨论的前提。