Skip to content

多语言乱码修复工具 Encoding Fixer:用评分排序替代"编码检测" ​

译注:本文编译自 dev.to 文章《Encoding Fixer (Multilingual Mojibake Repair)》,作者介绍了一款用于修复多语言乱码(Mojibake)的前端工具的设计思路与实现细节。原文链接见文末。

乱码是一类特殊的 bug:字节本身没有损坏,但显示出来的文字却像数据被破坏了一样。一个 UTF-8 文件若被当作 Windows-1252 打开,é 会变成 é;一个 Big5 文件若被当作 UTF-8 解码,则会变成一堆毫不相关的符号。真正的难点在于,乱码后的可见文本往往已经无法告诉你当初用的是哪个解码器。

作者因此围绕一个更稳妥的问题构建了这款工具:哪种合理的解码方式能产生看起来最不"破碎"的文本,以及我们对这个结果应该有多大信心? 这刻意区别于"确定性地检测编码"——组件会尝试多个候选编码、对输出打分,并最多展示五个结果,供人工结合语言和上下文进行核验。

文件模式:保留原始字节 ​

当原始文件可用时,界面使用 FileReader.readAsArrayBuffer(),而不是从一个 JavaScript 字符串开始:

javascript
function loadFile(file) {
  fileError.value = "";
  fileName.value = file.name;

  const reader = new FileReader();
  reader.onload = (e) => {
    fileBuffer.value = e.target.result;
  };
  reader.onerror = () => {
    fileError.value = t("encodingFixer.fileLoadError");
    fileBuffer.value = null;
  };
  reader.readAsArrayBuffer(file);
}

这样每次解码尝试都能拿到原始字节序列。候选编码目录涵盖 UTF-8、GBK/GB2312、GB18030、Big5、Shift-JIS、EUC-JP、EUC-KR、若干 ISO-8859 变体、Windows-1250 至 Windows-1257,以及 KOI8-R/U。对每个候选编码,TextDecoder 以 fatal 模式运行:

javascript
const decoder = new TextDecoder(enc, { fatal: true });
const text = decoder.decode(buf);

无法解码该字节序列的编码会被直接跳过,而不是生成一个充满替换字符的误导性字符串。成功解码的候选会被打分,只有得分高于 10 的才会保留。

评分:一组不完美信号的组合 ​

评分器最多采样 2000 个字符,统计可打印码点,并对常见的错误解码特征区间进行扣分:

javascript
const sample = text.slice(0, 2000);
let printable = 0;
let suspicious = 0;

for (let i = 0; i < sample.length; i++) {
  const cp = sample.charCodeAt(i);
  if (cp === 9 || cp === 10 || cp === 13 || (cp >= 32 && cp !== 127)) {
    printable++;
  }
  if (isSuspiciousCodePoint(cp)) suspicious++;
}

let score = Math.round(printableRatio * 100)
  - Math.round(suspiciousRatio * 300);

可疑区间包括半角片假名、私用区字符、彝文字符以及孤立的谚文辅音。得分随后被限制在 0 到 99 之间。UTF-8 若能干净解码会获得少量加分,而原始 ISO-8859-1 和 Windows-1252 输出若包含大量控制区间字节则会被扣分。替换字符也会按其比例扣分——缺失数据更多的结果,不应仅仅因为大部分字符可打印就排在干净结果之前。

需要强调的是,这只是启发式排序,而非语言理解。一个短标识符、一个代码文件或一个不常见的专有名词,即使解码正确,也可能看起来"可疑"。

粘贴模式:重放一次乱码错误 ​

文本一旦被复制,其原始字节就已丢失。粘贴路径使用 iconv-lite 尝试一个更小的跨编码搜索:把可见的乱码按四种可能的源编码之一重新编码,再把这些字节按四种可能的目标编码之一解码:

javascript
for (const sourceEncoding of SOURCE_ENCODINGS) {
  const rawBytes = iconv.encode(originalText, sourceEncoding.enc);

  for (const targetEncoding of TARGET_ENCODINGS) {
    if (sourceEncoding.enc === targetEncoding.enc) continue;
    const restoredText = iconv.decode(rawBytes, targetEncoding.enc);
    if (!restoredText || restoredText === originalText) continue;
    // score and keep the candidate
  }
}

源编码列表为 Big5、GBK、Shift-JIS 和 Windows-1252;目标编码为 UTF-8、Big5、GBK 和 Shift-JIS。包含过多合成 ? 字符的结果会被丢弃,因为 iconv-lite 会用 ? 替代无法匹配编解码器的字符,而一串合法的问号可能获得虚高的分数。当输入中不含字面问号时,这些生成的问号会显示为 �,以便让部分恢复的情况可见。

打分后,候选结果按置信度从高到低排序并去重。文件模式以预览的前 100 个字符作为签名,粘贴模式则比较完整的恢复文本。这避免了界面被多个源/目标组合产生的同一可读结果填满,同时仍保留不同的合理修复方案。第一个候选会获得视觉高亮,但复制和下载操作对所有候选都可用。下载始终以 UTF-8 文本 blob 形式输出,无论产生它的编码标签是什么——这对保存修复结果很方便,但如果另一个系统明确要求 Big5 或 Shift-JIS 字节,则需要注意。

诚实的边界 ​

原始文件永远是最好的输入。文件模式下,预览上限为 5000 个字符,界面最多返回五个去重后的候选。粘贴模式下,复制已经解码过的乱码可能永久丢失字节信息,对 CJK 文本尤其如此;穷举所有组合也无法重建已经不存在的字节。高置信度分数是证据而非证明,因此在用候选结果覆盖原文件之前,请用已知词汇、文件头或源系统样本进行比对。

作者已将这一思路做成一个小型免费工具:Encoding Fixer (Multilingual Mojibake Repair)。它最实用的场景是:你能上传原始的 .txt、.csv 或 .log 文件,并让恢复过程保持可逆。

原文链接 ​

https://dev.to/begoodtool/encoding-fixer-multilingual-mojibake-repair-4633