TATECHATLAS
◎ 简体中文
编程

Python中稳健的CSV解析:处理BOM、方言和换行符

本指南介绍如何使用Python的csv模块自动检测分隔符、通过utf-8-sig处理Unicode字节顺序标记(BOM),并利用Sniffer类和正确的文件打开参数管理跨平台换行问题,实现无需手动字符串分割的可靠CSV读取。

本文内容

读取可能带 BOM 的 UTF-8 CSV 时,使用 encoding='utf-8-sig' 和 newline=''。若导出约定明确了分隔符,应直接指定。对于未知格式,Sniffer.sniff 从已解码的文本推测格式,但不判断是否存在表头。使用 DictReader 前检查列名,并处理 csv.Error。

理解CSV方言与格式

CSV(逗号分隔值)格式并无单一的通用标准。尽管RFC 4180提供了指导原则,但许多应用程序(尤其是Microsoft Excel)在分隔符、引用字符和行结束符方面存在细微差异。这些差异使得手动字符串分割不可靠,因为同一文件可能使用分号而非逗号,或包含复杂的引用规则。

Python的csv模块通过‘方言’(dialect)概念抽象了这些差异。方言是一组参数的集合,如分隔符、引用字符和行终止符,用于定义特定应用的数据格式。无需为每个新文件源编写自定义解析逻辑,即可通过模块自动处理这些变化。

使用Sniffer实现格式自动检测

Sniffer.sniff 接收字符串,而不是原始字节。1024 个字符只是可选样本,不是可靠的最小值,也不保证识别正确。采样后应将文件位置移回开头。Sniffer.has_header 是独立的启发式判断,也可能出错;已知导出规范优于猜测。

处理字节顺序标记(BOM)

许多基于Windows的应用程序会在UTF-8文件开头添加字节顺序标记(BOM),以标识编码方式。若使用标准的'utf-8'编码打开此类文件,BOM(字节序列0xef, 0xbb, 0xbf)将被视为实际数据,导致首列名称被不可见字符污染。

为解决此问题,应在open()函数中使用'utf-8-sig'编码。该编码专门用于识别UTF-8 BOM,并在解码过程中跳过它,确保首行标题干净可用。

配置CSV文件的打开方式

使用csv模块时常见的陷阱是未指定newline参数。根据Python文档,打开文件供csv模块使用时,必须始终设置newline=''。

若省略此参数,Python的I/O层可能自行执行换行符转换,导致意外行为,如出现额外空行或错误处理包含内部换行的引用字段。设置newline=''将行终止的控制权完全交由csv模块的内部解析器。

使用csv.reader以列表形式读取数据

csv.reader函数返回一个迭代器,每次生成一行字符串列表。当仅关注数据位置(如第三列)而无需映射到特定名称时,此方法非常适用。结合检测到的方言,reader可自动完成分隔与去引用操作。

import csv
from pathlib import Path

# Illustrative UTF-8 input with BOM, semicolons and a quoted newline.
Path('example.csv').write_text(
    '\ufeffID;Name\n1;"Ada; Lovelace"\n2;"Grace\nHopper"\n',
    encoding='utf-8', newline=''
)
with open('example.csv', newline='', encoding='utf-8-sig') as f:
    sample = f.read(1024)  # Characters, not bytes.
    f.seek(0)
    try:
        dialect = csv.Sniffer().sniff(sample, delimiters=',;\t')
    except csv.Error as error:
        raise ValueError('Cannot determine CSV dialect; specify it explicitly') from error
    for row in csv.reader(f, dialect=dialect):
        print(row)
# Expected illustrative output:
# ['ID', 'Name']
# ['1', 'Ada; Lovelace']
# ['2', 'Grace\nHopper']

使用DictReader将行映射为字典

未提供 fieldnames 时,DictReader 总是把第一行作为键,与是否使用 Sniffer 无关。下方示例依赖上一个示例创建的 ID 和 Name 表头。没有表头时,明确传入 fieldnames 或使用 csv.reader。在处理数据前拒绝不符合预期的列名。

正确的表头并不保证每行字段数量一致。DictReader 默认用 None 填充缺少的值,将多余的值放在 None 键下;重复列名可能覆盖字典中已有的值。因此应检查表头名称是否唯一,以及每行实际字段数量是否符合约定。仅成功解析为字典,不代表数据已满足应用要求。处理缺失或多余字段时,应明确拒绝、记录或采用约定的补全规则,不要把异常行悄悄当成正常数据。

import csv

# Uses example.csv created above; its delimiter and header are known.
with open('example.csv', newline='', encoding='utf-8-sig') as f:
    reader = csv.DictReader(f, delimiter=';')
    if reader.fieldnames != ['ID', 'Name']:
        raise ValueError('Unexpected CSV header')
    for row in reader:
        print(row['ID'], repr(row['Name']))
# Expected illustrative output:
# 1 'Ada; Lovelace'
# 2 'Grace\nHopper' 

编码与错误处理管理

在处理多样化的数据源时,可能遇到不符合预期编码的字符。open()函数支持'errors'参数。使用'default'(即'strict')会在遇到无效字节时抛出UnicodeDecodeError,适用于数据验证。若希望跳过有问题的字符,可使用'ignore'或'replace'。

始终确保编码与源文件一致。虽然'utf-8-sig'可处理BOM,但如果文件实际为'latin-1'编码,必须显式指定,否则将导致解码错误。

通过自定义方言实现高级格式控制

若遇到Sniffer无法识别的高度非标准文件格式,可使用csv.register_dialect()定义自定义方言。这允许硬编码分隔符、引用字符等参数,之后可在reader或writer对象中通过名称引用。这对于组织内部重复使用的专有格式尤其有用。

检查清单

  • 确认open()函数调用中包含newline=''参数。
  • 若文件包含BOM,确认使用encoding='utf-8-sig'。
  • 在读取样本用于sniff后,确认调用了f.seek(0)。
  • 检查用于sniff的样本大小是否足够大以捕捉分隔符。

csv.Sniffer.sniff()方法基于启发式,若样本过小或数据高度不规则,可能产生误判。DictReader要求存在有效标题行以正确映射键;若无标题,它将使用第一行作为键,可能导致数据丢失或错误。

参考来源

  1. Python: csv ↗
  2. Python: encodings ↗
返回顶部 ↑