转自:https://github.com/ProtoTeam/blog/blob/master/201712/3.mdhtml
做者简介:nekron 蚂蚁金服·数据体验技术团队前端
由于中文的博大精深,以及早期文件编码的不统一,形成了如今可能碰到的文件编码有GB2312
、GBk
、GB18030
、UTF-8
、BIG5
等。由于编解码的知识比较底层和冷门,一直以来我对这几个编码的认知也很肤浅,不少时候也会疑惑编码名究竟是大写仍是小写,英文和数字之间是否是须要加“-”,规则究竟是谁定的等等。node
我肤浅的认知以下:git
编码 | 说明 |
---|---|
GB2312 | 最先的简体中文编码,还有海外版的HZ-GB-2312 |
BIG5 | 繁体中文编码,主要用于台湾地区。些繁体中文游戏乱码,其实都是由于BIG5编码和GB2312编码的错误使用致使 |
GBK | 简体+繁体,我就当它是GB2312+BIG5,非国家标准,只是中文环境内基本都遵照。后来了解到,K竟然是“扩展”的拼音首字母,这很中国。。。 |
GB18030 | GB家族的新版,向下兼容,最新国家标准,如今中文软件都理应支持的编码格式,文件解码的新选择 |
UTF-8 | 不解释了,国际化编码标准,html如今最标准的编码格式。 |
通过长时间的踩坑,我终于对这类知识有了必定的认知,如今把一些重要概念从新整理以下:github
首先要消化整个字符编解码知识,先要明确两个概念——字符集和字符编码。web
顾名思义就是字符的集合,不一样的字符集最直观的区别就是字符数量不相同,常见的字符集有ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。正则表达式
字符编码决定了字符集到实际二进制字节的映射方式,每一种字符编码都有本身的设计规则,例如是固定字节数仍是可变长度,此处不一一展开。windows
常提到的GB23十二、BIG五、UTF-8等,若是未特殊说明,通常语义上指的是字符编码而不是字符集。后端
字符集和字符编码是一对多的关系,同一字符集能够存在多个字符编码,典型表明是Unicode字符集下有UTF-八、UTF-16等等。浏览器
当使用windows记事本保存文件的时候,编码方式能够选择ANSI(经过locale判断,简体中文系统下是GB家族)、Unicode、Utf-8等。
为了清晰概念,须要指出此处的Unicode,编码方式实际上是UTF-16LE。
有这么多编码方式,那文件打开的时候,windows系统是如何判断该使用哪一种编码方式呢?
答案是:windows(例如:简体中文系统)在文件头部增长了几个字节以表示编码方式,三个字节(0xef, 0xbb, 0xbf)表示UTF-8;两个字节(0xff, 0xfe或者0xfe, 0xff)表示UTF-16(Unicode);无表示GB**。
值得注意的是,因为BOM不表意,在解析文件内容的时候应该舍弃,否则会形成解析出来的内容头部有多余的内容。
这个涉及到字节相关的知识了,不是本文重点,不过提到了就顺带解释下。LE和BE表明字节序,分别表示字节从低位/高位开始。
咱们常接触到的CPU都是LE,因此windows里Unicode未指明字节序时默认指的是LE。
node的Buffer API中基本都有相应的2种函数来处理LE、BE,贴个文档以下:
const buf = Buffer.from([0, 5]); // Prints: 5 console.log(buf.readInt16BE()); // Prints: 1280 console.log(buf.readInt16LE());
我第一次接触到该类问题,使用的是node处理,当时给个人选择有:
因为node-iconv涉及node-gyp的build,而开发机是windows,node-gyp的环境准备以及后续的一系列安装和构建,让我这样的web开发人员痛(疯)不(狂)欲(吐)生(嘈),最后天然而然的选择了iconv-lite。
解码的处理大体示意以下:
const fs = require('fs') const iconv = require('iconv-lite') const buf = fs.readFileSync('/path/to/file') // 能够先截取前几个字节来判断是否存在BOM buf.slice(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])) // UTF-8 buf.slice(0, 2).equals(Buffer.from([0xff, 0xfe])) // UTF-16LE const str = iconv.decode(buf, 'gbk') // 解码正确的判断须要根据业务场景调整 // 此处截取前几个字符判断是否有中文存在来肯定是否解码正确 // 也能够反向判断是否有乱码存在来肯定是否解码正确 // 正则表达式内常见的\u**就是unicode码点 // 该区间是常见字符,若是有特定场景能够根据实际状况扩大码点区间 /[\u4e00-\u9fa5]/.test(str.slice(0, 3))
随着ES20151的浏览器实现愈来愈普及,前端编解码也成为了可能。之前经过form表单上传文件至后端解析内容的流程如今基本能够彻底由前端处理,既少了与后端的网络交互,并且由于有界面反馈,用户体验上更直观。
通常场景以下:
const file = document.querySelector('.input-file').files[0] const reader = new FileReader() reader.onload = () => { const content = reader.result } reader.onprogerss = evt => { // 读取进度 } reader.readAsText(file, 'utf-8') // encoding可修改
fileReader支持的encoding列表,可查阅此处。
这里有一个比较有趣的现象,若是文件包含BOM,好比声明是UTF-8编码,那指定的encoding会无效,并且在输出的内容中会去掉BOM部分,使用起来更方便。
若是对编码有更高要求的控制需求,能够转为输出TypedArray:
reader.onload = () => { const buf = new Uint8Array(reader.result) // 进行更细粒度的操做 } reader.readAsArrayBuffer(file)
获取文本内容的数据缓冲之后,能够调用TextDecoder继续解码,不过须要注意的是得到的TypedArray是包含BOM的:
const decoder = new TextDecoder('gbk') const content = decoder.decode(buf)
若是文件比较大,可使用Blob的slice来进行切割:
const file = document.querySelector('.input-file').files[0] const blob = file.slice(0, 1024)
文件的换行不一样操做系统不一致,若是须要逐行解析,须要视场景而定:
**注意:**这个是各系统默认文本编辑器的规则,若是是使用其余软件,好比经常使用的sublime、vscode、excel等等,都是能够自行设置换行符的,通常是\n或者\r\n。
可使用TextEncoder将字符串内容转换成TypedBuffer:
const encoder = new TextEncoder() encoder.encode(String)
值得注意的是,从Chrome 53开始,encoder只支持utf-8编码2,官方理由是其余编码用的太少了。这里有个polyfill库,补充了移除的编码格式。
前端编码完成后,通常都会顺势实现文件生成,示例代码以下:
const a = document.createElement('a') const buf = new TextEncoder() const blob = new Blob([buf.encode('我是文本')], { type: 'text/plain' }) a.download = 'file' a.href = URL.createObjectURL(blob) a.click() // 主动调用释放内存 URL.revokeObjectURL(blob)
这样就会生成一个文件名为file的文件,后缀由type决定。若是须要导出csv,那只须要修改对应的MIME type:
const blob = new Blob([buf.encode('第一行,1\r\n第二行,2')], { type: 'text/csv' })
通常csv都默认是由excel打开的,这时候会发现第一列的内容都是乱码,由于excel沿用了windows判断编码的逻辑(上文提到),当发现无BOM时,采用GB18030编码进行解码而致使内容乱码。
这时候只须要加上BOM指明编码格式便可:
const blob = new Blob([new Uint8Array([0xef, 0xbb, 0xbf]), buf.encode('第一行,1\r\n第二行,2')], { type: 'text/csv' }) // or const blob = new Blob([buf.encode('\ufeff第一行,1\r\n第二行,2')], { type: 'text/csv' })
这里稍微说明下,由于UTF-8和UTF-16LE都属于Unicode字符集,只是实现不一样。因此经过必定的规则,两种编码能够相互转换,而代表UTF-16LE的BOM转成UTF-8编码其实就是代表UTF-8的BOM。
附: