1. 程式人生 > >編碼總結

編碼總結

目錄

1. 基礎概念

1.1 位元組

位元組(Byte)是計算機中儲存資料的單元,一個位元組等於一個8位的位元,計算機中的所有資料,不論是磁碟檔案上的還是網路上傳輸的資料(文字、圖片、視訊、音訊檔案)都是由位元組組成的。

1.2 字元

你正在閱讀的這篇文章就是由很多個字元(Character)構成的,字元一個資訊單位,它是各種文字和符號的統稱,比如一個英文字母是一個字元,一個漢字是一個字元,一個標點符號也是一個字元。

1.3 字符集

字符集(Character Set)就是某個範圍內字元的集合,不同的字符集規定了字元的個數,比如 ASCII 字符集總共有128個字元,包含了英文字母、阿拉伯數字、標點符號和控制符。而 GB2312 字符集定義了7445個字元,包含了絕大部分漢字字元。常見的字符集有:ASCII及其擴充套件字符集,GB2312字符集,GBK字符集,UNICODE字符集等

1.4 字元碼

字元碼(Code Point)指的是字符集中每個字元的數字編號,例如 ASCII 字符集用 0-127 這連續的128個數字分別表示128個字元,"A" 的編號就是65。

1.5 字元編碼

字元編碼(Character Encoding)是將字符集中的字元碼對映為位元組流的一種具體實現方案,常見的字元編碼有 ASCII 編碼、UTF-8 編碼、GBK 編碼等。某種意義上來說,字符集與字元編碼有種對應關係,例如 ASCII 字符集對應 有 ASCII 編碼。ASCII 字元編碼規定使用單位元組中低位的7個位元去編碼所有的字元。例如"A" 的編號是65,用單位元組表示就是0×41,因此寫入儲存裝置的時候就是b'01000001'。

1.6 編碼、解碼

編碼的過程是將字元轉換成位元組流,解碼的過程是將位元組流解析為字元。

理解了這些基本的術語概念後,我們就可以開始討論計算機的字元編碼的演進過程了。

2. ASCII、Unicode和UTF-8的關係

2.1 ASCII(字符集)

我們知道,計算機內部,所有資訊最終都是一個二進位制值。每一個二進位制位(bit)有0和1兩種狀態,因此八個二進位制位就可以組合出256種狀態,這被稱為一個位元組(byte)。也就是說,一個位元組一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從00000000到11111111。

上個世紀60年代,美國製定了一套字元編碼,對英語字元與二進位制位之間的關係,做了統一規定。這被稱為 ASCII 碼,一直沿用至今。

最開始ASCII只定義了128個字元編碼,比如空格SPACE是32(二進位制00100000),大寫的字母A是65(二進位制01000001)。這128個符號(包括32個不能打印出來的控制符號),只佔用了一個位元組的後面7位,最前面的一位統一規定為0。

2.2 Unicode(字符集)

英語用128個符號編碼就夠了,但是用來表示其他語言,128個符號是不夠的。要處理中文顯然一個位元組是不夠的,至少需要兩個位元組,而且還不能和ASCII編碼衝突,全世界有上百種語言,各國有各國的標準,就會不可避免地出現衝突,結果就是,在多語言混合的文字中,顯示出來會有亂碼。

因此,Unicode應運而生。Unicode把所有語言都統一到一套編碼裡,他涵蓋了全球所有的文字和二進位制的對應關係,每個符號的編碼都不一樣,這樣就不會再有亂碼問題了。

Unicode規定如何編碼,解決了字元和二進位制的對應關係,但是使用unicode表示一個字元,太浪費空間。例如:利用unicode表示“Python”需要12個位元組才能表示,比原來ASCII表示增加了1倍。

由於計算機的記憶體比較大,並且字串在內容中表示時也不會特別大,所以內容可以使用unicode來處理,但是儲存和網路傳輸時一般資料都會非常多,那麼增加1倍將是無法容忍的!!!為了解決儲存和網路傳輸的問題,出現了UTF,我們最常用的就是UTF-8編碼。

Unicode編碼有不同的實現方式,比如在傳輸和儲存的過程中,“漢”字的Unicode編碼是6C49,我可以用4個ascii數字來傳輸、儲存這個編碼;也可以用utf-8編碼的3個連續的位元組E6 B1 89來表示它。只要通訊雙方都要預定好,正確解析即可。

