如何在前端识别文件的编码

3,350 阅读6分钟

背景

最近做需求时,遇到了读取歌词文件展示时发生乱码的情况。大家都知道,文字乱码基本都是解码时使用的解码方法不对,可能文件用的是GBK编码,而我用了UTF8解码,这势必会出现乱码。

如何解决

  1. 后端将所有上传的文件都统一改成用UTF8进行编码,下发时,前端默认用UTF8进行解码即可

  2. 前端上传文件时做限制,比如不能直接上传源文件,需要将源文件的内容复制到前端提供的编辑器,然后上传,这样就能保证了编码都是使用utf8

  3. 后端不做处理,文件下发时,前端对文件编码进行识别,然后再解码展示。

三种方法对比

个人觉得最优是第二种,因为它在文件上传时就做了限定,但是这种每次需要复制粘贴的操作对用户并不友好。

其次是第一种方法,后端对文件编码进行识别以及转换。这种方法对用户比较友好,但是后端工作量会大一点,而且文件识别并不一定是准确的,没有第二种方法可靠,但是也能解决大部分场景。

最差的方法就是第三种方法,如果在前端进行处理,那每个项目每个地方需要展示文件内容时,都需要走一遍文件识别编码再解码的逻辑。

项目目前现状

  • 后台正在赶进度,抽不出人力来调研如何识别文件编码,在赶项目进度中

  • 前端这次需求压力不大,提测之后有人力空余

结合实际情况加上个人意愿(前端对这个有点兴趣),就暂时使用了第三种方法。当然这只是临时解决方法,后面产品会单独弄一个需求来优化这边处理。

进入主题——前端如何识别文件的编码

这个问题我们可以从gignupg朋友中找到一个不错的答案。下面内容是我结合gignupg的内容和代码进行一个自己的梳理。

检测一个文件编码的过程

  1. 通过文件头部的Byte Order Mark(BOM)进行检查

  2. 如果BOM不适用,则判断是否是UTF-8编码

  3. 如果也不是UTF-8编码,则首先推测文件内容的语言,通过语言的常用编码来确定编码

通过BOM来确定文件编码

Byte Order Mark(BOM, 字节顺序标记),是位于码点U+FEFF的统一码字符的名称。当以UTF-16或UTF-32来将UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序。它常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的标记(from 维基百科),简单来说,BOM是存在于文件的开头,用于标识该文件编码的标记。因此我们第一步是查看文件是否存在BOM,如果存在我们就可以知道文件编码了,代码大概实现如下


const byteOrderMarkBuffer = new FileReader();

byteOrderMarkBuffer.onload = () => {

const uInt8String = new Uint8Array(byteOrderMarkBuffer.result)

.slice(0, 4)

.join(" ");

const byteOrderMark = checkByteOrderMark(uInt8String);

if (byteOrderMark) {

const byteOrderMarkReader = new FileReader();

byteOrderMarkReader.onload = () => {

data.content = byteOrderMarkReader.result;

return data.content;

};

byteOrderMarkReader.onerror = (err) => {

reject(err);

};

byteOrderMarkReader.readAsText(file, fileInfo.encoding);

}

};

byteOrderMarkBuffer.onerror = (err) => {

reject(err);

};

byteOrderMarkBuffer.readAsArrayBuffer(file);

使用FileReader对象读取文件流(Blob/File格式),读取完成后,将数据存储进8位无符号整数数组中,接着取前4个字节(之所以取前四个Byte,是因为BOM最长长度为4个字节)的内容,拼接起来生成文件的BOM标识。最后使用checkByteOrderMark函数对BOM进行检查,如果检查可以找到结果,则重新使用FileReader对象用指定的编码方式对文件进行读取,最后得到文件非乱码内容。接下来,我们了解一下checkByteOrderMark的实现。


// checkByteOrderMark函数

const byteOrderMarks = require("../config/byteOrderMarkObject.js");

module.exports = (uInt8Start) => {

for (const element of byteOrderMarks) {

if (element.regex.test(uInt8Start)) return element.encoding;

}

return null;

};

// byteOrderMarkObject.js文件内容

module.exports = [

{

encoding: "UTF-EBCDIC",

regex: new RegExp("221 115 102 115"),

},

{

encoding: "GB-18030",

regex: new RegExp("132 49 149 51"),

},

{

encoding: "UTF-32LE",

regex: new RegExp("255 254 0 0"),

},

{

encoding: "UTF-32BE",

regex: new RegExp("0 0 254 255"),

},

{

encoding: "UTF-8",

regex: new RegExp("239 187 191"),

},

{

encoding: "UTF-7",

regex: new RegExp("43 47 118"),

},

{

encoding: "UTF-1",

regex: new RegExp("247 100 76"),

},

{

encoding: "SCSU",

regex: new RegExp("14 254 255"),

},

{

encoding: "BOCU-1",

regex: new RegExp("251 238 40"),

},

{

encoding: "UTF-16BE",

regex: new RegExp("254 255"),

},

{

encoding: "UTF-16LE",

regex: new RegExp("255 254"),

},

];

