引言

之前做一个POC的时候,Vicky同学遇到一个关于编码的问题,问到我,我觉得当时没有解释得很清楚,于是决定查阅相关的资料文档,写一篇文章,记录这个问题及对背后的原因、原理的理解。


问题

关于这个问题,为了简化起见,我会做一些假设。问题原型是有一个Web application,后台用Java实现,前端Javascript。前端页面上有一个下载文件的功能,这个功能实现的基本逻辑是:后台用Java API读取一个文件成字节流 -> 用Java API将字节流转成Base64 encoded string -> 后台将这个string返回给前端 -> 前端AJAX Call接收到后台返回的string -> 前端调用Javascript API将Encoded string做decode,得到decoded string -> 调用Javascript API将string写入文件 - > 最后前端页面出现下载提示,用户选择下载。

后台代码的基本逻辑如下:

String a = "a";

Base64.getEncoder().encodeToString(a.getBytes())

最开始用这个逻辑实现文本文件(xml)的下载,没有问题,下载下来的文件能够正常打开并且显示正确。之后用同样的逻辑实现二进制文件(pdf)的下载,结果下载下来的文件不能打开。这是什么原因呢?

此外,在研究这个问题的过程中发现另外一个编码问题:之前的文本文件全都是英文字符,当我加入中文字符以后,这些中文字符在下载下来的文件中也是乱码,如下图。这又是什么原因呢?

关于编码的那些事-LMLPHP


编码相关

以解释上面两个问题为出发点,我查阅了相关资料文档,以下是我对常见术语的理解。


二进制文件:计算机系统里面所有文件都是二进制文件,即一个字节一个字节排列而成,文本文件也是二进制文件。

文本文件:采用特定编码表示常见文字符号的文件,这种文件会将文字符号转换成指定编码对应的code,然后以二进制的方式存储。

编码:编码是信息从一种形式或格式转换为另一种形式的过程。


ASCII: 第一代的编码标准,美国人提出,英文全称是American Standard Code for Information Interchange,中文翻译是“美国信息互换标准代码”,等同于国际标准ISO/IEC 646。 简单讲,计算机一个字节的八位可以组合出256中不同的状态,将前128种状态分别代表128个英文字符(其中包括大小写字母、数字、空格、标点符号以及一些特殊的控制字符)。这就是计算机专业同学刚上大学要了解的ASCII码表,比如小写的a是97,大写A是65,等等。

由于这种编码只定义了所有的英语字符,所以如果世界上所有电脑都采用英语系统,也就没有下面编码什么事了。但是现实是残酷的,世界上各个国家,甚至民族都有自己的语言符号,将这些语言文字符号在计算机系统中显示存储,随着计算机的普及,是一件水到渠成的必须要解决的问题,于是就有了以下各种编码方式的出现。


ISO-8859-1:ASCII码只用到一个字节的前128个状态,这个标准扩展了ASCII,将后面128个状态(128-255)利用了起来,增加了对一些西欧拉丁系语言特殊字符的支持。比如,德语的元音字符ü对应252。


以上两种标准都是单字节编码。单字节编码能够最多支持256个字符,计算机要世界上如此多的语言,显然是不够的,于是应运而生,就出现了多字节编码。最开始各个主流语言都出现了自己的编码规则,比如简体中文的GB2312,繁体中文的BIG5,日语的JIS等。


ANSI: 默认的编码方式,对于英文系统是ASCII编码,对于简体中文系统是GB2312编码,对于繁体中文系统是Big5码。


GB2312: 用两个字节代表一个汉字字符。这种编码包含了六千多个常用汉字。比如中文的“严”字用D1CF代表。


GBK: GB2312编码基本上能够满足常用需求,但是对于古文里偏僻的汉字,少数民族的文字等是没有对应的编码的,于是就出现了GBK。这种编码扩展了GB2312,增加了偏僻汉字,少数民族文字的支持。

这里GB是国标的意思,K是扩展的意思。


JIS: 日语文字的编码标准。


以上标准都是双字节标准,即都是用计算机两个字节代表一个字符。


UNICODE: 上面的编码标准是互不兼容的,ISO为了这种各自为政的局面,决定制定一套统一的标准能够代表世界上所有的文字,这个标准就叫做UNICODE。 UNICODE又叫做UCS,是Universal Multiple-Octet Coded Character Set的缩写。