2.3 UTF-8

UTF 是為unicode編碼 設計 的一種 在儲存 和傳輸時節省空間的編碼方案。其中UTF-8(Unicode Transformation Format)廣泛應用於網際網路,它是一種變長的字元編碼,可以根據具體情況用1-4個位元組來表示一個字元。比如英文字元這些原本就可以用ASCII碼錶示的字元用UTF-8表示時就只需要一個位元組的空間,和ASCII是一樣的。

1、如果你要傳輸的文字包含大量英文字元,用UTF-8編碼就能節省空間:

字元 ASCII Unicode UTF-8
A 01000001 00000000 01000001 01000001
x 01001110 00101101 11100100 10111000 10101101

從上面的表格還可以發現,UTF-8編碼有一個額外的好處,就是ASCII編碼實際上可以被看成是UTF-8編碼的一部分,所以,大量只支援ASCII編碼的歷史遺留軟體可以在UTF-8編碼下繼續工作。

2、對於多位元組(n個位元組)的字元,第一個位元組的前n為都設為1,第n+1位設為0,後面位元組的前兩位都設為10。剩下的二進位制位全部用該字元的unicode碼填充。

image

以漢字“好”為例,“好”對應的Unicode是597D,對應的區間是0000 0800--0000 FFFF,因此它用UTF-8表示時需要用3個位元組來儲存,597D用二進位制表示是: 0101100101111101,填充到1110xxxx 10xxxxxx 10xxxxxx得到11100101 10100101 10111101,轉換成16進位制:e5a5bd,因此“好”的Unicode"597D"對應的UTF-8編碼是"E5A5BD"

    中文        好
    unicode         0101   100101   111101
    編碼規則     1110xxxx 10xxxxxx 10xxxxxx
                --------------------------
    utf-8       11100101 10100101 10111101
                --------------------------
    16進位制utf-8     e   5    a   5    b   d

3. 計算機系統中的編碼

無論以什麼編碼在記憶體裡顯示字元,存到硬碟上都是2進位制。

ascii編碼(美國):
    l   0b1101100
    o   0b1101111
    v   0b1110110
    e   0b1100101
GBK編碼(中國):
    老   0b11000000 0b11001111
    男   0b11000100 0b11010000
    孩   0b10111010 0b10100010

Shift_JIS編碼(日本):
    私   0b10001110 0b10000100
    は   0b10000010 0b11001101

ks_c_5601-1987編碼(韓國):
    나   0b10110011 0b10101010
    는   0b10110100 0b11000010

TIS-620編碼(泰國):
    ฉัน  0b10101001 0b11010001 0b10111001
...

要注意的是,存到硬碟上時是以何種編碼存的,再從硬碟上讀出來時,就必須以何種編碼讀,要不然就亂了。

雖然有了unicode and utf-8 , 但是由於歷史問題,各個國家依然在大量使用自己的編碼,比如中國的windows,預設編碼依然是gbk,而不是utf-8。

基於此,如果中國的軟體出口到美國,在美國人的電腦上就會顯示亂碼,因為他們沒有gbk編碼。若想讓中國的軟體可以正常的在 美國人的電腦上顯示,只有以下2條路可走:

  • 1、讓美國人的電腦上都裝上gbk編碼
  • 2、把你的軟體編碼以utf-8編碼

但是這也是有問題

  • 1、全世界有幾百上千種編碼,顯然不可能專門為中國安裝gbk,第一條方案行不通
  • 2、所有軟體都以utf-8編碼,由於很多軟體已經開發出來了,重新編碼會花費巨大的精力。

但這並不是沒有解決方案,試想一箇中國人和一個韓國人溝通,兩個人並不會對方的語言,但是兩個人都會英語,那麼這就簡單了,直接用英語溝通就可以了。其實上面已經提到過了unicode字符集,這個字符集所有國家的電腦都能認識,假設中國使用gbk,美國人使用utf-8,只要當我們的程式碼從硬碟讀到記憶體中的時候,使用正確的解碼方式(比如GBK)讀取資料,然後將資料轉換成unicode存入記憶體,處理完之後,再使用正確的編碼方式(比如utf-8),顯示在瀏覽器上,這樣中文就可以正常展示了。