checkByteOrderMark函数通过遍历一个存储着BOM信息的数组对象,使用正则来判断文件传入的头部信息是否与BOM中的一种匹配,如果匹配上,则可以直接得到编码方式。

判断文件是否使用UTF-8进行编码

如果文件没有BOM,下一步,我们可以判断文件是否由使用率最高的UTF-8(如今UTF-8使用率超过97%)编码的,过程如下。


const utfReader = new FileReader();

utfReader.onload = () => {

const utfContent = utfReader.result;

const utf8 = checkUTF(utfContent);

if (utf8) {

data.content = utfContent;

return data.content

}

};

utfReader.onerror = (err) => {

reject(err);

};

utfReader.readAsText(file, "UTF-8");

使用FileReader直接以UTF-8编码方式进行解码,得到的内容,调用checkUTF函数进行判断,如果是以UTF-8编码的,则返回结果,checkUTF函数如下


// checkUTF函数内容

module.exports = (content) => {

for (let b = 0; b < content.length; b++) {

// If ? is encountered it's definitely not utf8!

if (content[b] === "�") {

return false;

}

}

return true;

}

checkUTF函数遍历传入的字符串,通过查看是否有�符号来判断是否是UTF-8编码。原理是,UTF-8对于乱码的内容会转化为这个符号�,因此,如果有该符号,则文件不是以UTF-8编码的。

通过推测语言来确定文件编码

上面两种方法都试过了,还是有问题,那么可以通过推测文件内容使用的语言来推测文件的编码。


const isoReader = new FileReader();

isoReader.onload = () => {

data.content = isoReader.result;

return(processContent(data));

};

isoReader.readAsText(file, "ISO-8859-1");

首先,直接用FileReader对象以”ISO-8859-1“的编码方法进行解码得到文本,然后使用processContent函数对解码的内容进行语言识别和处理

下面来看processContent函数的内容


// processContent函数

const countAllMatches = require("./processing-content/countAllMatches.js");

module.exports = (data) => {

data.languageArr = countAllMatches(data);

const language = data.languageArr.reduce((acc, val) =>

acc.count > val.count ? acc : val

).name;

data.pos = data.languageArr.findIndex(

(elem) => elem.name === language

);

// Determine the encoding

const encoding = data.languageArr[data.pos].encoding;

// .... 其他内容

};

processContent调用countAllMatcheds函数获取到一个语言数组,然后遍历这个数组,拿到count值最大的语言名字,接着通过名字获取对应语言在语言数组中的下标,最后根据下标拿到该语言的编码方式。


// countAllMatcheds函数

const languageArr = require("../../config/languageObject.js");

module.exports = (data, encoding) => {

const newLanguageArr = [];

// Cloning the language array and making sure that "count" has no reference to "languageArr"!

languageArr.forEach((obj) => {

const updatedLangObj = {};

Object.keys(obj).forEach((key) => {

if (key !== "count") {

updatedLangObj[key] = obj[key];

} else {

updatedLangObj.count = 0;

}

});

newLanguageArr.push(updatedLangObj);

});

const regex = "isoRegex";

// Populating the count property of the language array

newLanguageArr.forEach((lang) => {

if (lang[regex]) {

const matches = data.content.match(lang[regex]);

if (matches) lang.count = matches.length;

}

});

return newLanguageArr;

};

// languageObject.js太长了,就不粘贴,直接给一个url

// https://github.com/gignupg/Detect-File-Encoding-and-Language/blob/main/src/config/languageObject.js

countAllMatcheds函数代码逻辑是,先复制一遍语言数组,然后遍历语言数组,使用正则来匹配每种语言关键词在解码后的文本中出现的次数,然后使用count字段来记录这个次数,这个次数是在上一个processContent函数中用到的,根据count字段最大值来判断文本属于哪种语言,而每种语言会有一些常用编码,通过这个来获取编码。

image.png

上面截图是语言数组的每个元素的结构,包含语言的名字,正则匹配的关键词以及其常用编码方式,utfFrequency和isoFrequency是用来计算可信度的。

其他

至此,关于前端如何识别文件的编码已经结束了,gignupg其实还计算了结果的可信度,大家感兴趣可以去github中查看源码

参考资料

  1. gignupgGithub

  2. 字节顺序标记维基百科

  3. 各种编码使用率