1. 程式人生 > >WEB開發中的字符集和編碼

WEB開發中的字符集和編碼

引言

我相信很多人在初接觸程式設計時,都被字符集狠狠地虐過,特別是資料庫的中文亂碼問題,那麼亂碼是怎麼產生的呢? 我們都知道計算機是以二進位制儲存和執行的,那麼它是怎麼把二進位制資料轉換為各種文字的呢? 還有我們常用的各種字符集,常用的編碼轉換,都是怎麼進行的呢?

本博文所寫的內容不是技術乾貨,只是對我們常用的字符集和編碼的一個小總結,小科普。我相信讀完本文,您應該對 字符集和常見編碼方式 有個差不多的認識了。

ASCII碼

ASCII碼(American Standard Code for Information Interchange,美國資訊交換標準程式碼)應該是我們最初接觸過的編碼方式了,程式設計最常用的字元都被它包括在內。它使用7bit來表示 128(2e7)個字元,最高位固定為 0,共佔用一個位元組。其中:

  • 0~31 及 127(共33個)是控制字元或通訊專用字元(其餘為可顯示字元),如控制符:TAB(製表符)、CR(回車)、DEL(刪除)、BS(退格)等,常用的ASCII值為 8、9、10 和13 分別轉換為退格、製表、換行和回車字元。

  • 48~57 為 0 到 9 十個阿拉伯數字。

  • 65~90 為 26 個大寫英文字母,97~122 號為 26 個小寫英文字母,其餘為一些標點符號、運算子號等。

  • 32~47,58~64,123~126 代表常用標點符號(:‘等);

我們會發現這些中很多都可以在鍵盤上可以找得到。

tips:

  • PHP中我們可以使用ord($char)
    來得到一個字元的ASCII碼;
  • 可以用chr($int) 來得到得到對應ASCII數值的字元;

ANSI編碼

美國人發明了計算機,並將他們最常用的字元以一個位元組存入了計算機,可是世界上這麼多的語言都要用計算機來表示怎麼辦呢?

為了使計算機支援多種語言,不同的國家和地區制定了不同的標準。而對於漢字,產生了 GB2312、 BIG5、 JIS 等各自的編碼標準。這些使用 1 個位元組表示一個英文字元, 2 個位元組來代表一個字元的各種漢字延伸編碼方式,稱為 ANSI 編碼。

我們在使用window系統儲存檔案選擇編碼方式時,會看到有這個ANSI編碼這個選項,在不同的windows系統中,ANSI代表著不同的編碼。不同ANSI編碼之間互不相容,當資訊在國際間交流時,無法將屬於兩種語言的文字,儲存在同一段 ANSI 編碼的文字中。

Unicode編碼

來源

既然ANSI編碼有著不同編碼之間互不相容不能共存的缺點,而現代網路中又會頻繁出現多語言互動,如果在多語言網路傳播時,一個 '11011011' 到底代表著什麼字元呢?

這時,Unicode 應運而生,它是一個足夠大的字元編碼對映表,將所有字元都囊括其中,每一個都對應唯一一個 Unicode 數值。如漢字 '好' 對應的 unicode 數值為 '0x597d', 轉為二進位制為 '0101 1001 0111 1101',表示它需要 16 bit,兩個位元組,當然還有需要更多位元組來儲存的字元(原諒我舉起不來粟子)。

最新的UCS-4標準是一個尚未填充完全的31位 Unicode 字符集,它使用 31 位來儲存字元,加上恆為 0 的首位,共需佔據 32 位,4 位元組。這樣,Unicode 便能儲存 2e31 個字元,已經完全足夠儲存世界上所有的字元了。

tips:

  • 在網路傳輸中,中文字元會被轉換為 Unicode 來傳輸,用正則匹配一箇中文字元為:\x{4e00}-\x{9fa5}

  • PHP中想檢視一箇中文字元的 Unicode 碼,可以使用json_encode($str)

  • 想 json_encode 保持原中文不自動轉為 Unicode 可以使用json_encode($str, JSON_UNESCAPED_UNICODE);新增一個 option 常量。

  • 亂碼的產生就是因為對資料編碼和解碼的方式不同: windows中使用 ANSI 標準的 GBK 編碼,資料庫中使用 Unicode 的不同的編碼方式儲存,網頁瀏覽器又以不同編碼來解析,統一為 UTF-8 進行資料編碼即可解決這類問題。

