春江暮客

春江暮客的个人学习分享网站

Python CSV 编码排障:识别 BOM,严格转换 GB18030 和 UTF-16

2026-10-14 技术
Python CSV 编码排障:识别 BOM,严格转换 GB18030 和 UTF-16

CSV 在另一台机器上能打开,放进 Linux 上的 Python 脚本却报 UnicodeDecodeError;或者第一列明明叫 sample_id,程序读到的却是 \ufeffsample_id。先把字节编码处理正确,再做CSV 流式统计,能把这两类问题分开排查。

这篇教程用 Python 标准库检查 BOM,把明确来源的 GB18030、带 BOM 的 UTF-16 或 UTF-8 文件转成不带 BOM 的 UTF-8。示例面向初学者到中级读者,只转换编码,不改变字段、引号、分隔符或换行。

1. 方法:先确定来源编码

按下面的顺序处理:

  1. 查看导出工具的编码设置,保留原始文件。
  2. 查看文件开头的字节,有 BOM 时据此选择解码方式。
  3. 没有 BOM 时,向数据提供方确认编码;不要轮流试编码,把“不报错”当成正确。
开头字节 本文使用的参数
ef bb bf --from-encoding utf-8-sig
ff fe 或 fe ff,且不是 UTF-32 BOM --from-encoding utf-16
没有 BOM,来源明确为 GB18030 --from-encoding gb18030
没有 BOM,来源明确为 UTF-8 --from-encoding utf-8-sig

utf-8-sig 解码会跳过开头可选的 UTF-8 BOM。没有 BOM 不代表文件就是 GB18030;解码成功也不能证明文本含义正确。不要用 errors="ignore" 或 errors="replace" 绕过问题,它们可能丢弃字节或替换字符,见 Python 编码错误处理文档。

2. 准备:安装条件与演示文件

需要 Python 3.11 或更新版本,不需要 pip 包。本文示例在 Linux、Python 3.11.4 上验证。脚本只接受不超过 20 MiB 的输入,会在内存中保留输入字节、解码文本和输出字节;它不是大型文件流式转码器。

在一个新的可写目录中操作。下面的命令使用独占创建,若文件已经存在会报错,不会覆盖:

python3 - <<'PYTHON'
from pathlib import Path
text = 'sample_id,note\r\n001,中文\r\n002,"comma,inside"\r\n003,"line1\r\nline2"\r\n'
for name, encoding in (
    ("source-gb.csv", "gb18030"),
    ("source-bom.csv", "utf-8-sig"),
    ("source-utf16.csv", "utf-16"),
):
    with Path(name).open("xb") as handle:
        handle.write(text.encode(encoding))
PYTHON

查看前四个字节:

python3 - <<'PYTHON'
from pathlib import Path
for name in ("source-gb.csv", "source-bom.csv", "source-utf16.csv"):
    with Path(name).open("rb") as handle:
        prefix = handle.read(4)
    print(name, prefix.hex(" "))
PYTHON

GB18030 示例以 ASCII 表头 73 61 6d 70 开始,UTF-8 BOM 示例以 ef bb bf 73 开始。UTF-16 示例的 BOM 可能是 ff fe 或 fe ff,取决于生成它的平台;不要因此手工删除两个字节后按 UTF-8 读取。

3. 实现:保存完整脚本

把下面的代码保存为 convert_csv_encoding.py:

"""Convert a small text/CSV file to UTF-8 without overwriting any file."""
import argparse
import codecs
from pathlib import Path
import sys

MAX_BYTES = 20 * 1024 * 1024


def bom_name(data):
    # UTF-32 must precede UTF-16: their prefixes can overlap.
    for mark, name in (
        (codecs.BOM_UTF32_LE, "utf-32-le"),
        (codecs.BOM_UTF32_BE, "utf-32-be"),
        (codecs.BOM_UTF8, "utf-8-sig"),
        (codecs.BOM_UTF16_LE, "utf-16-le"),
        (codecs.BOM_UTF16_BE, "utf-16-be"),
    ):
        if data.startswith(mark):
            return name
    return None


