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