UTF-8: UTF是UCS Transfer Format的缩写。可变长的UNICODE标准的实现,举个例子,UTF-8表示英文字符用一个字节表示(与ASCII兼容),表示汉字通常是三个字节,比如e6b189代表中文的“汉”字,e5ad97代表中文的“字”字。

UTF-16/32:通常不用。


对于问题的解释

回过头来解释上面遇到的两个问题。

第一个问题,为什么xml文件的下载没有问题,而pdf文件的下载却是打开乱码呢?

首先,前端调用Javascript API将Encoded string做decode,得到decoded string的代码如下:

var decodedStr = atob(data);

atob这个方法输入一个encoded的string,输出一个decoded的string。其实现逻辑主要分三步: 第一步将encoded的string采用默认的编码转换成byte array,第二步将对byte array做base64 decode转换,得到转换后的byte array,第三步,将byte array采用默认的编码转换成string。这里默认的编码是ISO-8859-1。

然后,调用如下代码实现文件的save功能:

var blob = new Blob([atob(decodedStr)], {type: "application/pdf"});

var link=document.createElement('a');

link.href=window.URL.createObjectURL(blob);

link.download="downloadedFile.pdf";

link.click();

Javascript的Blob实现下载功能会默认采用utf-8编码。由于utf-8跟ASCII兼容,但是不跟ISO-8859-1兼容,ISO-8859-1编码里面的后127个字符在utf-8里面会有另外一个code对应。举个例子:decodedStr中的一个字符"?"在ISO-8859-1编码里面code是e2,当存储成文件的时候应用utf-8的编码,其对应的code是c3a2,所有对应于ISO-8859-1编码后127位的字节都会转成utf-8码,通常都变成了两个字节。如下图所示(注:上半部分是正常可打开的pdf的十六进制视图,下半部分是打不开的pdf的十六进制视图):

关于编码的那些事-LMLPHP 

但是由于这个文件是二进制文件,不应该有此转换,所以就出现了这个问题。

有两个解决方案:第一种方案,存储文件的时候指定编码,我做了以下尝试,但是不生效,暂时还没找到如何指定编码。

var blob = new Blob([atob(decodedStr)], {type: "application/pdf"; charset: "iso-8859-1"});


第二种方案,先将decodedStr手动转成byte array,然后再构造Blob,这种情况下Blob就不会再做转换,下载下来的文件就

能够正确打开。

var decodedStr = atob(data); var bytes = new Uint8Array( decodedStr.length ); for(var i=0; i<decodedStr.length; i++){ bytes[i] = decodedStr.charCodeAt(i); } var blob = new Blob([bytes.buffer], { type: 'application/pdf' });

有同学可能会问,为什么xml文件下载下来就可以正常打开?这是因为xml文件里面全都是英文字符和符号,都是ASCII码可以表示的(ISO-8859-1前128个,ISO-8859-1兼容ASCII),所以在上面提到的下载过程中转码成utf-8没有问题。


第二个问题,当我在xml文件里加入中文字符以后,这些中文字符在下载下来的文件中也是乱码。这又是什么原因呢?

同样的,我们先看正常显示和乱码显示文件的十六进制视图对比(注:下图是正常显示文件,上图是乱码显示文件):

关于编码的那些事-LMLPHP

 从图上可以看出,字节e6被转成了utf-8对应的码c3a6。其实,下图本来已经是utf-8编码(e6b189代表中文的“汉”字,e5ad97代表中文的“字”字),所以再经过一次转换就会出现乱码。

解决方案同上,直接写入byte array。


最后的话:在对字符串、文本、文件做处理的时候一定要注意编码方式,不然很可能就会出现意想不到的乱码问题。


Rerefences:

  • https://en.wikipedia.org/wiki/ASCII

  • https://en.wikipedia.org/wiki/ISO/IEC_8859-1

  • https://en.wikipedia.org/wiki/UTF-8

  • http://www.fileformat.info/info/unicode/char/00E6/index.htm

  • https://en.wikipedia.org/wiki/Base64


本文分享自微信公众号 - 天马行空布鲁斯(gh_2feda5c053bd)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

08-31 00:13