轉載請註明來源: https://tlanyan.me/ascii-bina...
近期對識別文件格式感到好奇,不幸和字符編碼搞混,不明其中原理致使心煩意亂。爲了消除文件格式和字符編碼的疑惑,上網搜索並翻出之前收藏的文章,心結總算了卻。本文便是對文件和字符編碼的我的總結。html
剛開始的疑惑是:文本文件和二進制文件有什麼區別?爲何一個能顯示內容,另外一個的內容常常沒法(用文本編輯器)正常顯示?linux
馬里蘭大學的這篇培訓筆記,把二者的區別講得清楚:文本文件是二進制文件的一種,底層存儲也是0和1;文本文件可讀性和移植性好,但表現字符有限;二進制文件數據存儲緊湊,無字符編碼限制。文本文件基本上只能存放數字、文字、標點等有限字符組成的內容;二進制沒有字符約束,可隨意存儲圖像、音視頻等數據。程序員
用存儲數字的例子能夠形象的看出文本文件和二進制文件存儲內容上的差別。例如要存儲數字1234567890,文本文件要存儲0-9這十個數字的ASCII碼,對應的十六進制表示爲:31 32 33 34 35 36 37 38 39 30
,佔用10個字節;1234567890對應的二進制爲「0100 1001 1001 0110 0000 0010 1101 0010
」,佔用4個字節(二進制表示32位,一個字節8位),存儲到文件的16進製表示爲(大端):49 96 02 D2
。web
文本文件按字符存放內容,二進制按字節存放,這是兩種文件最本質的區別。根據這個特性,能夠推斷出一些常見結論:二進制文件經常比文本文件緊湊,佔用空間少;文本文件更友好易用,能用所見即所得的方式編輯;二進制文件經常須要專用程序打開,等等。網絡
回過頭看文本編輯器打開二進制文件經常是亂碼的現象。例如一個二進制文件存放了一個整數1234(四個字節),用16進製表示爲:00 00 04 D2
。文本編輯器打開後逐個字符解釋,會發現這幾個字節拼不出可顯示的字符,只好亂碼相待。亂碼的緣由是文本編輯器不能正確解析字節流,這也是二進制文件須要用專用軟件打開的緣由。例如jpg文件要用看圖軟件打開,若是用音樂播放器打開,完蛋!視頻文件要用播放器打開,用壓縮軟件打開,歇菜!編輯器
瞭解文本文件和二進制文件的區別後,再來看文件格式。咱們知道,Windows按文件拓展名識別文件格式,並調用對應的程序打開文件;(類)Unix系統,拓展名無關緊要,那麼怎麼知道這個文件是什麼格式呢?函數
幸虧有file命令,這個命令能夠告訴咱們文件究竟是什麼格式。文件拓展名不是文件格式的本質區別,內容纔是。把a.zip改爲a.txt/a.jgp/a.mp3,不管什麼文件名,file都讓其原形畢露:Zip archive data, at least v1.0 to extract
。編碼
file命令的工做原理可這篇文章。.net
說完了文件,再來講文件內容中的編碼。常見的127個ASCII字符,沒啥編碼好說的,反正幾乎全部的編碼方式都兼容它。雙字節、多字節字符,編碼方式和字節序,纔是困擾程序員的問題。一個漢字,GBK編碼須要兩個字節,還要考慮本機的大小端,才能肯定存放的最終形式;網絡通信時,要轉換成網絡字節序(大端序),接收方纔能正常解析。開發人員若是對字符編碼不熟悉,通訊時遇到亂碼問題,調試就很困難。unix
UCS(Universal Multiple Octet Coded Character Set)標準的制定,讓開發人員遠離混亂的多字節字符集。UCS標準裏,全部的字符都有惟一的碼點(Code Point),根據碼點就可查到對應字符。UCS用兩個字節表示一個碼點(UCS-4標準是4個字節),對應一個字符。因爲使用了兩個字節,可容納2^16-1(6w+)字符,基本上容下各國經常使用的字符(UCS-4理論上可容納上二十億個字符,目前收納超過16W個字符)。注意UCS只是一個標準,規定了碼點與字符的一一對應關係,但沒有定義如何存儲在計算機中。
規定Unicode字符存儲方式的工做由UTF(Unicode Transformation Format)完成,應用最多的方案是UTF-16和UTF-8。UTF-16使用兩個字節表示一個字符,Windows, MacOS, Java平臺默認的字符編碼方案都是UTF-16。因爲有兩個字節,便存在大端和小端兩種方案的區分。只有ASCII字符的文件,使用UTF-16編碼存在空間浪費嚴重的現象(浪費50%的存儲),由Ken Thompson(C語言發明人)和Robe Pike(Go語言發明人)提出的UTF-8編碼方案很快流行起來。UTF-8是單字節流,不存在字節序問題,也不須要BOM。目前UTF-8是web通行標準。
USC-2的取值範圍是U+0000~U+FFFF,與UTF-8的對應關係以下:
十六進制 | 二進制 |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
從編碼能夠看出,與二進制相比,浪費了不少空間。不過這也沒辦法,可顯示的字符更容易閱讀和理解,人類很難抗拒這個誘惑。
UTF-8轉換規則爲: 1. 若是某字節第一位是 0 ,那麼斷定爲 ASCII 字節,除了 0 外餘下的 7 位是 ASCII 碼,因此 UTF-8 是兼容 ASCII 碼的; 2. 若是第一個字節是 1 ,那麼連續的幾個 「1」 表明從這個字符開始,後面連續的幾個字節實際上是一個字位,且後面的字節都要以10開頭。
瞭解如上規則,咱們的程序即可輕鬆的處理UTF-8編碼的字節流。例如要找出「中」的UTF-8編碼,則能夠這樣處理(注意文件是UTF-8編碼):
$char = "中"; $length = strlen($char); $bytes = pack("a" . $length, $char); echo "UTF-8:" . bin2hex($bytes) . "\n"; // 或者 echo "UTF-8:"; for ($index = 0; $index < $length; ++ $index) { echo bin2hex($char{$index}); } echo PHP_EOL;
也能夠寫出針對UTF-8編碼的strlen函數:
function myStrlen(string $string) { $slen = strlen($string); $mlen = 0; $maxByteLength = 4; $maxOffset = 7; for ($i = 0; $i < $slen; ++ $i) { $byte = ord($string{$i}); // 從01xxxxxx開始對比,直到11110xxxx 10xxxxxx 10xxxxxx 10xxxxxx。只須要對比第一個字節便可 for ($offset = 0; $offset < $maxByteLength; ++ $offset) { $result = $byte & (1 << ($maxOffset - $offset)); if ($result === 0) { $i += $offset; ++ $mlen; break; } } } return $mlen; } $string = "Coder不是工程師!"; echo "mb_strlen:" . mb_strlen($string) . "\n"; echo "mStrlen:" . myStrlen($string) . "\n";
瞭解其原理,亂碼再也不困惑和迷茫。