花拾录
← 返回知识库

一个坏文件拖垮整轮批量任务:Python 读取时的编码容错

编程语言导入2026/09/220 阅读0 评论

你写了个批量脚本,要扫描几千个文件、从中检索关键词。前几百个跑得好好的,突然进程整个崩掉,报一个 UnicodeDecodeError。等你去查,发现「肇事」的只是其中一个编码不规范的小文件。为了这一个文件,整轮任务白跑了。

现象

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1024:
invalid start byte

或者:

UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-1:
invalid continuation byte

特点很鲜明:

  • 报错是 DecodeError,方向是「往里读」;
  • 崩的是整个进程,不是跳过那个文件继续;
  • 如果脚本已经处理了一半,可能留下半成品输出;
  • 换个数据集,崩在别的文件上,因为「哪个文件编码不规范」是随机的。

根因

根因其实很简单——读文件时没给容错选项。

Python 的 open() 默认行为是:指定了 encoding='utf-8' 后,一旦遇到不符合 UTF-8 规则的字节序列,就抛 UnicodeDecodeError 并中止。这是「严格模式」(errors='strict'),也是默认模式。

问题是,你处理的这批文件「来源不可控」:可能是别人给的、从各种旧系统导出的、甚至是从网页上抓下来的。其中难免有一两个不是 UTF-8——可能是 GBK、可能是拉丁编码、也可能是文件损坏。只要有一个读不过去,默认行为就是整个脚本倒下。

从脚本的角度看,这是「一颗老鼠屎坏了一锅汤」;从 Python 的角度看,它只是忠实执行了「遇到非法数据就报错」的默认约定。

解决

加上容错参数 errors='replace'(或 errors='ignore'),让读取在遇到坏字节时不中止:

with open(path, 'r', encoding='utf-8', errors='replace') as f:
    text = f.read()

两个选项的区别:

  • errors='replace':把无法解码的字节替换成 �(U+FFFD)。保留长度信息,你知道这里「有个解不出来的字符」。
  • errors='ignore':直接丢弃坏字节。文本更干净,但你不知道丢了什么。

做批量检索时,推荐 errors='replace'——你要的是「别崩、尽量多读到内容」,而不是「绝对不能有坏字符」。检索关键词时,� 不会干扰匹配。

如果批量处理里更想要「跳过坏文件、继续下一个」的语义,可以包一层:

for path in files:
    try:
        with open(path, 'r', encoding='utf-8', errors='replace') as f:
            text = f.read()
    except OSError as e:
        print(f'[SKIP] {path}: {e}')
        continue
    process(path, text)

更讲究一点的做法是先探测编码再读,用 chardet 之类的库猜出真实编码,但那是「尽量读对」的策略;errors='replace' 是「保证不崩」的策略。批量任务的第一优先级是不崩。

延伸与预防

这条坑的教训可以推广成一句通用原则:

批量处理「来源不可控的输入」时,容错参数不是可选项,是必选项。

不只是编码——同一类问题到处都是:

  • 解析 JSON 时某个文件格式不对,要不要 catch;
  • 读 CSV 时某行列数不对,要不要跳过;
  • 解析日期时某个值格式奇怪,要不要给默认值。

共同点是:你的任务是「处理一大批」,不是「处理完美的那一批」。 一个坏输入导致整批失败,是设计上的错误,而不是数据的问题。

设计批量任务时,一个很有用的自检问题是:

如果我处理的这批数据里,有 1% 是坏的,会发生什么?

如果答案是「整个任务崩掉」,那就该在关键环节加容错,并把坏输入记录下来(而不是静默丢弃),跑完之后再统一看哪几个文件出了问题、要不要单独处理。让流程跑完,同时让问题可见——这才是一个健壮的批处理应有的样子。

评论(0)

  • 还没有评论,来抢沙发~

相关文章