不論是Python3x、Java還是其他程式語言,Unicode編碼都成為語言的預設編碼格式,而資料最後儲存到介質中的時候,不同的介質可有用不同的方式,有些人喜歡用UTF-8,有些人喜歡用GBK,這都無所謂,只要平臺統一的編碼規範,具體怎麼實現並不關心。

image

4. python2.x中的編碼

注意:以下內容只限於python2.x

4.1 python的解析

Python的誕生時間比Unicode要早很多,Python的預設編碼是ASCII

>>> import sys
>>> sys.getdefaultencoding()
'ascii'

所以在Python原始碼檔案中如果不顯示地指定編碼的話,將出現語法錯誤

#test.py
print "學習編碼"

執行後會報錯

$ python test.py
  File "test.py", line 1
SyntaxError: Non-ASCII character '\xe5' in file test.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

為了在原始碼中支援非ASCII字元,必須在原始檔的第一行或者第二行顯示地指定編碼格式:

#!/usr/bin/python
# -*- coding: utf-8 -*-

有了上面的配置,當Python直譯器讀取原始碼檔案時,會按UTF-8編碼讀取,我們儲存檔案的時候也要按照utf-8的格式儲存,utf-8是儲存和讀取的中間橋樑,只有雙方遵守這個規則,才能正確的讀取和寫入。現在的IDE一般會有選項設定儲存檔案的編碼型別,改變聲明後同時換成宣告的編碼儲存,但文字編輯器控們需要小心。

4.2 python2.x中字串

首先我們要理解字元和位元組的區別,字元是用來顯示的,而位元組是儲存和傳輸時使用,網路傳輸的是位元組流,檔案儲存的也是位元組流,而編輯器要顯示檔案內容,就需要轉化為字元來顯示,字元和位元組之間的關係可以定義如下

encode(字元, 編碼方案) -> 位元組
decode(位元組, 編碼方案) -> 字元

可見encode和decode是一對逆向操作,它們都需要指定編碼方案,如果編碼方案不一致,則會操作失敗。

在Python裡有兩種型別的字串型別:位元組字串(str)unicode的字串一個位元組字串就是unicode經過編碼後的位元組組成的序列,有可能是 ascii, gbk, utf-8 等等中的任意一種,str中到底是用的哪一種編碼,取決於它所在的場景,跟 locale ,檔案編碼等有關係。

image

str和unicode都是basestring的子類。unicode可以通過對位元組串str使用正確的字元編碼進行解碼後獲得,str也可以通過對unicode的編碼得到。

程式碼

# coding: UTF-8

s = '好'
u = u'好'

print repr(s)
print repr(u)
print s
print u
print repr(s.decode('UTF-8'))
    
print s == u
print s.decode('UTF-8') == u

結果

'\xe5\xa5\xbd'
u'\u597d'
好
好
u'\u597d'


False
True

檔案儲存為utf8格式,輸出規範的十六進位制和ASCII碼。

