一堆乱码文件批量转 UTF-8:GBK/Big5/带BOM 一次收拾干净(含 3 个实跑大坑)

2026-10-06 · 喵窝资源铺

财务软件导出的 .csv、老系统给的 .txt、打印机存的台账——十有八九不是 UTF-8。拿到 Python 或服务器里一读,要么 UnicodeDecodeError 直接崩,要么 charset=gbk 硬读之后变成 鍏徃 这种乱码。手动一个个另存为太慢,批量转又怕剥坏字节、转错编码把内容彻底毁掉。这篇给一份先探测、再备份、后转换、拿不准就交人工的批量转码脚本,代码完整实跑过,真实输出贴在下面。

它做的判断顺序

  1. 按扩展名 + 解码失败双重拦掉二进制文件(png/xlsx/docx…),不碰;
  2. 开头是 FF FE/FE FF 的 UTF-16 直接标跳过(本次不转换,见坑);
  3. 能 decode('utf-8') 的:没 BOM 就认为已经干净、原样不动;带 EF BB BF 的剥掉 BOM;
  4. 剩下非 UTF-8 的,在 gb18030 与 big5 之间按解码后中文汉字占比打分择优,两者差距太小就判"拿不准",列进清单交人工;
  5. 转换前把原始字节备份成 *.bak-enc,写入抛异常就还原。

脚本全文

import os, sys

SKIP_EXT = {'.png','.jpg','.jpeg','.gif','.pdf','.zip','.xlsx','.docx','.exe','.mp3','.mp4'}

def cjk_ratio(s):
    """解码后文本里汉字占非空白字符的比例"""
    cjk = sum(1 for ch in s if '\u4e00' <= ch <= '\u9fff')
    total = sum(1 for ch in s if not ch.isspace())
    return cjk / total if total else 1.0

def decode_candidates(raw):
    out = []
    for enc in ('gb18030', 'big5'):
        try: out.append((enc, raw.decode(enc)))
        except UnicodeDecodeError: pass
    return out

converted, skipped, ambiguous, failed = [], [], [], []
for fn in sorted(os.listdir(sys.argv[1])):
    p = os.path.join(sys.argv[1], fn)
    ext = os.path.splitext(fn)[1].lower()
    # 坑1:备份文件自己也是 GBK,必须显式排除,否则无限套娃
    if not os.path.isfile(p) or ext in SKIP_EXT or fn.endswith('.bak-enc'):
        continue
    raw = open(p, 'rb').read()
    if not raw:
        skipped.append((fn, '空文件')); continue
    if raw.startswith(b'\xff\xfe') or raw.startswith(b'\xfe\xff'):
        skipped.append((fn, 'UTF-16,未处理')); continue

    has_bom = raw.startswith(b'\xef\xbb\xbf')
    try:
        raw.decode('utf-8'); is_utf8 = True
    except UnicodeDecodeError:
        is_utf8 = False

    if is_utf8:
        if has_bom:                 # 坑2:BOM 要真剥掉,写回时别带着 \ufeff
            with open(p, 'w', encoding='utf-8', newline='') as f:
                f.write(raw[3:].decode('utf-8'))
            converted.append((fn, '剥BOM', len(raw), os.path.getsize(p)))
        else:
            skipped.append((fn, '已是 UTF-8'))
        continue

    cands = decode_candidates(raw)
    if not cands:
        skipped.append((fn, '二进制/无法识别')); continue
    ranked = sorted(cands, key=lambda c: -cjk_ratio(c[1]))
    best_enc, best_txt = ranked[0]
    runner = ranked[1] if len(ranked) > 1 else None
    # 坑3:gb18030 和 big5 都能"解码成功",靠占比差距判断是否真的认对了
    if runner and abs(cjk_ratio(best_txt) - cjk_ratio(runner[1])) < 0.15:
        ambiguous.append((fn, best_enc, runner[0])); continue

    open(p + '.bak-enc', 'wb').write(raw)          # 先备份,可回滚
    try:
        with open(p, 'w', encoding='utf-8', newline='') as f: f.write(best_txt)
        converted.append((fn, best_enc, len(raw), os.path.getsize(p)))
    except Exception as e:
        os.replace(p + '.bak-enc', p); failed.append((fn, str(e)))

