阅读 769

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

背景

最近做需求时,遇到了读取歌词文件展示时发生乱码的情况。大家都知道,文字乱码基本都是解码时使用的解码方法不对,可能文件用的是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中查看源码。


作者:文仔CXKS NLXX
链接:https://juejin.cn/post/7032211978324181028


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