PHP是一門很靈活的語言。正由於它太靈活了,甚至有些怪異,因此你們對它的評價褒貶不一。其實我想說的是,任何一門語言都有它自身的哲學,有它存在的出發點。PHP爲Web而生,它以快速上手、快速開發而著稱,因此它也常被冠以簡單、新手用的語言等標籤。我倒不這麼認爲,所謂選對的工具去作對的事,沒有包打天下的語言。而至於說其簡單,卻也未必。javascript
我以前有篇文詳細介紹過pack和unpack:PHP: 深刻pack/unpack ,若是有不明白的地方,建議再回過頭去看多幾遍。如今應該可以寫出如下代碼:php
<?php echo pack("C", 97) . "\n";
$ php -f test.php a
可是,爲何會輸出'a'呢?雖然咱們知道字符'a'的ASCII碼就是97,可是pack方法返回的是二進制字符串,爲何不是輸出一段二進制而是'a'?爲了確認pack方法返回的是一段二進制字符串,這裏我對官方的pack的描述截了個圖:
html
確實如此,pack返回包含二進制字符串的數據,接下來詳細進行分析。前端
這裏所說的'程序',實際上是個宏觀的概念。java
對於在控制檯中執行腳本(這裏是指PHP做爲cli腳原本執行),腳本的輸出會寫入標準輸出(stdin)或標準錯誤(stderr),固然也有可能會重定向到某個文件描述符。拿標準輸出來講,暫且忽略它是行緩衝、全緩衝或者是無緩衝。腳本進程執行完畢後若是有輸出則會在控制檯上輸出字符串。那這裏的控制檯就是所說的'程序'。
shell
對於Web來講(這裏是指PHP做爲Web的服務器端語言),程序執行完後會將結果響應給瀏覽器或其它UserAgent,爲了方便描述,這裏統一稱爲UserAgent。這裏的UserAgent就是所說的'程序'。數據庫
固然還有其它狀況,好比在GUI窗口中的輸出,編輯器打開一個文件等等,這都涉及到如何顯示字符串的問題。windows
控制檯經過shell命令來執行腳本,它會fork一個子進程,以後經過exec替換子進程的地址空間,由於這個子進程不是會話首進程,因此它能夠關聯到控制終端。腳本輸出執行完畢後退出,回到控制檯。來看下面的例子:瀏覽器
<?php $str = '回'; echo $str . "\n";
$ php -f test.php 回
test.php是UTF-8編碼的文件,個人Linux系統的Locales是zh_CN.UTF-8。服務器
$ locale LANG=zh_CN.UTF-8 LANGUAGE= LC_CTYPE="zh_CN.UTF-8" LC_NUMERIC="zh_CN.UTF-8" LC_TIME="zh_CN.UTF-8" LC_COLLATE="zh_CN.UTF-8" LC_MONETARY="zh_CN.UTF-8" LC_MESSAGES="zh_CN.UTF-8" LC_PAPER="zh_CN.UTF-8" LC_NAME="zh_CN.UTF-8" LC_ADDRESS="zh_CN.UTF-8" LC_TELEPHONE="zh_CN.UTF-8" LC_MEASUREMENT="zh_CN.UTF-8" LC_IDENTIFICATION="zh_CN.UTF-8" LC_ALL=
回到剛纔的代碼,test.php是UTF-8編碼的文件,漢字'回'是三個字節表示的UTF8字符(若是不明白,能夠看個人另外一篇文章:JavaScript: 詳解Base64編碼和解碼),因此test.php文件的內容保存在硬盤上的數據就是4個字節('\n'是ASCII字符,用1個字節表示)。test.php執行輸出時,將這4個字節發送到標準輸出,以後被沖洗(這裏忽略掉被flush的時機),由控制檯來顯示。回想一下Linux系統上的locale設置,很顯然是採用UTF8的機制來顯示字符,因此前三個字節被當成一個UTF8字符,它被組合在一塊兒轉成Unicode碼而後查表,再顯示出來。
<?php $str = '回'; echo $str . "\n"; echo $str{0} . $str{1} . $str{2} . "\n";
$ php -f test.php 回 回
能夠看到,不論是整個字符輸出,仍是三個字節連在一塊兒輸出,結果是同樣的。咱們接下來看看不一樣平臺上同一個字符的Unicode編碼和UTF-8編碼是否同樣:
PHP測試:
<?php $str = '回'; $bin = pack("C3", ord($str{0}), ord($str{1}), ord($str{2})); $hex = strtoupper(bin2hex($bin)); echo "UTF-8編碼: " . $hex . "\n"; /** * 1110xxxx 10xxxxxx 10xxxxxx */ $byte1 = ord($str{0}); $byte2 = ord($str{1}); $byte3 = ord($str{2}); $c1 = (($byte1 & 0x0F) << 4) | (($byte2 & 0x3F) >> 2); $c2 = (($byte2 & 0x03) << 6) | ($byte3 & 0x3F); $dec = (($c1 & 0x00FF) << 8) | $c2; echo "Unicode編碼: " . $dec . "\n";
$ php -f test.php UTF-8編碼: E59B9E Unicode編碼: 22238
JavaScript測試:
<script type="text/javascript"> /** * UTF16和UTF8轉換對照表 * U+00000000 – U+0000007F 0xxxxxxx * U+00000080 – U+000007FF 110xxxxx 10xxxxxx * U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx * U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx * U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx * U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx */ var code = ('回').charCodeAt(0); // 1110xxxx var byte1 = 0xE0 | ((code >> 12) & 0x0F); // 10xxxxxx var byte2 = 0x80 | ((code >> 6) & 0x3F); // 10xxxxxx var byte3 = 0x80 | (code & 0x3F); console.group('Test chr: '); console.log("UTF-8編碼:", byte1.toString(16).toUpperCase() + '' + byte2.toString(16).toUpperCase() + '' + byte3.toString(16).toUpperCase()); console.log("Unicode編碼: ", code); console.groupEnd(); </script>
咱們看到輸出是同樣的。
此次無非是由剛纔的控制檯執行變成了UserAgent,其實道理仍是同樣的。服務器端PHP腳本輸出會經過HTTP的響應返回給UserAgent,那麼UserAgent就要對它進行顯示。固然,這裏還有點例外。數據是經過網絡做爲字節流發送回UserAgent,一般UserAgent有幾種方式來判斷字節流是屬於什麼編碼(或許還涉及到壓縮,但這裏將不考慮這個因素)。
服務器端能夠經過響應頭部來告訴UserAgent應該用什麼編碼來處理這些數據,好比:
<?php header("Content-Type: text/html; charset=utf8");
或者是HTML頁面中的<meta />標籤,好比:
<meta charset="utf-8" />
可是萬一這兩種方式都沒有提供,那也只能靠猜了。事實也確實如此,據我所知,Firefox就是這麼作的,而且將代碼開源了:universalchardet 。可是這種方式並不能百分之百正確檢測,因此偶爾會訪問到亂碼的頁面。
在windows上用notepad新建文本文件另存爲時有幾種編碼選項:ANSI, Unicode, Unicode BigEndian, UTF-8。
在其它編輯器中選項更多,包括有BOM和無BOM的。BOM是文件頭的前幾個字節,經過BOM,處理它的程序就知道這個文件是採用什麼編碼,而且是什麼字節序。然而在PHP中,歷來都沒有將BOM考慮進去,因此PHP解釋器去執行一個PHP文件時,不會忽略前幾個BOM字節,這就致使了問題。通常的問題在於發送cookie前,BOM被輸出了。因此如今通常推薦無BOM的文件。
無BOM有時候也是會有問題的,由於這須要處理它的程序去檢測它是什麼編碼。檢測的方式通常是掃描文件,而後根據不一樣編碼的規則來判斷二進制。這裏舉一個出現問題的例子。在windows上新建一個文本文件並保存爲ANSI編碼,而後在文件中輸入'聯通',如圖所示:
保存好後關閉test.txt文件,而後再雙擊打開,如圖所示:
咱們看到顯示的是亂碼,具體咱們能夠分析一下產生亂碼的緣由。用Editplus新建一個ANSI文件,輸入'聯通',而後切換到十六進制查看方式,以下圖所示:
對應的十六進制是:C1 AA CD A8,轉成二進制後以下:
11000001 10101010 11001101 10101000
接着咱們來看下UTF-8的轉換表:
U+00000000 – U+0000007F 0xxxxxxx U+00000080 – U+000007FF 110xxxxx 10xxxxxx U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
很顯然都被看成了二字節的UTF-8字符,拿GBK的編碼去UTF-8的碼錶裏查,您說能查到嗎?
如今咱們已經知道了不論是什麼編碼的數據,老是一個字節一個字的存儲,而且在存儲時會進行相應的編碼轉換。好比漢字'回'的GBK編碼和UTF-8編碼的字節數和編碼值都不同,因此在將GBK的文件另存爲UTF-8時必然會存在轉換,反之也是同樣的。而在讀取時若是有BOM就按BOM規定的編碼來處理,不然要進行編碼檢測後再處理。
以前講了這麼多編碼方面的問題,其實就是爲了讓你們更好的理解接下來要講的。pack能夠將ASCII進行打包而後輸出(事實上就是將一個多字節變成多個單字節,以後能夠經過unpack轉換回來),這個咱們已經知道了。可是方式有不少種,原理是同樣的。咱們來詳細分析。對pack/unpack不太熟悉的仍是建議去翻看我以前的一篇文章:PHP: 深刻pack/unpack 。由於本人的機器是小端序的,因此本文只考慮小端序。大端序是同樣的方式,只不過字節序不同罷了,能夠先判斷本機的字節序再處理。
<?php echo pack("C", 0x61) . "\n"; echo pack("S", 0x6161) . "\n"; echo pack("L", 0x61616161) . "\n"; echo pack("L", 0x9E9BE561) . "\n"; echo chr(0xE5) . chr(0x9B) . chr(0x9E) . "\n"; echo pack("H6", "E59B9E") . "\n";
$ php -f test.php a aa aaaa a回 回 回
咱們一句句的來分析,首先是:
echo pack("C", 0x61) . "\n"; echo pack("S", 0x6161) . "\n"; echo pack("L", 0x61616161) . "\n";
這三句代碼很簡單,C是無符號字節,S是2個無符號字節,L是4個無符號字節,因此輸出也沒什麼疑問。不管幾個字節,都是ASCII碼,0x61的二進制的高位爲0,因此能正確顯示。
echo pack("L", 0x9E9BE561) . "\n";
咱們或許還記得漢字'回'的UTF-8編碼爲:0xE59B9E,L是按主機字節序打包的,而個人機器是小端序,因此0x9E9BE561打包後就變爲:0x61E59B9E。0x61是字符'a'的ASCII碼,然後面的三個字節程序經過判斷0xE5就能知道這是一個三字節的UTF-8字符,所以這三個字節會轉成Unicode碼去查表,而後顯示。
echo chr(0xE5) . chr(0x9B) . chr(0x9E) . "\n";
chr是返回ASCII碼所代碼的字符,它其實不只僅是轉換單字節的字符,對於多字節一樣適用。它會根據剛纔所說的規則將三個UTF-8字節轉成Unicode碼而後去查表。
echo pack("H6", "E59B9E") . "\n";
對於H格式字符,它和h的區別就是前者是高四位在前,後者是低四位在前,但它們都是以半字節爲單位讀取的,而且以十六進制的方式。您應該看到我在用H進行打包時傳的是字符串"E59B9E",若是傳的是0xE59B9E就不對了,這樣的話先會轉成十進制15047582,而後在前面加上0x變成十六進制0x15047582。
所謂按半字節讀取實際上是這樣的,好比0x47,先轉成十進制71,而後變成十六進制的0x71。按半字節讀取必然會丟棄4位,而後要補0。讀取了0x7,對H來講,它是高位,那麼在低位補0變成0x70。對於h來講,它是低位,那麼在高位補0變成0x07。
unpack是pack的逆函數,固然unpack有本身的語法,但這不是重點,由於這些只是表象。
unpack其實只是將多個字節壓縮成一個字節。好比0x12和0x34這兩個字節若是要組成一個雙字節,則可使用unpack的S格式化字符來實現,代碼以下:
<?php $data = unpack("S", pack("H*", "3412")); print_r($data); echo '0x' . dechex($data[1]) . "\n";
$ php -f test.php Array ( [1] => 4660 ) 0x1234
由於是小端序,因此要寫成"3421"。其實還能夠用位運算的方式來實現。這個時候就不須要考慮字節序了,由於字節序只是存儲時才須要考慮的問題,對於輸出來講,是按照咱們天然的方式:
<?php print "0x" . dechex((0x12 << 8) | 0x34) . "\n";
$ php -f test.php 0x1234
PHP官方文檔上所描述的chr方法的原型參數是一個int型,雖然形參名爲ascii,但不要被騙了。如圖所示:
chr確實是能夠接收一個int類型的參數,而不只僅是一個ASCII碼。還記得以前所作的測試嗎?經過chr方法將三個UTF-8的字節組合在一塊兒。很顯然UTF-8的每一個字節都大於127,由於最高位都是1。
不過提及來chr方法仍是比較傻的,好比有以下代碼:
<?php echo chr(0xE5) . chr(0x9B) . chr(0x9E) . "\n"; echo chr(0xE59B9E) . "\n";
$ php -f test.php 回 ?
chr方法徹底沒有考慮將0xE59B9E拆成三個字節來組合,因此最終是亂碼。
ord接受一個string類型的參數,它用於返回參數的ASCII碼。以下圖所示:
雖然它只返回ASCII碼,但它的參數卻不限定。好比您能夠傳遞單字節或多字節。舉例以下:
<?php echo ord("a") . "\n"; echo ord("回") . "\n"; echo 0xE5 . "\n";
$ php -f test.php 97 229 229
傳入漢字'回',它會自動截取第一個字節,而後返回它的十進制表示。
理解了原理,其實本身去實現也就是那麼回事。本文以格式化字符L爲例,L是無符號32位整型,它是按主機字節序來打包的。因此咱們要先判斷機器的字節序。
<?php function IsBigEndian() { $bin = pack("L", 0x12345678); $hex = bin2hex($bin); if (ord(pack("H2", $hex)) === 0x78) { return FALSE; } return TRUE; } if (IsBigEndian()) { echo "大端序"; } else { echo "小端序"; } echo "\n";
$ php -f test.php 小端序
代碼很是簡單,由於PHP不能直接操做內存,因此藉助於pack來實現。L格式化字符表示主機字節序,若是機器是小端序,則0x12345678經過L打包後會變成4個字節而且字節序是:0x78, 0x56, 0x34, 0x12,若是是大端序則是:0x12, 0x34, 0x56, 0x78。而後經過H2格式化字符獲取2個高4位(即一個高位字節),若是是0x78那就是小端序,不然就是大端序。
接下來是my_pack方法的實現,僅僅實現了L格式化字符,代碼以下:
<?php // 判斷字節序 function IsBigEndian() { $bin = pack("L", 0x12345678); $hex = bin2hex($bin); if (ord(pack("H2", $hex)) === 0x78) { return FALSE; } return TRUE; } // 自定義打包方法 function my_pack($num) { $bin = ""; $padding = 0; if ($num >= 0x00 && $num <= 0xFF) { // 補3個字節 $padding = str_repeat(chr(0), 3); if (IsBigEndian()) { // 大端序 $bin = $padding . chr($num); } else { // 小端序 $bin = chr($num) . $padding; } } else if ($num > 0xFF && $num <= 0xFFFF) { // 補2個字節 $padding = str_repeat(chr(0), 2); $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = $padding . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . $padding; } } else if ($num > 0xFFFF && $num <= 0x7FFFFF) { // 補1個字節 $padding = chr(0); $byte2 = ($num >> 16) & 0xFF; $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = $padding . chr($byte2) . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . chr($byte2) . $padding; } } else { $byte1 = ($num >> 24) & 0xFF; $byte2 = ($num >> 16) & 0xFF; $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = chr($byte1) . chr($byte2) . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . chr($byte2) . chr($byte1); } } return $bin; } $bin = my_pack(0x12); print_r(unpack("L", $bin)); $bin = pack("L", 0x12); print_r(unpack("L", $bin)); $bin = my_pack(0x1234); print_r(unpack("L", $bin)); $bin = pack("L", 0x1234); print_r(unpack("L", $bin)); $bin = my_pack(0x123456); print_r(unpack("L", $bin)); $bin = pack("L", 0x123456); print_r(unpack("L", $bin)); $bin = my_pack(0x12345678); print_r(unpack("L", $bin)); $bin = pack("L", 0x12345678); print_r(unpack("L", $bin));
$ php -f test.php Array ( [1] => 18 ) Array ( [1] => 18 ) Array ( [1] => 4660 ) Array ( [1] => 4660 ) Array ( [1] => 1193046 ) Array ( [1] => 1193046 ) Array ( [1] => 305419896 ) Array ( [1] => 305419896 )
測試中調用pack和my_pack的結果是同樣的。unpack的實現就是pack的逆操做,只需把pack的結果的每個字節取到它的ASCII碼(能夠經過ord方法來獲得),而後將4個字節根據高低位次序(這還要根據大小端)經過位運算變成一個4字節的整數,其它格式化字符也是相似如此實現。接下來僅僅實現格式化字符L的unpack版本,代碼以下:
<?php // 判斷字節序 function IsBigEndian() { $bin = pack("L", 0x12345678); $hex = bin2hex($bin); if (ord(pack("H2", $hex)) === 0x78) { return FALSE; } return TRUE; } // 自定義打包方法 function my_pack($num) { $bin = ""; $padding = 0; if ($num >= 0x00 && $num <= 0xFF) { // 補3個字節 $padding = str_repeat(chr(0), 3); if (IsBigEndian()) { // 大端序 $bin = $padding . chr($num); } else { // 小端序 $bin = chr($num) . $padding; } } else if ($num > 0xFF && $num <= 0xFFFF) { // 補2個字節 $padding = str_repeat(chr(0), 2); $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = $padding . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . $padding; } } else if ($num > 0xFFFF && $num <= 0x7FFFFF) { // 補1個字節 $padding = chr(0); $byte2 = ($num >> 16) & 0xFF; $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = $padding . chr($byte2) . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . chr($byte2) . $padding; } } else { $byte1 = ($num >> 24) & 0xFF; $byte2 = ($num >> 16) & 0xFF; $byte3 = ($num >> 8) & 0xFF; $byte4 = $num & 0xFF; if (IsBigEndian()) { // 大端序 $bin = chr($byte1) . chr($byte2) . chr($byte3) . chr($byte4); } else { // 小端序 $bin = chr($byte4) . chr($byte3) . chr($byte2) . chr($byte1); } } return $bin; } // 自定義解包方法 function my_unpack($bin) { $byte1 = ord($bin{0}); $byte2 = ord($bin{1}); $byte3 = ord($bin{2}); $byte4 = ord($bin{3}); if (IsBigEndian()) { // 大端序 $num = ($byte1 << 24) | ($byte2 << 16) | ($byte3 << 8) | $byte4; } else { // 小端序 $num = ($byte4 << 24) | ($byte3 << 16) | ($byte2 << 8) | $byte1; } return array($num); } $bin = my_pack(0x12); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = pack("L", 0x12); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = my_pack(0x1234); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = pack("L", 0x1234); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = my_pack(0x123456); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = pack("L", 0x123456); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = my_pack(0x12345678); print_r(unpack("L", $bin)); print_r(my_unpack($bin)); $bin = pack("L", 0x12345678); print_r(unpack("L", $bin)); print_r(my_unpack($bin));
$ php -f test.php Array ( [1] => 18 ) Array ( [0] => 18 ) Array ( [1] => 18 ) Array ( [0] => 18 ) Array ( [1] => 4660 ) Array ( [0] => 4660 ) Array ( [1] => 4660 ) Array ( [0] => 4660 ) Array ( [1] => 1193046 ) Array ( [0] => 1193046 ) Array ( [1] => 1193046 ) Array ( [0] => 1193046 ) Array ( [1] => 305419896 ) Array ( [0] => 305419896 ) Array ( [1] => 305419896 ) Array ( [0] => 305419896 )
知道了原理,實現起來就一點也不難,無非就是要注意字節序的問題。
ISO 8859-1又稱 Latin-1 或西歐語言。是國際標準化組織內ISO/IEC 8859的第一個8位字符集。它以ASCII爲基礎,在空置的0xA0-0xFF的範圍內,加入96個字母及符號,藉以供使用附加符號的拉丁字母語言使用。
從定義可知,Latin-1編碼是單字節編碼,向下兼容 ASCII ,其編碼範圍是0x00~0xFF。0x00~0x7F之間徹底和ASCII碼一致,0x80~0x9F之間是控制字符,0xA0~0xFF之間是文字符號。
ISO-8859-1收錄的字符除ASCII收錄的字符外,還包括西歐語言、希臘語、泰語、阿拉伯語、希伯來語對應的文字符號。歐元符號出現的比較晚,沒有被收錄在ISO-8859-1當中。
由於ISO-8859-1編碼範圍使用了單字節內的全部空間,在支持ISO-8859-1的系統中傳輸和存儲其餘任何編碼的字節流都不會被拋棄。換言之,把其餘任何編碼的字節流看成ISO-8859-1編碼看待都沒有問題。這是個很重要的特性,MySQL數據庫默認編碼是Latin-1就是利用了這個特性。ASCII編碼是一個7位的容器,ISO-8859-1編碼是一個8位的容器。
pack/unpack在實際工做中用得很是多,由於不少公司用PHP作前端,經過TCP調用接口,這就須要用到pack/unpack來打包和解包。但願本文能對你們有幫助。