注意 Unicode 只是一種符號集,字元儲存的具體實現方式看下面

UTF-8

我們知道了按照 Unicode 的標準,儲存一個字元最多要使用 4 個位元組。如果所有的字元都按照這個標準來儲存,那麼歐美國家可能要哭了,因為他們本來可以用一個位元組輕鬆儲存文件的,因為國際化,所有的儲存空間要增大三倍。為了解決這個問題,UTF-8(8-bit Unicode Transformation Format)出現了。

UTF-8採用變長的編碼方式,使用 1~4 個位元組來表示一個符號:

  • 對於單位元組的符號,位元組的第一位設為 0,後面 7 位為這個符號的 unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。
  • 對於 n 位元組的符號(n>1),第一個位元組的前 n 位都設為 1,第 n+1 位設為0,後面位元組的前兩位一律設為 10。剩下的沒有提及的二進位制位,全部為這個符號的 unicode 碼。

於是,皆大歡喜,UTF-8 成為了網際網路使用最廣泛的 Unicode 編碼實現方式。

除此之外,Unicode 還有UTF-7、Punycode、CESU-8、SCSU、UTF-32、GB18030 等實現方式;

UTF8MB4

utf8mb4 並不是 Unicode 的實現方式之一,它是 mysql 的編碼方式,在最新的 mysql 中,utf8mb4 已經可以代替 utf8,並具有 utf8 不具有的特點。

mb4, 即 most bytes 4, mysql 的 utf8 編碼最多使用 3 個位元組儲存一個字元,在儲存 4 位元組字元的時候會報錯,而 utf8mb4 最多可以使用4個位元組來儲存一個字元。所以它可以用來儲存更多的 Unicode 字元,包括一些 Emoji 表情(Emoji 是一種特殊的 Unicode 編碼,常見於 ios 和 android 手機上),和很多不常用的漢字,以及任何新增的 Unicode 字元。

由於 utf8mb4 為 utf8 的超集,所以 utf8 編碼的 mysql 資料庫可以平滑過渡到 utf8mb4。

Url編碼

url 編碼是 web 開發中最常用的編碼了。由於 url 中一些字元有特殊的作用,那麼它被稱為保留字元(reserved purpose),如 = 用來賦值, ? 用來表示 query_string 的開始, # 用來標識錨點。當我們僅僅想把這些字元當作普通字串傳輸該怎麼辦呢,這就需要使用 url 編碼。

URL編碼(URL encoding),由於其使用 % 為字首來替代特殊字元,也被稱作百分號編碼,是特定上下文的統一資源定位符 (URL)的編碼機制。也用於為 "application/x-www-form-urlencoded" MIME 準備資料, 因為它用於通過 HTTP 的請求操作 (request) 提交 HTML 表單資料。

轉換規則:

首先需要把該字元的 ASCII 的值表示為兩個十六進位制的數字,然後在其前面放置轉義字元( % ),置入 URI 中的相應位置;對於非 ASCII 字元(如中文等), 需要轉換為 UTF-8 位元組序, 然後每個位元組按照上述方式表示。

下表是常見的字元和 urlencode 之後的 標識:

charurlcharurlcharurlcharurlcharurl
! %21 # %23 $ %24 & %26 ' %27
( %28 ) %29 * %2A + %2B , %2C
/ %2F : %3A ; %3B = %3D ? %3F
@ %40 [ %5B ] %5D

tips: PHP中使用 urlencode()urldecode() 進行 url 的編碼和解碼。

Base64編碼

base64 也是一種 web 開發中的常用編碼,它能實現簡單的可逆加密,同時在系統之間傳輸二進位制等字元使用 base64 編碼也很方便。

它使用 A-Z a-z 0-9 + / 等 64 (2e6) 個字元來表示字元。嚴格來說,還有用來標識結尾處分組的位元組數的 = , 它只會出現在編碼串的最後。

編碼規則:

將一個字串以分為三個位元組(3 * 8 = 24 bit)為一個分組, 將此 24 個 bit 分為四組,每組 6 bit, 然後使用 其 6 bit 對應的十進位制數來映射出一個 base64 字元;

