本文章首發於我的博客,鑑於 sf 博客樣式具備賞心悅目的美感,遂發表於此。javascript
本文還在持續更新中,最新版請移步原博客。html
最近在看《Dive Into Python 3》,第四章講了字符串相關知識,看後才發現,字符串遠比咱們想象的要複雜多。就像該書所說的java
Everything you thought you knew about strings is wrong.python
是的,我以前對字符串的理解都是錯的。linux
也許你會詫異,字符串有什麼難的,即使遇到亂碼的狀況隨便 Google 下就能找到解決方法,可是這樣你不以爲有種被動的感受嘛,我以爲和學習任何東西同樣,學習編程首要是學習其思想,知道某事物爲何(why)要這麼作,至於如何作(how)那只是前輩們提出的解決方案,咱們能夠參考,隨便掌握下來。git
本文下面首先講解字符、字符串、編碼、ASCII、Unicode、UTF-8 等一些基本概念,而後會介紹在使用計算機時是如何如編碼打交道的,也就是實戰部分。
但願你們在閱讀完本文後,都能對 string 有一全新的認識。github
當咱們談到字符串(string或text)時,你可能會想到「計算機屏幕上的那些字符(characters)與符號(symbols)」,你正在閱讀的文章,無非也是由一串字符組成的。可是你也許會發現,你沒法給「字符串」一明肯定義,可是咱們就是知道,就像給你一個蘋果,你能說出其名字,可是不能給出準肯定義同樣。這個問題先放一放,後面我再解釋。編程
咱們知道,計算機並不能直接處理操做字符與符號,它只認識 0、1 這兩個數字,因此若是想讓計算機顯示各類各樣的字符與符號,就必須定義它們與數字的一一映射關係,也就是咱們所熟知的字符編碼(character encoding)。你可簡單的認爲,字符編碼爲計算機屏幕上顯示的字符與這些字符保存在內存或磁盤中的形式提供了一種映射關係。字符編碼紛繁複雜,有些專門爲特定語言優化,像針對簡體中文的編碼就有 GB2312,GBK(GB 是 Guajia Biaozhun 的簡寫)等,針對英文的 ASCII;另外一些專門用於多語言環境,像後面要講到的 UTF-8。vim
咱們能夠把字符編碼看做一種解密密鑰(decryption key),當咱們收到一段字節流時,不管來自文件仍是網絡,若是咱們知道它是「文本(text)」,那麼咱們就須要知道採用何種字符編碼來解碼這些字節流,不然,咱們獲得的只是一堆無心義的符號,像 ������。api
計算機最先起源於以英文爲母語的美國,英文中的符號比較少,用七個二進制位就足以表示,如今最多見也是最流行的莫過於 ASCII 編碼,該編碼使用 0 到 127 之間的數字來存儲字符(65表示「A」,97表示「a」)。
咱們知道一個字節是 8 位,ASCII 編碼其實只使用了其中的低 7 位,還剩下 1 位。在早期,不少 OEM 廠商就想着能夠利用 128-255 來表示一些特殊符號,其中比較有名的是 IBM-PC 提出了 OEM 字符集(character set),爲歐洲國家的語言提供註音以及一些線畫符號(line drawing characters),以下圖,利用這些人們能夠在計算機上畫一些有趣的圖形。
隨着計算機的普及,使用計算機的人再也不僅僅侷限於以英文爲母語的國家,不一樣國家的 OEM 字符集也如雨後春筍,層出不窮。因爲0-127的編碼已經固定下來,因此它們大都使用128-255來編碼本身的符號集。例如,有些地方用 130 表示 é
,但在以色列表示爲 Hebrew letter Gimel ג
。
這種 OEM 廠商「混戰」的狀況最終被 ANSI 標準制止,在 ANSI 標準中,對於0-127之間的編碼與 ASCII 保持一致,最高位用 0 填充。可是對於128以及之上的編碼,爭議比較大,不一樣地區每每不一致,這些不一樣的編碼,致使了不一樣的 code pages 的產生。
能夠看到,只是單字節的編碼問題就已經存在這麼嚴重的問題了,像中文(Chinese),日文(Japanese),韓文(Korean)(業界通常稱爲 CJK)等象形(表意)文字(ideograph-based language),字符數量比較多,1 個字節是放不下的,因此須要更多的字節來進行字符的編碼。和單字節編碼同樣,若是沒有統一的規範,不一樣國家本身定製本身的編碼標準,那麼不一樣國家之間是沒法進行交流的,因此,須要一個囊括世界上全部字符的編碼方案的出現,可是這裏還有個問題,若是用多字節來對字符進行編碼,那麼對於 ASCII 字符來講,是比較浪費空間的,針對這兩個問題,聰明的人們提出了一全新的字符編碼方案——Unicode。
Unicode 的全稱是 universal character encoding,中文通常翻譯爲「統一碼、萬國碼、單一碼」。
Unicode 主要解決了前面所提到的兩個問題:
統一世界上全部字符的編碼,使得語言不一樣的國家也能正常交換信息
提出了一箇中間層,使得字符的編碼與存儲形式分離解耦,這樣不一樣國家就能夠採用不一樣的存儲方案,來解決單字節表達字符數有限與多字節編碼浪費的矛盾。
Unicode 的關鍵創新點在於爲字符編碼與最終的存儲形式加了一中間層(術語爲 code points),這樣,當一種語言有新字符產生時,只需分配新的 code point 便可,具體的存儲形式(一個字節仍是兩個字節,採用大端仍是小端)不須要關心。
這讓我想到了以前在 SICP 看到的一句話,真是軟件開發領域的銀彈
任何問題,均可以經過增長一層抽象來解決。
Unicode 中採用四個字節來定義 code point,每個 code point 都表明世界上惟一的字符,不會出現同一 code point 在不一樣國家表示不一樣字符的狀況。好比,U+0041
老是表明A
,即使某語言中沒有這個字符。
Unicode 的存儲形式通常稱爲UTF-*
編碼,其中 UTF 全稱爲 Unicode Transformation Format
,常見的有:
UTF-32 編碼是 Unicode 最直接的存儲方式,用 4 個字節來分別表示 code point 中的 4 個字節,也是 UTF-*
編碼家族中惟一的一種定長編碼(fixed-length encoding)。UTF-32 的好處是可以在O(1)
時間內找到第 N 個字符,由於第 N 個字符的編碼的起點是 N*4 個字節,固然,劣勢更明顯,四個字節表示一個字符,別說以英文爲母語的人不幹,咱們中國人也不幹了。
UTF-16 最少能夠採用 2 個字節表示 code point,須要注意的是,UTF-16 是一種變長編碼(variable-length encoding),只不過對於 65535 以內的 code point,採用 2 個字節表示而已。若是想要表示 65535 之上的字符,須要一些 hack 的手段,具體能夠參考wiki UTF-16#U.2B10000_to_U.2B10FFFF。很明顯,UTF-16 比 UTF-32 節約一半的存儲空間,若是用不到 65535 之上的字符的話,也可以在O(1)
時間內找到第 N 個字符。
UTF-16 與 UTF-32 還有一個不明顯的缺點。咱們知道不一樣的計算機存儲字節的順序是不同的,這也就意味着
U+4E2D
在 UTF-16 能夠保存爲4E 2D
,也能夠保存成2D 4E
,這取決於計算機是採用大端模式仍是小端模式,UTF-32 的狀況也相似。爲了解決這個問題,引入了 BOM (Byte Order Mark),它是一特殊的不可見字符,位於文件的起始位置,標示該文件的字節序。對於 UTF-16 來講,BOM 爲U+FEFF
(FF 比 FE 大 1),若是某 以 UTF-16 編碼的文件以FF FE
開始,那麼就意味着字節序爲小端模式,若是以FE EE
開始,那麼就是大端模式。
UTF-16 對於以英文爲母語的人來講,仍是有些浪費了,這時聰明的人們(準確說是Ken Thompson與Rob Pike)又發明了另外一個編碼——UTF-8。在 UTF-8 中,ASCII 字符采用單字節。其實,UTF-8 前 128 個字符與 ASCII 字符編碼方式一致;擴展的拉丁字符像ñ
、ö
等採用2個字節存儲;中文字符采用 3 個字符存儲,使用頻率極少字符采用 4 個字節存儲。因而可知,UTF-8 也是一種變長編碼(variable-length encoding)。
UTF-8 的編碼規則很簡單,只有二條:
對於單字節的符號,字節的第一位設爲0,後面7位爲這個符號的 code point。所以對於英語字母,UTF-8編碼和ASCII碼是相同的。
對於n字節的符號,第一個字節的前n位都設爲1,第n+1位設爲0,後面字節的前兩位一概設爲10。剩下的沒有說起的二進制位,所有爲這個符號的 code point。
經過上面這兩個規則,UTF-8 就不存在字節順序在大小端不一樣的狀況,因此用 UTF-8 編碼的文件在任何計算機中保存的字節流都是一致的,這是其很重要一優點;UTF-8 的另外一大優點在於對 ASCII 字符超節省空間,存儲擴展拉丁字符與 UTF-16 的狀況同樣,存儲漢字字符比 UTF-32 更優。
UTF-8 的一劣勢是查找第 N 個字符時須要O(N)
的時間,也就是說,字符串越長,就須要更長的時間來查找其中的每一個字符。其次是在對字節流解碼、字符編碼時,須要遵循上面兩條規則,比 UTF-1六、UTF-32 略麻煩。
隨着互聯網的興起,UTF-8 是逐漸成爲使用範圍最廣的編碼方案。
咱們在互聯網上查找編碼相關資料時,常常會看到UCS-2
、UCS-4
編碼,它們和UTF-*
編碼家族是什麼關係呢?要想理清它們之間的關係,須要先弄清楚,什麼是 UCS。
UCS 全稱是 Universal Coded Character Set,是由 ISO/IEC 10646定義的一套標準字符集,是不少字符編碼的基礎,UCS 中大概包含 100,000 個抽象字符,每個字符都有一惟一的數字編碼,稱爲 code point。
在19世紀八十年代晚期,有兩個組織同時在 UCS 的基礎上開發一種與具體語言無關的統一的編碼方案,這兩個組織分別是 IEEE 與 Unicode Consortium,爲了保持這兩個組織間編碼方案的兼容性,兩個組織嘗試着合做。早期的兩字節編碼方案叫作「Unicode」,後來更名爲「UCS-2」,在研發過程發,發現 16 位根本不可以囊括全部字符,因而 IEEE 引入了新的編碼方案——UCS-4 編碼,這種編碼每一個字符須要 4 個字節,這一行爲馬上被 Unicode Consortium 制止了,由於這種編碼太浪費空間了,又由於一些設備廠商已經對 2 字節編碼技術投入大量成本,因此在 1996 年 7 月發佈的 Unicode 2.0 中提出了 UTF-16 來打破 UCS-2 與 UCS-4 之間的僵局,UTF-16 在 2000 年被 IEFE 組織制定爲RFC 2781標準。
因而可知,UCS-*
編碼是一歷史產物,目前來講,統一編碼方案最終的贏家是 UTF-*
編碼。
根據UTF-16 FOR PROCESSING,如今流行的三大操做系統 Windows、Mac、Linux 均採用 UTF-16 編碼方案,上面連接也指出,現代編程語言像 Java、ECMAScript、.Net 平臺上全部語言等在內部也都使用 UTF-16 來表示字符。
上圖爲 Mac 系統中文件瀏覽器 Finder 的界面,其中全部的字符,在內存中都是以 UTF-16 的編碼方式存儲的。
你也許會問,爲何操做系統都這麼偏心 UTF-16,Stack Exchange 上面有一個精彩的回答,感興趣的能夠去了解
爲了適應多語言環境,Linux/Mac 系統經過 locale 來設置系統的語言環境,下面是我在 Mac 終端輸入locale
獲得的輸出
LANG="en_US.UTF-8" <==主語言的環境 LC_COLLATE="en_US.UTF-8" <==字串的比較排序等 LC_CTYPE="en_US.UTF-8" <==語言符號及其分類 LC_MESSAGES="en_US.UTF-8" <==信息顯示的內容,如功能表、錯誤信息等 LC_MONETARY="en_US.UTF-8" <==幣值格式的顯示等 LC_NUMERIC="en_US.UTF-8" <==數字系統的顯示信息 LC_TIME="en_US.UTF-8" <==時間系統的顯示資料 LC_ALL="en_US.UTF-8" <==語言環境的總體設定
locale 按照所涉及到的文化傳統的各個方面分紅12個大類,上面的輸出只顯示了其中的 6 類。爲了設置方便,咱們能夠經過設置LC_ALL
、LANG
來改變這 12 個分類熟悉。其優先級關係爲
LC_ALL
>LC_*
>LANG
設置好 locale,操做系統在進行文本字節流解析時,若是沒有明確制定其編碼,就用 locale 設定的編碼方案,固然如今的操做系統都比較聰明,在用默認編碼方案解碼不成功時,會嘗試其餘編碼,如今比較成熟的編碼測探技術有Mozila 的 UniversalCharsetDetection 與 ICU 的 Character Set Detection 。
通常來講,高級編程語言都提供都對字符的支持,像 Java 中的 Character 類就採用 UTF-16 編碼方案。
這裏有個文字遊戲,通常咱們說「某某字符串是XX編碼」,其實這是不合理的,由於字符串壓根就沒有編碼這一說法,只有字符纔有,字符串只是字符的一串序列而已。
不過咱們平時並無這麼嚴謹,不過你要明白,當咱們說「某某字符串是XX編碼」時,知道這其實指的是該字符串中字符的編碼就能夠了。
這也就回答了本文一開始提到問題,什麼是字符串,這裏用《Diving into Python 3》書上的一句話來總結下:
Bytes are not character, bytes are bytes. Characters are an abstraction. A string is a sequence of those abstraction.
咱們能夠作個簡單的實驗來驗證 Java 中確實使用 UTF-16 編碼來存儲字符:
public class EncodingTest { public static void main(String[] args) { String s = "中國人a"; try { //線程睡眠,不要讓線程退出 Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
在使用 javac 編譯這個類時,javac 會按照操做系統默認的編碼去解析字節流,若是你保存的源文件編碼與操做系統默認不一致,是可能出錯的,能夠在啓動 javac 命令時,附加-encoding <encoding>
選項來指明源代碼文件所使用的編碼。
# 編譯生成 .class 文件 javac -encoding utf-8 EncodingTest.java # 執行該類 java EncodingTest # 使用 jps 查看其 pid,而後用 jmap 把程序運行時內存的內容 dump 下來 jmap -dump:live,format=b,file=encoding_test.bin <pid> # 在 Linux/Mac 系統上,使用 xxd 命令以十六進制查看該文件,我這裏用管道傳給了 vim xxd encoding_test.bin | vim -
在 vim 中能夠看到下圖所示片斷
其中我用紅框標註部分就是上面 EncodingTest 類中字符串s
的內容,4e2d
是「中」的 code point,56fd
是「國」的 code point, 4eba
是「人」的 code point,0061
是「a」的 code point。而在 UTF-16 編碼中,0-66535之間的字符直接用兩個字節存儲,這也就證實了 Java 中的 Character
是使用 UTF-16 編碼的。
首先說下 Python 解釋器如何解析 Python 源程序。
在 Python 2 中,Python 解析器默認用 ASCII 編碼來讀取源程序,當程序中包含非 ASCII 字符時,解釋器會報錯,下面實驗在我 Mac 上用 python 2.7.6 進行:
$ cat str.py #!/usr/bin/env python a = "衆人過" $ python str.py File "str.py", line 2 SyntaxError: Non-ASCII character '\xe4' in file str.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
咱們能夠經過在源程序起始處用coding: name
或coding=name
來聲明源程序所用的編碼。
Python 3 中改變了這一行爲,解析器默認採用 UTF-8 解析源程序。
按理接下來應該介紹 Python 中對字符的處理了,可是發現這裏面東西太多了,介於本文篇幅緣由,這裏再也不介紹,後面有空能夠單獨寫篇文件介紹。你們感興趣的能夠參考下面的文章:
在咱們的 Web 瀏覽器接收到來自世界各地的 HTML/XML 時,也須要正確的編碼方案纔可以正常顯示網頁,在現代的 HTML5 頁面,通常經過下面的代碼指定
<meta charset="UTF-8">
4.0.1 以前的 HTML 頁面使用下面的代碼
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
XML 使用屬性標註其編碼
<?xml version="1.0" encoding="UTF-8" ?>
仔細想一想,這裏有個矛盾的地方,由於咱們須要事先知道某字節流的編碼才能正確解析該字節流,而這個字節流的編碼是保存在這段字節流中的,和「雞生蛋,蛋生雞」的問題有點像,其實這並非一個問題,由於大部分的編碼都是兼容 ASCII 編碼的,而這些 HTML/XML 開始處基本都是 ASCII 字符,因此採用瀏覽器默認的編碼方案便可解析出該字節流所聲明的編碼,在解析出該字節流所用編碼後,使用該編碼從新解析該字節流。
這篇文章我用了週末 2 天時間才完成,在 wiki 上搜索的資料時,須要消耗較大精力去整理,由於各個編碼都不是孤立存在的,要想完整了解某編碼,須要從起發展歷史開始,在不一樣編碼條目間來回切換,才能對其有深刻理解。這是我意料以外的。
字符串,這個既熟悉又陌生的東西,相信你們在看本文後,你們都可以對其有一全新的認識。