def convert(source, destination, encoding):
    with source.open("rb") as handle:
        data = handle.read(MAX_BYTES + 1)
    if len(data) > MAX_BYTES:
        raise ValueError("input exceeds the 20 MiB limit")
    bom = bom_name(data)
    allowed = {
        "utf-8-sig": {None, "utf-8-sig"},
        "gb18030": {None},
        "utf-16": {"utf-16-le", "utf-16-be"},
    }
    if bom not in allowed[encoding]:
        raise ValueError(f"BOM {bom!r} conflicts with {encoding}")
    text = data.decode(encoding, errors="strict")
    result = text.encode("utf-8", errors="strict")
    # Input validation finishes before exclusive output creation.
    with destination.open("xb") as handle:
        handle.write(result)
    return len(data), len(result)


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("input", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("--from-encoding", required=True,
                        choices=("utf-8-sig", "gb18030", "utf-16"))
    args = parser.parse_args()
    try:
        before, after = convert(args.input, args.output, args.from_encoding)
    except (OSError, UnicodeError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"OK: input_bytes={before} output_bytes={after}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

脚本先读取有限的字节数,检查 BOM 与所选编码是否冲突,然后严格解码并重新编码。UTF-32 BOM 会先于 UTF-16 检查,避免把 ff fe 00 00 错当成 UTF-16;UTF-32 不在本例支持范围内。UTF-16 输入必须带 BOM,未带 BOM 的文件请先确认字节序,再从来源工具重新导出。

输出使用 open() 的 x 模式,所以已有文件、同名输入路径及已有符号链接都不会被覆盖。输入解码失败时,输出尚未创建。写入遇到磁盘错误或进程被中断时,可能留下部分输出;这不是原子提交,只有成功退出的结果才能进入后续处理。

先查看用法,再执行:

python3 --version
python3 convert_csv_encoding.py --help
python3 convert_csv_encoding.py source-gb.csv clean-gb.csv --from-encoding gb18030
python3 convert_csv_encoding.py source-bom.csv clean-bom.csv --from-encoding utf-8-sig
python3 convert_csv_encoding.py source-utf16.csv clean-utf16.csv --from-encoding utf-16

成功时输出 OK: input_bytes=... output_bytes=... 并返回 0。输入、编码或文件操作错误返回 1,参数错误由 argparse 返回 2。转换过程直接处理完整文本,不重新解析或写出 CSV,因此 CRLF、带引号的逗号、字段内换行和 001 的前导零都会保留;它也不会检查 CSV 的业务格式。

4. 验证:检查内容,不只看退出码

对三个结果执行同一个检查:

python3 - <<'PYTHON'
import codecs
import csv
from pathlib import Path

expected = [
    ["sample_id", "note"],
    ["001", "中文"],
    ["002", "comma,inside"],
    ["003", "line1\r\nline2"],
]
outputs = [Path(name).read_bytes() for name in
           ("clean-gb.csv", "clean-bom.csv", "clean-utf16.csv")]
assert outputs[0] == outputs[1] == outputs[2]
assert not outputs[0].startswith(codecs.BOM_UTF8)
for name in ("clean-gb.csv", "clean-bom.csv", "clean-utf16.csv"):
    with open(name, encoding="utf-8", newline="") as handle:
        assert list(csv.reader(handle, strict=True)) == expected
print("PASS: identical UTF-8 bytes; IDs, quotes and embedded CRLF preserved")
PYTHON

预期输出:

PASS: identical UTF-8 bytes; IDs, quotes and embedded CRLF preserved

CSV 验证时使用 newline="",让解析器处理换行,具体说明见 Python CSV 文档。真实数据还要核对中文、表头、记录数和关键 ID;编码已正确但原文早已是乱码时,转码不会恢复丢失的信息,需要重新导出。

下面检查错误路径。第一条故意用 UTF-8 解码 GB18030,第二条尝试覆盖已有输出:

python3 convert_csv_encoding.py source-gb.csv wrong.csv --from-encoding utf-8-sig
printf 'exit=%s\n' "$?"
python3 -c 'from pathlib import Path; assert not Path("wrong.csv").exists()'

python3 convert_csv_encoding.py source-bom.csv clean-bom.csv --from-encoding utf-8-sig
printf 'exit=%s\n' "$?"

两次都应显示 ERROR: ... 和 exit=1;wrong.csv 不应存在,已有的 clean-bom.csv 应保持原样。然后重新执行本节的内容验证,确认覆盖尝试没有改变文件。

5. 接入自动化与常见问题

只在转换成功后启动下一步。换用新的输出文件名,或在每轮任务的独立工作目录中运行:

python3 convert_csv_encoding.py source-gb.csv ready.csv --from-encoding gb18030 &&
  python3 -c 'import csv; f=open("ready.csv", encoding="utf-8", newline=""); print(next(csv.reader(f))); f.close()'

预期表头为 ['sample_id', 'note']。后续可以做重复 ID 检查;编码转换不能代替唯一性检查。

现象 具体处理
UTF-8 UnicodeDecodeError 核对导出编码;仅在来源确认后使用 gb18030 或重新导出 UTF-8。
第一列包含 \ufeff 对 UTF-8 来源使用 utf-8-sig,再检查首列表头。
BOM ... conflicts with ... 核对前四个字节和来源,选择对应参数;UTF-32 需另行处理。
File exists 选择新的输出名,避免用输入路径、已存在文件或符号链接作为输出。
超过 20 MiB 改用带增量解码和失败提交控制的流式方案;不要简单取消限制后处理超大文件。
Permission denied 或磁盘写入失败 改用可写目录并检查可用空间;失败任务的部分输出不可当作成功结果。
转换成功却仍有乱码 检查来源编码是否选错,或原始导出内容是否已经损坏。

保留原始文件及来源编码记录。需要追踪文件变化时,可使用SHA-256 清单分别记录原始和转换后的文件;编码不同通常会导致校验值不同,这不等于字段内容发生变化。

总结

先确认来源编码,再用严格解码转换为 UTF-8,避免用忽略错误的方式掩盖内容丢失。保留原始文件,用新路径接收结果,并同时检查退出码和实际 CSV 内容。编码处理通过后,再进入统计、去重或其他自动化步骤。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它