如 UTF-8(三個位元組表示一箇中文) 中文 ‘琪’ 轉 base64 的過程為

  • 轉換為十六進位制表示為 e790aa
  • 每個十六進位制字元轉換為4個二進位制bit為 11100111 10010000 10101010
  • 拆分為四個 6 bit 分組為 111001 111001 000010 101010
  • 對應的十進位制數字為57 57 2 42
  • 對應 base64 編碼 為 55Cq;

十進位制對應 base64 編碼的 對映表如下:

那麼一個字串拆分到最後不足三位元組了怎麼辦呢?

  • 二個位元組的情況:將這二個位元組的 16 bit 分為三組,那麼最後一組只有 4 bit (16 % 6 = 4); 在這 4 個 bit 末尾新增 2 個 0 同樣湊成 6 bit;再在末尾補上一個=號標識補位,以便於解碼;
  • 一個位元組的情況:將這一個位元組一共 8 bit 分為兩組,那麼最後一組只有 2 bit (8 % 6 = 2); 在這 2 個 bit 末尾新增 4 個 0 同樣湊成 6 bit;再在末尾補上==號標識補位,以便於解碼;

由於原來三個位元組的字元最後轉換成四個位元組來表示,base64 編碼後字串長度一般為原來 的 3/4。

以下是我為了完全瞭解 base64 編碼自己用 PHP 實現的一個 base64 編碼類(寫完編碼犯懶了。。。):

<?php

class Base64 {

    private $mapping = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 
    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 
    'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
     's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', 
     '4', '5', '6', '7', '8', '9', '+', '/',];

    /**
     * base64 主方法
     *
     * @param $str
     *
     * @return string
     */
    public function encode($str) {
        // 將字串unpack成為十六進位制
        $unpacked = unpack('H*', $str);
        $hex = str_split($unpacked[1]);
        $bin_str = $this->HexToBin($hex);

        return $this->binToBase64($bin_str);
    }

    /**
     * 將二進位制字串分組後對映為對應的base64字串
     *
     * @param $bin_str
     *
     * @return string
     */
    private function binToBase64($bin_str) {
        $base64_str = '';
        $bin_list = str_split($bin_str, 6);
        foreach ($bin_list as $bin) {
            $append = '';
            switch (strlen($bin)) {
                // $bin為6位的不用特殊處理
                case 6:
                    break;
                // $bin為4位的是二位元組字串 2*8%6 = 4
                case 4:
                    $bin = str_pad($bin, 6, '0', STR_PAD_RIGHT);
                    $append = '=';
                    break;
                // $bin為2位的是一位元組字串 1*8%6 = 2
                case 2:
                    $append = '==';
                    $bin = str_pad($bin, 6, '0', STR_PAD_RIGHT);
                    break;
            }
            $order = base_convert($bin, 2, 10);
            $char = $this->mapping[$order];
            $base64_str .= $char . $append;
        }

        return $base64_str;
    }

    /**
     * 將十六進位制字串轉換為二進位制字串
     *
     * @param $hex
     *
     * @return string
     */
    private function hexToBin($hex) {
        $bin_str = '';
        foreach ($hex as $char) {
            // 將十六進位制轉為二進位制字串,每個十六進位制字元轉為4位二進位制,不足的以0補充
            $bin = base_convert($char, 16, 2);
            if (strlen($bin) < 4) {
                $bin = str_pad($bin, 4, '0', STR_PAD_LEFT);
            }
            $bin_str .= $bin;
        }

        return $bin_str;
    }
}

$encoder = new Base64();
var_dump($encoder->encode('枕邊書blog')); // 5p6V6L655LmmYmxvZw==
var_dump(base64_encode('枕邊書blog')); // 5p6V6L655LmmYmxvZw==

tips: 在 PHP 中使用 base64_encode()base64_decode() 進行 base64 編碼和解碼。

小結

字符集和編碼一般不是 web 開發中的重點,但瞭解一下也挺有意思的,既能增長見識,還能預防哪一天突然踩了其中的坑。

如果您覺得本文對您有幫助,可以幫忙點一下推薦,也可以關注我。如有錯漏之處,煩請指出,謝謝。

參考: