引言前端
以前作一個POC的時候,Vicky同窗遇到一個關於編碼的問題,問到我,我以爲當時沒有解釋得很清楚,因而決定查閱相關的資料文檔,寫一篇文章,記錄這個問題及對背後的緣由、原理的理解。app
問題ide
關於這個問題,爲了簡化起見,我會作一些假設。問題原型是有一個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)的下載,結果下載下來的文件不能打開。這是什麼緣由呢?3d
此外,在研究這個問題的過程當中發現另一個編碼問題:以前的文本文件全都是英文字符,當我加入中文字符之後,這些中文字符在下載下來的文件中也是亂碼,以下圖。這又是什麼緣由呢?code
編碼相關orm
以解釋上面兩個問題爲出發點,我查閱了相關資料文檔,如下是我對常見術語的理解。xml
二進制文件:計算機系統裏面全部文件都是二進制文件,即一個字節一個字節排列而成,文本文件也是二進制文件。htm
文本文件:採用特定編碼表示常見文字符號的文件,這種文件會將文字符號轉換成指定編碼對應的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的十六進制視圖):
可是因爲這個文件是二進制文件,不該該有此轉換,因此就出現了這個問題。
有兩個解決方案:第一種方案,存儲文件的時候指定編碼,我作了如下嘗試,可是不生效,暫時還沒找到如何指定編碼。
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文件里加入中文字符之後,這些中文字符在下載下來的文件中也是亂碼。這又是什麼緣由呢?
一樣的,咱們先看正常顯示和亂碼顯示文件的十六進制視圖對比(注:下圖是正常顯示文件,上圖是亂碼顯示文件):
從圖上能夠看出,字節e6被轉成了utf-8對應的碼c3a6。其實,下圖原本已是utf-8編碼(e6b189表明中文的「漢」字,e5ad97表明中文的「字」字),因此再通過一次轉換就會出現亂碼。
解決方案同上,直接寫入byte array。
最後的話:在對字符串、文本、文件作處理的時候必定要注意編碼方式,否則極可能就會出現意想不到的亂碼問題。
Rerefences: