背景:
????????文本以及字符串處理在網(wǎng)絡(luò)應(yīng)用中隨處可見歹苦,面對(duì)不同的用戶接口調(diào)用方,以及不同的我們需要調(diào)用的接口,可能都會(huì)有不一樣的編碼規(guī)范仔蝌。
? ? ? ? 而我在以前的開發(fā)中,對(duì)于字符串的處理經(jīng)常會(huì)是以猜的方式來處理荒吏,encode敛惊、decode等各種方式都試一遍,發(fā)現(xiàn)哪個(gè)能解決問題就用哪個(gè)绰更,從來沒有真正的了解字符串相關(guān)的原理與邏輯瞧挤。
? ? ? ? 所以,最近通過閱讀《Fluent Python》一書以及一些源碼儡湾、博文特恬,好好了解了一下字符相關(guān)的原理以及在開發(fā)中的應(yīng)用,記錄一下學(xué)習(xí)過程中覺得有用的地方以及一些理解徐钠。
ASCII癌刽,Unicode 和 UTF-8之間的關(guān)系
ASCII碼
? ? ? ??ASCII是最早的編碼,美國發(fā)明尝丐,使用一個(gè)字節(jié)8位显拜,最多可以展示255個(gè)字符,但是ASCII編碼中第一位沒有使用摊崭,僅用字節(jié)的后七位表示了127個(gè)字符讼油。
Unicode?
? ? ? ? Unicode是一種所有符號(hào)的編碼,包含了世界上各種語言的符號(hào)呢簸,目前有一百多萬個(gè)符號(hào)在這個(gè)集合中矮台。
? ? ? ? 但是Unicode只是一個(gè)集合,是把所有的字符就放在這個(gè)集合里根时,它規(guī)定了符號(hào)的二進(jìn)制代碼瘦赫,卻沒有規(guī)定這個(gè)二進(jìn)制代碼應(yīng)該如何存儲(chǔ)。
? ? ? ? 因?yàn)閁nicode表示的字符大部分需要兩個(gè)字節(jié)蛤迎,甚至有的會(huì)更多确虱。如果沒有規(guī)定的存儲(chǔ)方式,或者合適的存儲(chǔ)方式替裆,統(tǒng)一用兩個(gè)字節(jié)來表示所有的字符校辩,那么ASCII碼中本來可以用一個(gè)字節(jié)表示的英文字母窘问,就也需要兩個(gè)字節(jié)表示(第一個(gè)字節(jié)全部是0),這樣如果目標(biāo)全是英文宜咒,就相當(dāng)于占用的存儲(chǔ)空間直接翻倍了惠赫。
UTF-8
? ??????UTF-8(UTF-16)是 Unicode 的實(shí)現(xiàn)方式之一。UTF-8是目前使用最廣的一種 Unicode 的實(shí)現(xiàn)方式故黑。
? ??????UTF-8編碼把一個(gè)Unicode字符根據(jù)不同的數(shù)字大小編碼成1-6個(gè)字節(jié)儿咱,英文字母被編碼成1個(gè)字節(jié),漢字通常是3個(gè)字節(jié)场晶,少量很生僻的字符會(huì)被編碼成4-6個(gè)字節(jié)混埠。這樣在全是英文的地方,就不會(huì)用浪費(fèi)空間诗轻,對(duì)于需要多字節(jié)的漢字钳宪,也能正常展示。
? ? ? ? UTF-8的編碼規(guī)則有一下兩條:
1)對(duì)于單字節(jié)的符號(hào)概耻,字節(jié)的第一位設(shè)為0使套,后面7位為這個(gè)符號(hào)的 Unicode 碼罐呼。因此對(duì)于英語字母鞠柄,UTF-8 編碼和 ASCII 碼是相同的。
2)對(duì)于n字節(jié)的符號(hào)(n > 1)嫉柴,第一個(gè)字節(jié)的前n位都設(shè)為1厌杜,第n + 1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10计螺。剩下的沒有提及的二進(jìn)制位夯尽,全部為這個(gè)符號(hào)的 Unicode 碼。
????????如上圖所示為Unicode符號(hào)與UTF-8的對(duì)應(yīng)關(guān)系舉例登馒。
? ? ? ? 識(shí)別UTF-8有多少字節(jié)的核心是:如果一個(gè)字節(jié)的第一位是0匙握,則這個(gè)字節(jié)單獨(dú)就是一個(gè)字符;如果第一位是1陈轿,則連續(xù)有多少個(gè)1圈纺,就表示當(dāng)前字符占用多少個(gè)字節(jié)。
? ? ? ? 因?yàn)閁TF-8在只使用一個(gè)字節(jié)的時(shí)候麦射,和ASCII是一樣的蛾娶,所以UTF-8是可以兼容ASCII的。
?綜上所述潜秋,有如下結(jié)論:
????????Unicode和ASCII是同一種概念蛔琅,是是一種編碼方式。而UTF-8峻呛,UTF-16等是一種存儲(chǔ)方式罗售,在存儲(chǔ)和傳輸上節(jié)約空間辜窑、提高性能的一種編碼形式。
字符相關(guān)在Python中的使用
《Fluent Python》:
Unicode 標(biāo)準(zhǔn)把字符的標(biāo)識(shí)和具體的字節(jié)表述進(jìn)行了如下的明確區(qū)分寨躁。
????????字符的標(biāo)識(shí)谬擦,即碼位,是0~1 114 111的數(shù)字(十進(jìn)制)朽缎,在Unicode 標(biāo)準(zhǔn)中以4~6 個(gè)十六進(jìn)制數(shù)字表示惨远,而且加前綴“U+”。例如话肖,字母A的碼位是U+0041北秽,歐元符號(hào)的碼位是U+20AC,高音譜號(hào)的碼位是U+1D11E最筒。在Unicode 6.3中(這是Python 3.4使用的標(biāo)準(zhǔn))贺氓,約10%的有效碼位有對(duì)應(yīng)的字符。
????????字符的具體表述取決于所用的編碼床蜘。編碼是在碼位和字節(jié)序列之間轉(zhuǎn)換時(shí)使用的算法辙培。在UTF-8編碼中,A(U+0041)的碼位編碼成單個(gè)字節(jié)\x41邢锯,而在UTF-16LE編碼中編碼成兩個(gè)字節(jié)\x41\x00扬蕊。再舉個(gè)例子,歐元符號(hào)(U+20AC)在UTF-8編碼中是三個(gè)字節(jié)——\xe2\x82\xac丹擎,而在UTF-16LE中編碼成兩個(gè)字節(jié):\xac\x20尾抑。
????????把碼位轉(zhuǎn)換成字節(jié)序列的過程是編碼,把字節(jié)序列轉(zhuǎn)換成碼位的過程是解碼蒂培。通俗點(diǎn)講再愈,在開發(fā)過程中會(huì)碰到兩種字符:存儲(chǔ)在機(jī)器中的是字節(jié)序列,展現(xiàn)出來是Unicode字符(人類可讀的文本)护戳。將機(jī)器讀取的文本轉(zhuǎn)為人類可讀的文本就是解碼(decode)翎冲,將人類可讀的文本轉(zhuǎn)為給機(jī)器讀取的文本就是編碼(encode)。
在我們項(xiàng)目中的應(yīng)用
? ? ? ? 在開發(fā)過程中媳荒,為了避免對(duì)接不同的接口或者平臺(tái)的時(shí)候出現(xiàn)編碼方式不同造成的報(bào)錯(cuò)抗悍,我們需要在整個(gè)系統(tǒng)中都統(tǒng)一同一套編碼方式。
? ? ? ? 我們使用tornado框架肺樟,框架中g(shù)et_argument方法默認(rèn)會(huì)將拿到的參數(shù)轉(zhuǎn)為unicode:
? ? ? ? 在我們項(xiàng)目的utils.py文件中檐春,一般也都會(huì)有針對(duì)字符串的編碼轉(zhuǎn)換的方法可以調(diào)用:
?????????在編寫腳本的時(shí)候,在文件頂部定義腳本的編碼方式:
? ? ? ? 在具體使用的地方么伯,也要根據(jù)具體需求將文本解碼為我們所使用的UTF-8疟暖。
Python2規(guī)范化字符編碼
? ??????Python2(從2.5開始)則默認(rèn)使用ASCII。因此直接創(chuàng)建的字符串一般默認(rèn)為ASCII,如果調(diào)用接口或者進(jìn)行需要UTF-8的處理俐巴,需要對(duì)字符串進(jìn)行解碼骨望。
? ? ? ? 在進(jìn)行字符串處理的時(shí)候,可能會(huì)遇到無法編碼或者解碼的報(bào)錯(cuò)欣舵,要根據(jù)具體情況處理擎鸠。
????????一般比較常見的是兩種錯(cuò)誤:UnicodeEncodeError(把字符串轉(zhuǎn)換成二進(jìn)制序列時(shí)),UnicodeDecodeError(把二進(jìn)制序列轉(zhuǎn)換成字符串時(shí))缘圈。這種情況是因?yàn)槟繕?biāo)編碼方式?jīng)]有沒有我們需要編碼/解碼的字符串中的某個(gè)字符或者符號(hào)劣光。在進(jìn)行測(cè)試或者自己的一些簡(jiǎn)單開發(fā)的時(shí)候,可以在使用encode的時(shí)候糟把,增加對(duì)errors的指定處理方式來跳過(實(shí)際開發(fā)中绢涡,需要讓異常正常拋出):
city.encode('cp437', errors='ignore') 跳過錯(cuò)誤。
city.encode('cp437', errors='replace') 將無法編碼的字符用?代替遣疯。
city.encode('cp437', errors='xmlcharrefreplace')把無法編碼的字符替換成XML實(shí)體雄可。
? ? ? ? 對(duì)于有些情況,還可能出現(xiàn)音字符的情況缠犀,對(duì)于同樣一個(gè)字符数苫,可能會(huì)有不同的Unicode編碼方式。如下圖:
? ??????此時(shí)辨液,可以使用unicodedata.normalize函數(shù)對(duì)字符串進(jìn)行Unicode規(guī)范化虐急。
????????這個(gè)函數(shù)的第一個(gè)參數(shù)是這4個(gè)字符串中的一個(gè):'NFC'、'NFD'室梅、'NFKC'和'NFKD'戏仓。我們可以主要使用前兩個(gè):NFC(Normalization Form C)使用最少的碼位構(gòu)成等價(jià)的字符串,NFD把組合字符分解成基字符和單獨(dú)的組合字符亡鼠。具體使用如下:
? ??????unicodedata中的combining()函數(shù)可以用來判斷一個(gè)字符是否是音字符,可以用來將文本中的音字符全部過濾掉敷待。
? ? ? ? 在處理字符串的時(shí)候间涵,還有一些方法可以直接對(duì)字符串進(jìn)行簡(jiǎn)單判斷,靈活運(yùn)用起來會(huì)大大提高開發(fā)效率榜揖。包括isdigital(), startswith(),endswith()等勾哩。