print(f'转换 {len(converted)},跳过 {len(skipped)},拿不准 {len(ambiguous)},失败 {len(failed)}')
for x in converted: print('  [转]', x)
for x in skipped:   print('  [跳]', x)
for x in ambiguous: print('  [?]', x)

用法:python3 to_utf8.py 目录。没有 --delete 那种危险开关——它只转文本、只备份不删原始数据,转换是覆盖写入但原文留在 .bak-enc 里。确认无误后自己删备份即可。

实跑输出

造了 5 类样本:GBK 员工名单、Big5 繁体客户清单、GB18030 生僻字(赵㛃)、带 BOM 的 UTF-8 导出表、纯 ASCII 的 UTF-8 正常文件,外加一个空文件和一个 png。跑完:

转换 4,跳过 2,拿不准 0,失败 0
  [转] ('big5_客戶清單.csv', 'big5', 42, 60)
  [转] ('gb18030_生僻字.csv', 'gb18030', 35, 43)
  [转] ('gbk_员工名单.csv', 'gb18030', 67, 88)
  [转] ('utf8bom_导出.csv', '剥BOM', 44, 41)
  [跳] ('empty.txt', '空文件')
  [跳] ('utf8_正常.txt', '已是 UTF-8')

逐个核对转换后的内容:

文件转换后内容(截断)结果
gbk_员工名单.csv姓名,部门,工资 / 张三,销售部,8000 …✅
big5_客戶清單.csv姓名,備註 / 陳大文,台北客戶 …✅
gb18030_生僻字.csv姓名,备注 / 赵㛃,含 GB18030 扩展字✅
utf8bom_导出.csv订单号,金额(表头第一列不再是隐形 BOM)✅
utf8_正常.txt原样未动✅ 跳过

连跑第二次:转换 0,跳过 6——所有文件都已干净,不会重复转、不会生成套娃备份。幂等通过。

3 个真坑(都是踩过才写进来的)

坑 1:备份文件被当成待转文件,二次运行无限套娃。第一次跑完,目录里多了 ansi_台账.txt.bak-enc。这些备份本身就是原始 GBK 字节,第二次运行如果不显式排除,脚本会"贴心地"把它们也转成 UTF-8,生成 .bak-enc.bak-enc,再跑一次再套一层。修复很简单——过滤条件加 fn.endswith('.bak-enc')。但如果你用"备份到别的目录"方案,就要保证备份目录不在扫描范围内,否则同样套娃。
坑 2:BOM 剥没剥要验大小,别只看日志。第一版脚本打印"剥BOM",但文件字节数一个没变——因为它 decode('utf-8') 之后原样 encode('utf-8') 写回去了,\ufeff 还在。正确做法是读 raw[3:] 再解码,或者用 encoding='utf-8-sig' 打开。副作用值得警惕:如果按 'utf-8' 读一个带 BOM 的 CSV,第一列表头会变成 \ufeff订单号,跟代码里写的列名对不上,排查半天。
坑 3:GBK 与 Big5 靠"能否解码"区分不了。一段 Big5 繁体字节,用 gb18030 去解往往也能成功——解出来的是一串同样合法、但语义全错的汉字乱码。所以"try 一下能解就是它"完全不可靠。这份脚本的做法是:两个编码都解,比"解码后汉字占非空白字符的比例",谁高信谁;两者差距小于 0.15 就判"拿不准"、列清单交人工,绝不硬转。宁可漏转,不能转错——转错编码是内容不可逆损坏。

边界说明(如实)

不想自己折腾编码?把文件给我。一批 csv/txt 乱码要转 UTF-8,或者 GBK 表格导入数据库报错、Excel 打开中文全乱,我可以替你写好脚本、用你的真实文件实测后交付(¥19.9 表格/文本批量处理 / ¥29.9 脚本定制)。
→ 去下单页看看 | 虚拟物品交付后不退款;未收到或未达承诺免费补发/重做。

相关文章:邮箱列清洗去重 · 手机号列清洗去重 · CSV 按班级拆分(含编码自适应) · 一堆 CSV 并进一个 Excel(认编码认分隔符)

← 更多实跑干货 | 喵窝资源铺