多语言乱码修复工具 Encoding Fixer:用评分排序替代"编码检测"
译注:本文编译自 dev.to 文章《Encoding Fixer (Multilingual Mojibake Repair)》,作者介绍了一款用于修复多语言乱码(Mojibake)的前端工具的设计思路与实现细节。原文链接见文末。
乱码是一类特殊的 bug:字节本身没有损坏,但显示出来的文字却像数据被破坏了一样。一个 UTF-8 文件若被当作 Windows-1252 打开,é 会变成 é;一个 Big5 文件若被当作 UTF-8 解码,则会变成一堆毫不相关的符号。真正的难点在于,乱码后的可见文本往往已经无法告诉你当初用的是哪个解码器。
作者因此围绕一个更稳妥的问题构建了这款工具:哪种合理的解码方式能产生看起来最不"破碎"的文本,以及我们对这个结果应该有多大信心? 这刻意区别于"确定性地检测编码"——组件会尝试多个候选编码、对输出打分,并最多展示五个结果,供人工结合语言和上下文进行核验。
文件模式:保留原始字节
当原始文件可用时,界面使用 FileReader.readAsArrayBuffer(),而不是从一个 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 模式运行:
const decoder = new TextDecoder(enc, { fatal: true });
const text = decoder.decode(buf);无法解码该字节序列的编码会被直接跳过,而不是生成一个充满替换字符的误导性字符串。成功解码的候选会被打分,只有得分高于 10 的才会保留。
评分:一组不完美信号的组合
评分器最多采样 2000 个字符,统计可打印码点,并对常见的错误解码特征区间进行扣分:
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 尝试一个更小的跨编码搜索:把可见的乱码按四种可能的源编码之一重新编码,再把这些字节按四种可能的目标编码之一解码:
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