碼農基本功:字符集和編碼
一、基本概念
我們在使用計算機時,主要閱讀并關注字符串中的數字、英文字符、中文字符、Emoji 表情等,但是計算機并不關注字符串的單個字符到底是什么意思,因為計算機最終存儲和傳輸的都是二進制比特數據。
所以這里先來看下字符、字符集、編碼等基本概念。
- 字符:人類肉眼可以閱讀的最小書寫單元:字母、數字、標點、漢字、符號、Emoji 表情等。
- 碼點 (數字):每個字符分配的唯一整數編號。
- 字符集:字符和碼點 (數字) 的映射關系。
- 編碼(編碼方案):如何設計和實現字符集的 “映射關系”。
二、ASCII
首先來看看 ASCII 編碼。
ASCII 是最早期的 使用 1 個字節 (byte) ,7 位 (bit) 編碼,最高位始終為 0,來表示常見的英文字符、阿拉伯數字、標點符號和控制符號等。
基本上,你在鍵盤上面看到的字符,都是 ASCII 字符,使用 0 - 127 表示。
因為編碼范圍是固定的,所以主流編程語言都內置了 ASCII 字符和數字互相轉化的 API,例如在 Python 中,可以通過 ord 和 chr 兩個函數來獲取數字與 ASCII 字符的對應關系。
# 獲取數字與 ASCII 字符的對應關系
print(ord('A')) # 65
print(chr(65)) # 'A'
再比如,我們可以快速獲取到小寫字母 a-z 對應的數字:
for x in range(ord('a'), ord('z') + 1):
print(x, chr(x))
1. 局限性
對于語言為英文的計算機用戶來說,ASCII 編碼已經基本夠用了,但是對于非英文用戶來說,ASCII 編碼所能表示的字符太有限了!例如,對于中文用戶來說,單單漢字就不止 128 個,還有像日本、韓國等其他有自己語言的國家用戶來說,ASCII 編碼也存在同樣的問題。
此外,還有像 Emoji 表情等更加個性化的符號,要作為字符本身進行傳遞,ASCII 編碼同樣無能為力。
2. 理想中的編碼方案
為了解決 ASCII 編碼的不足,理想情況下,應該設計一個可以包含世界上所有國家語言的字符編碼方案,這樣不同的國家都可以采用一種編碼方案。
同時,用戶無需關注和編碼相關的系統設置等 (例如使用不同的語言需要進行不同的編碼設置)。
最后,為了兼容已有的 ASCII 編碼,其他國家可以在 ASCII 編碼的基礎上進行延續,各自使用不同的 “數字區間” 來表示對應的字符。
例如,ASCII 編碼 使用 0 - 127 來表示,那么其他國家的語言編碼方案可以簡單設置為:
- 中文使用 10000 - 100000 來表示
- 日文使用 100000 - 110000 來表示
- 韓文使用 110000 - 120000 來表示
- 以此類推
三、Unicode
為了解決 ASCII 編碼表現能力不足的問題,由 The Unicode Standard 開發了一套業界標準字符集/編碼方案,為每種語言中的每個字符設定了唯一的二進制編碼,并且跨語言、跨平臺,這也就是 Unicode 全球字符集編碼方案,簡稱 Unicode。
具體到實現細節來說,Unicode 又可以分為 編碼方式 和 實現方式 兩個層次:
- 編碼方式 (標準/接口):Unicode 使用數字范圍 0-0x10FFFF 來映射世界上不同國家的所有字符,最多可以表示 1114112個 字符
- 實現方式 (具體實現):每個字符和對應的數字之間如何互相轉換,例如漢字的 中 固定使用數字 20013 來表示,但是中和 20013,這兩者之間的轉換方式可以由不同的方式來完成,例如 UTF-8、UTF-16、UTF-32 等等
從代碼的視角來看,Unicode 是接口,UTF-8 是具體實現。
下面是一些字符轉換為 Unicode 對應編碼 (數字) 的 Python 代碼示例。
def main():
# 輸出 Unicode 中對應的唯一數字 (也就是碼位)
print(ord('中')) # 20013
print(ord('??')) # 128512
print(ord('A')) # 65
print(ord('a')) # 97
print(ord('1')) # 49
# 輸出 Unicode 編碼 (十六進制) 表示
print(hex(ord('中'))) # 0x4e2d
print(hex(ord('??'))) # 0x1f600
print(hex(ord('A'))) # 0x41
print(hex(ord('a'))) # 0x61
print(hex(ord('1'))) # 0x31
局限性/問題:
Unicode 雖然為每個字符分配了唯一的 (數字) 編號,但是它本身僅定義字符和數字的映射關系,并沒有指定數字在計算機中的存儲和傳輸方式 (二進制表示),這時候,就需要有專門的編碼方案來實現 Unicode 提出的標準 (接口)。
四、UTF-8
最為人熟知的 Unicode 編碼實現方案就是 UTF-8 了,除此之外,還有 UTF-16 和 UTF-32,以及僅針對中文字符編碼的 GBK 和 GB2312。
雖然每種編碼格式都有自己的特點和使用場景,但 UTF-8 因其高效性和兼容性成為互聯網最常用的編碼方式,幾乎所有的現代操作系統、主流編程語言和應用程序開發都支持并且默認使用 UTF-8。
UTF-8 成功背后的原因:
1. 向后兼容
UTF-8 采用可變長度編碼方式,對 ASCII 字符只用 1 個字節表示,而對其他字符則使用 2、3 或 4 個字節,具有向后完全兼容 ASCII 的優勢。
2. 空間效率優化
對 ASCII 字符只用 1 個字節表示,而對其他字符則使用 2、3 或 4 個字節,不會造成任何存儲空間的浪費。
def main():
# UTF-8 使用 1 個字節表示英文
print(len("ab".encode('utf-8'))) # 2
print(len("12".encode('utf-8'))) # 2
# UTF-8 使用 3 個字節表示中文
print(len("中文".encode('utf-8'))) # 6
# UTF-8 使用 4 個字節表示 Emoji 表情
print(len("??".encode('utf-8'))) # 4
3. 可擴展性
UTF-8 可以表示所有 Unicode 字符,包括未來可能新出現的字符,例如新出現的字符超出了目前 Unicode 指定的標準范圍,那么只需要做兩件事情就可以在完全兼容已有字符的前提下,去開發新的字符:
- Unicode 對于新字符制定新標準 (新字符對應的數字)
- UTF-8 使用更多變長字節來表示新字符即可 (例如一個新字符使用 5 個字節來表示)
五、亂碼
講完了 ASCII、Unicode、UTF-8,再來順帶講一個,亂碼符號: ?。
? 其實是 Unicode 定義的一個有效字符,其具體表示方式為:
U+FFFD “replacement character” ?
在 Python 中,我們可以直接輸出:
def main():
# 第一種方式
print("\uFFFD") # ?
# 第二種方式
print(chr(0xFFFD)) # ?
在 Python 中,當解碼器遇到無法解析的字節時,會插入此字符以保證字符串有效性,而不會直接報錯,保證解碼過程不會中斷。當然,除非手動指定 errors 參數的值設置 'strict'。
print(text.decode('utf-8', errors='strict')) # 涓?鏂?
大多數開發者肯定都遇到過的亂碼符號:?,例如常見的業務場景:網絡數據傳輸、數據庫服務器/客戶端數據傳輸、網頁爬蟲數據解析。
這背后的本質原因就是: 解碼和編碼使用了不同/不兼容的字符集編碼方案,導致某些字符無法映射到目標編碼字符集中的有效碼點 (數字),于是被強制替換為 ?。
下面使用一個小例子進行說明。
def main():
"""
原始數據使用 UTF-8 編碼
解碼時卻使用 GBK
通過將 errors 參數的值設置為 replace
最終輸出亂碼
"""
s = "中文".encode('utf-8')
print(s.decode(encoding='gbk', errors='replace')) # 涓?鏂?
除此之外,部分編程語言截取一部分中文字符時,也會出現亂碼符號,例如在 Go 語言中,截斷中文字符時,就會出現亂碼,下面是一個對應的示例代碼。
package main
func main() {
// 因為字符串中有中文,所以這種方式會出現亂碼:
s := "Go 語言的優勢是什么?"
s2 := s[2:5]
println(s2) // ?
}
所以說,理解了編碼規則,自然也就理解了為什么會出現亂碼。
六、檢測字符串編碼工具類
不同字符的可以支持多種編碼方式,我們可以通過程序來檢測字符支持的編碼方式,下面是一個 Python 的實現示例代碼。
def detect_supported_encodings(text):
"""
檢測給定的 Unicode 字符串支持的編碼實現
"""
encodings = ['ascii', 'utf-8', 'gbk', 'big5', 'latin-1']
supported = []
for encoding in encodings:
try:
# 如果編碼成功,加入支持結果集
text.encode(encoding)
supported.append(encoding)
except UnicodeEncodeError:
continue
return supported
def main():
# 測試不同字符集的兼容性
print("ASCII字符兼容性:", detect_supported_encodings("Hello World!"))
# ['ascii', 'utf-8', 'gbk', 'big5', 'latin-1']
print("中文字符兼容性:", detect_supported_encodings("中文"))
# ['utf-8', 'gbk'] (GBK 可編碼常見漢字)
print("Emoji兼容性:", detect_supported_encodings("??"))
# ['utf-8'] (只有 UTF-8 支持 Emoji)
七、小結
- Unicode 是一個編碼字符集標準,規定每個字符和碼點 (數字) 的唯一映射關系,無法直接用于存儲和傳輸
- UTF-8 提供了一種完全兼容、效率優化、可擴展的字符編碼實現方式,并成為互聯網/軟件領域的默認字符編碼方式
最后,有個 Unicode 三明治原則,可以作為大多數應用程序開發的最佳實踐。