$ hexdump -C test.py
00000000  23 20 63 6f 64 69 6e 67  3a 20 55 54 46 2d 38 0a  |# coding: UTF-8.|
00000010  0a 73 20 3d 20 27 e5 a5  bd 27 0a 75 20 3d 20 75  |.s = '...'.u = u|
00000020  27 e5 a5 bd 27 0a 0a 70  72 69 6e 74 20 72 65 70  |'...'..print rep|
00000030  72 28 73 29 0a 70 72 69  6e 74 20 72 65 70 72 28  |r(s).print repr(|
00000040  75 29 0a 70 72 69 6e 74  20 73 0a 70 72 69 6e 74  |u).print s.print|
00000050  20 75 0a 70 72 69 6e 74  20 72 65 70 72 28 73 2e  | u.print repr(s.|
00000060  64 65 63 6f 64 65 28 27  55 54 46 2d 38 27 29 29  |decode('UTF-8'))|
00000070  0a 0a 70 72 69 6e 74 20  73 20 3d 3d 20 75 0a 70  |..print s == u.p|
00000080  72 69 6e 74 20 73 2e 64  65 63 6f 64 65 28 27 55  |rint s.decode('U|
00000090  54 46 2d 38 27 29 20 3d  3d 20 75 0a              |TF-8') == u.|
0000009c

分析:

  • 1、首先檔案的儲存是位元組流,觀察上面的第二行和第三行,對照ascii表

第一段:

73 20 3d 20 27 e5 a5 bd 27

分別對照解釋

73->s, 20->空格,3d->等號,20->空格,27->單引號,`e5 a5  bd`->中文`好`,27->單引號

所以上面是 s = '好'

第二段:

75 20 3d 20 75 27 e5 a5 bd 27

分別對照解釋

73->s, 20->空格,3d->等號,20->空格,75->u, 27->單引號,`e5 a5  bd`->中文`好`,27->單引號

所以上面是 s = '好'

檔案儲存的時候是utf編碼的位元組流,我們讀取的時候按照utf-8編碼讀取是可以正確解析的。

  • 2、python2的預設編碼是ASCII,想寫中文,就必須宣告檔案頭的coding為gbk or utf-8, 宣告之後,python2直譯器以檔案頭宣告的編碼去解釋你的程式碼,但是載入到記憶體後,並不會主動幫你將str位元組串轉為unicode,也就是說,你的檔案編碼是utf-8,str位元組串載入到記憶體裡仍然為utf-8。Python2並不會自動的把檔案編碼轉為unicode存在記憶體裡,需要你自己人肉轉。但是python3自動把檔案編碼轉為unicode必定是呼叫了什麼方法,這個方法就是,decode(解碼) 和encode(編碼)
UTF-8 --> decode 解碼 --> Unicode
Unicode --> encode 編碼 --> GBK / UTF-8 ..

image

  • 3、上面的s,實際上是一個16進位制(\xe5\xa5\xbd)表示的二進位制位元組(11100101 10100101 10111101),這樣做的目的是可讀性更強,這就是位元組型別。u的16進位制(\u597d)對應的二進位制是(0101 100101 111101),兩者都讀入到記憶體中,顯然是不同的。如果按照s.decode('UTF-8')對s進行解碼得到了unicode,兩個對應的二進位制相同了,所以返回True。

4.3 如何識別編碼

chardet是一個非常優秀的編碼識別模組,通過pip 安裝:

pip install chardet

使用:

複製程式碼

>>> from chardet import detect

>>> a = "中文"

>>> detect(a)
{'confidence': 0.682639754276994, 'encoding': 'KOI8-R'}

4.4 讀寫檔案

內建的open()方法開啟檔案時,read()讀取的是str,讀取後需要使用正確的編碼格式進行decode()。write()寫入時,如果引數是unicode,則需要使用你希望寫入的編碼進行encode(),如果是其他編碼格式的str,則需要先用該str的編碼進行decode(),轉成unicode後再使用寫入的編碼進行encode()。如果直接將unicode作為引數傳入write()方法,Python將先使用原始碼檔案宣告的字元編碼進行編碼然後寫入。

# coding: UTF-8
 
f = open('test.txt')
s = f.read()
f.close()
print type(s) # <type 'str'>
# 已知是GBK編碼,解碼成unicode
u = s.decode('GBK')
 
f = open('test.txt', 'w')
# 編碼成UTF-8編碼的str
s = u.encode('UTF-8')
f.write(s)
f.close()

另外,模組codecs提供了一個open()方法,可以指定一個編碼開啟檔案,使用這個方法開啟的檔案讀取返回的將是unicode。寫入時,如果引數是unicode,則使用open()時指定的編碼進行編碼後寫入;如果是str,則先根據原始碼檔案宣告的字元編碼,解碼成unicode後再進行前述操作。相對內建的open()來說,這個方法比較不容易在編碼上出現問題。

# coding: GBK
 
import codecs
 
f = codecs.open('test.txt', encoding='UTF-8')
u = f.read()
f.close()
print type(u) # <type 'unicode'>
 
f = codecs.open('test.txt', 'a', encoding='UTF-8')
# 寫入unicode
f.write(u)
 
# 寫入str,自動進行解碼編碼操作
# GBK編碼的str
s = '漢'
print repr(s) # '\xba\xba'
# 這裡會先將GBK編碼的str解碼為unicode再編碼為UTF-8寫入
f.write(s) 
f.close()

5. 應用

在計算機記憶體中,統一使用Unicode編碼(萬國編碼),當需要儲存到硬碟或者需要傳輸的時候,就轉換為UTF-8編碼。

下面介紹一些例子

5.1 記事本

用記事本編輯的時候,從檔案讀取的UTF-8字元被轉換為Unicode字元到記憶體裡,編輯完成後,儲存的時候再把Unicode轉換為UTF-8儲存到檔案:

image

5.2 瀏覽網頁

瀏覽網頁的時候,伺服器會把動態生成的Unicode內容轉換為UTF-8再傳輸到瀏覽器:

image

5.3 vim編輯器

Vim 有四個跟字元編碼方式有關的選項,encoding、fileencoding、fileencodings、termencoding ,它們的意義如下:

  • encoding: Vim 內部使用的字元編碼方式,包括 Vim 的 buffer (緩衝區)、選單文字、訊息文字等。你可以用另外一種編碼來編輯和儲存檔案,如你的vim的encoding為utf-8,所編輯的檔案採用cp936編碼,vim會自動將讀入的檔案轉成utf-8(vim的能讀懂的方式),而當你寫入檔案時,又會自動轉回成cp936(檔案的儲存編碼).

  • fileencoding: Vim 中當前編輯的檔案的字元編碼方式,Vim 儲存檔案時也會將檔案儲存為這種字元編碼方式 (不管是否新檔案都如此)。

  • fileencodings: Vim自動探測fileencoding的順序列表, 啟動時會按照它所列出的字元編碼方式逐一探測即將開啟的檔案的字元編碼方式,並且將 fileencoding 設定為最終探測到的字元編碼方式。因此最好將Unicode 編碼方式放到這個列表的最前面,將拉丁語系編碼方式 latin1 放到最後面。

  • termencoding: Vim 所工作的終端 (或者 Windows 的 Console 視窗) 的字元編碼方式。如果vim所在的term與vim編碼相同,則無需設定。

我們來看看 Vim的多字元編碼方式支援是如何工作的。

  • 1、Vim 啟動,根據 .vimrc 中設定的 encoding 的值來設定 buffer、選單文字、訊息文的字元編碼方式。

  • 2、讀取需要編輯的檔案,根據 fileencodings 中列出的字元編碼方式逐一探測該檔案編碼方式。並設定 fileencoding 為探測到的,看起來是正確的 (注1) 字元編碼方式。

  • 3、對比 fileencoding 和 encoding 的值,若不同則呼叫 iconv 將檔案內容轉換為encoding 所描述的字元編碼方式,並且把轉換後的內容放到為此檔案開闢的 buffer 裡,此時我們就可以開始編輯這個檔案了。

  • 4、編輯完成後儲存檔案時,再次對比 fileencoding 和 encoding 的值。若不同,再次呼叫 iconv 將即將儲存的 buffer 中的文字轉換為 fileencoding 所描述的字元編碼方式,並儲存到指定的檔案中。建議 encoding 的值設定為utf-8。

.vimrc檔案參考配置如下:

:set encoding=utf-8
:set fileencodings=utf-8,gbk,big5,cp936,gb18030,gb2312,utf-16
:set fileencoding=utf-8
:set termencoding=utf-8

以上配置,先嚐試用utf-8進行解碼,如果用utf-8解碼到了一半出錯(所謂出錯的意思是某個地方無法用utf-8正確地解碼),那麼就從頭來用gbk重新嘗試解碼,如果gbk又出錯(注意gbk並不是像utf-8似的規則編碼,所以所謂的出錯只是說某個編碼沒有對應的有意義的字,比如0),就嘗試用big5,仍然出錯就嘗試用cp936。這一趟下來,如果中間的某次解碼從頭到尾都沒有出錯,那麼 vim就認為這個檔案是這個編碼的,不會再進行後面的嘗試了。

參考資料

https://www.cnblogs.com/freewater/archive/2011/08/26/2154602.html

https://blog.csdn.net/jq_ak47/article/details/51769841

https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819196283586a37629844456ca7e5a7faa9b94ee8000

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

http://www.cnblogs.com/fnng/p/5008884.html

https://liguangming.com/how-to-use-utf-8-with-python

https://segmentfault.com/a/1190000004625718

https://www.cnblogs.com/huxi/archive/2010/12/05/1897271.html

https://www.zhihu.com/question/31833164