前端编解码

背景

因为中文的博大精深,以及早期文件编码的不统一,造成了现在可能碰到的文件编码有gb2312gbkgb18030utf-8big5等。因为编解码的知识比较底层和冷门,一直以来我对这几个编码的认知也很肤浅,很多时候也会疑惑编码名到底是大写还是小写,英文和数字之间是不是需要加“-”,规则到底是windows定的还是国家定的等等。

我肤浅的认知如下:

编码 说明
gb2312 最早的简体中文编码,还有海外版的hz-gb-2312
big5 繁体中文编码,主要用于台湾地区。小时候有些繁体中文游戏乱码,都是因为big5编码和gb2312编码的识别混乱导致
gbk 简体+繁体,我就当它是gb2312+big5,向下兼容,在解码时我一般选择该编码,因为打的字少。后来了解到,这个就是windows帮中国“好心的”扩展了中文编码,致使编码库又多了个新成员
gb18030 gb家族的新版,向下兼容,国家标准,现在中文软件都理应支持的编码格式,文件解码的新选择
utf-8 不解释了,国际化编码标准,html现在最标准的编码格式。注:windows上的文本编辑器用到的utf-8是带BOM的

BOM

当使用windows记事本保存文件的时候,编码方式可以选择ANSI(通过locale判断,简体中文系统下是gb家族)、Unicode、UTF-8等。那文件打开的时候,系统是如何判断该使用哪种编码方式呢?

答案是:windows(例如:简体中文系统)在文件头部增加了几个字节以表示编码方式,三个字节(0xef, 0xbb, 0xbf)表示utf8;两个字节(0xff, 0xfe或者0xfe, 0xff)表示unicode;无表示gbk。

值得注意的是,由于BOM不表意,在解析文件内容的时候应该舍弃,不然会造成解析出来的内容头部有多余的内容。

unicode

unicode由于设计之初的种种外因、内因,应用不广,我也了解不多,就简单说明下:

  • utf系列是unicode的实现
  • 设计强制使用两个字节表示所有字符,在英文场景下造成极大的浪费。相对的,utf-8以一个字节表示英文
  • 上小节提到有两种方式表示unicode,分别是LE和BE。这个表示字节序,分别表示字节是从低位/高位开始(因为每个字符都用到2个字节,而且相反的顺序能映射到不同的字符)。node的Buffer API中基本都有相应的2种函数来处理LE、BE:
buf.readInt16LE(offset[, noAssert])
buf.readInt16BE(offset[, noAssert])

后端解码

我第一次接触到该类问题,使用的是node处理,当时给我的选择有node-iconv(系统iconv的封装)以及iconv-lite(纯js)。由于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])) // utf8
buf.slice(0, 2).equals(Buffer.from([0xff, 0xfe])) // unicode

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可修改

支持的encoding列表2。这里有一个比较有趣的现象,如果文件包含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)

文件的换行不同操作系统不一致,如果需要逐行解析,需要视场景而定:

  • Linux: \n
  • Windows: \r\n
  • Mac OS: \r

注意:这个是各系统默认文本编辑器的规则,如果是使用其他软件,比如常用的sublime、vscode、excel等等,都是可以自行设置换行符的,一般是\n或者\r\n。

前端编码

可以使用TextEncoder将字符串内容转换成TypedBuffer:

const encoder = new TextEncoder() 
encoder.encode(String)

值得注意的是,从Chrome 53开始,encoder只支持utf-8编码3,官方理由是其他编码用的太少了。这里有个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.txt,后缀由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编码是unicode编码的实现,所以通过一定的规则,unicode编码都可以转为utf-8编码。而表明unicode的BOM转成utf-8编码其实就是表明utf-8的BOM。




附:

  1. TypedArray
  2. supported encodings
  3. TextEncoder
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 字符集和编码简介 在编程中常常可以见到各种字符集和编码,包括ASCII,MBCS,Unicode等字符集。确切的说...
    兰山小亭阅读 8,464评论 0 13
  • 可以看我的博客 lmwen.top 或者订阅我的公众号 简介有稍微接触python的人就会知道,python中...
    ayuLiao阅读 3,097评论 1 5
  • 最近校招季,特把自己面试中遇到的问题整理整理,以巩固自己的知识。 Java中对于容器有两大类存储方式,一种是单元素...
    末日没有进行曲阅读 1,136评论 0 12
  • Write a program that can translate Morse code in the form...
    lintong阅读 364评论 0 6
  • 词:董书利 爱是你恨也是你 总有熟悉会牢记 一段刻骨经历 和一首对应的歌曲 我是我你是你 路过熟悉一切就会想起 一...
    星巢文化阅读 215评论 0 1