Python 編碼為什麼那麼蛋疼?

初學pyhton寫爬蟲程序,上手很快,但是解決編碼問題幾乎浪費了我90%的時間,百度、谷歌一大堆結果幾乎是千篇一律,

以下是通過一個資源鏈接獲取頭信息,並從中獲取資源名稱

# -*- coding: utf-8 -*-
__author__ = "CW"

import http.client

path = "d:/cache/"
def reqFileName(url):
url = url.replace("http://zz.dm1080p.com", "")
conn = http.client.HTTPConnection("zz.dm1080p.com")
conn.request("HEAD", url)
response = conn.getresponse()
fName = response.getheader("Content-Disposition")
fName = fName.replace("attachment; filename="", "").replace(""","")
return fName

print(reqFileName("http://zz.dm1080p.com/d.php?file1435636945HN37AV9.rar"))

該代碼段報如下錯誤:

builtins.UnicodeEncodeError: "gbk" codec can"t encode character "xc9" in position 13: illegal multibyte sequence

在網上找了很久沒有找到比較好的解決方案,

基本就是

fName.encode("gbk","ignore")/fName.encode("gbk","replace")/fName.encode("gb2312")


一旦走上了編程之路,如果你不把編碼問題搞清楚,那麼它將像幽靈一般糾纏你整個職業生涯,各種靈異事件會接踵而來,揮之不去。只有充分發揮程序員死磕到底的精神你才有可能徹底擺脫編碼問題帶來的煩惱,我第一次遇到編碼問題是寫 JavaWeb 相關的項目,一串字元從瀏覽器遊離到應用程序代碼中,翻江倒海沉浸到資料庫中,隨時隨地都有可能踩到編碼的地雷。第二次遇到編碼問題就是學 Python 的時候,在爬取網頁數據時,編碼問題又出現了,當時我的心情是奔潰的,用時下最ing的一句話就是:「我當時就懵逼了」。為了搞清字元編碼,我們得從計算機的起源開始,計算機中的所有數據,不論是文字、圖片、視頻、還是音頻文件,本質上最終都是按照類似 01010101 的數字形式存儲的。我們是幸運的,我們也是不幸的,幸運的是時代賦予了我們都有機會接觸計算機,不幸的是,計算機不是我們國人發明的,所以計算機的標準得按美帝國人的習慣來設計,那麼最開始計算機是通過什麼樣的方式來表現字元的呢?這要從計算機編碼的發展史說起。

ASCII

每個做 JavaWeb 開發的新手都會遇到亂碼問題,每個做 Python 爬蟲的新手都會遇到編碼問題,為什麼編碼問題那麼蛋疼呢?這個問題要從1992年 Guido van Rossum 創造 Python 這門語言說起,那時的 Guido 絕對沒想到的是 Python 這門語言在今天會如此受大家歡迎,也不會想到計算機發展速度會如此驚人,儘管 Guido 在當初設計這門語言時是不需要關心編碼的,因為在英語世界裡,字元的個數非常有限,26個字母(大小寫)、10個數字、標點符號、控制符,也就是鍵盤上所有的鍵所對應的字元加起來也不過是一百多個字元而已,這在計算機中用一個位元組的存儲空間來表示一個字元是綽綽有餘的,因為一個位元組相當於8個比特位,8個比特位可以表示256個符號。於是聰明的美國人就制定了一套字元編碼的標準叫ASCII(American Standard Code for Information Interchange),每個字元都對應唯一的一個數字,比如字元A對應的二進位數值是01000001,對應的十進位就是65。最開始ASCII只定義了128個字元編碼,包括96個文字和32個控制符號,一共128個字元只需要一個位元組的7位就能表示所有的字元,因此 ASCII 只使用了一個位元組的後7位,最高位都為0。每個字元與ASCII碼的對應關係可查看網站ascii-code。

EASCII(ISO/8859-1)

然而計算機慢慢地普及到其他西歐地區時,他們發現還有很多西歐所特有的字元是 ASCII 編碼表中沒有的,於是後來出現了可擴展的 ASCII 叫 EASCII ,顧名思義,它是在ASCII的基礎上擴展而來,把原來的7位擴充到8位,它完全兼容ASCII,擴展出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。然而 EASCII 時代是一個混亂的時代,大家沒有統一標準,他們各自把最高位按照自己的標準實現了自己的一套字元編碼標準,比較著名的就有 CP437, CP437 是 Windows 系統中使用的字元編碼,如下圖:

另外一種被廣泛使用的 EASCII 還有 ISO/8859-1(Latin-1),它是國際標準化組織(ISO)及國際電工委員會(IEC)聯合制定的一系列8位元字符集的標準,ISO/8859-1 只繼承了 CP437 字元編碼的128-159之間的字元,所以它是從160開始定義的,不幸的是這些眾多的 ASCII 擴充字集之間互不兼容。

GBK

隨著時代的進步,計算機開始普及到千家萬戶,比爾蓋茨讓每個人桌面都有一台電腦的夢想得以實現。但是計算機進入中國不得不面臨的一個問題就是字元編碼,雖然咱們國家的漢字是人類使用頻率最多的文字,漢字博大精深,常見的漢字就有成千上萬,這已經大大超出了 ASCII 編碼所能表示的字元範圍了,即使是 EASCII 也顯得杯水車薪,於是聰明的中國人自己弄了一套編碼叫 GB2312,又稱GB0,1981由中國國家標準總局發布。GB2312 編碼共收錄了6763個漢字,同時他還兼容 ASCII,GB 2312的出現,基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率,不過 GB2312 還是不能100%滿足中國漢字的需求,對一些罕見的字和繁體字 GB2312 沒法處理,後來就在GB2312的基礎上創建了一種叫 GBK 的編碼,GBK 不僅收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。同樣 GBK 也是兼容 ASCII 編碼的,對於英文字元用1個位元組來表示,漢字用兩個位元組來標識。

Unicode

對於如何處理中國人自己的文字我們可以另立山頭,按照我們自己的需求制定一套編碼規範,但是計算機不止是美國人和中國人用啊,還有歐洲、亞洲其他國家的文字諸如日文、韓文全世界各地的文字加起來估計也有好幾十萬,這已經大大超出了ASCII碼甚至GBK所能表示的範圍了,況且人家為什麼用採用你GBK標準呢?如此龐大的字元庫究竟用什麼方式來表示好呢?於是統一聯盟國際組織提出了Unicode編碼,Unicode的學名是"Universal Multiple-Octet Coded Character Set",簡稱為UCS。Unicode有兩種格式:UCS-2和UCS-4。UCS-2就是用兩個位元組編碼,一共16個比特位,這樣理論上最多可以表示65536個字元,不過要表示全世界所有的字元顯示65536個數字還遠遠不過,因為光漢字就有近10萬個,因此Unicode4.0規範定義了一組附加的字元編碼,UCS-4就是用4個位元組(實際上只用了31位,最高位必須為0)。理論上完全可以涵蓋一切語言所用的符號。世界上任何一個字元都可以用一個Unicode編碼來表示,一旦字元的Unicode編碼確定下來後,就不會再改變了。但是Unicode有一定的局限性,一個Unicode字元在網路上傳輸或者最終存儲起來的時候,並不見得每個字元都需要兩個位元組,比如一字元「A「,用一個位元組就可以表示的字元,偏偏還要用兩個位元組,顯然太浪費空間了。第二問題是,一個Unicode字元保存到計算機裡面時就是一串01數字,那麼計算機怎麼知道一個2位元組的Unicode字元是表示一個2位元組的字元呢,還是表示兩個1位元組的字元呢,如果你不事先告訴計算機,那麼計算機也會懵逼了。Unicode只是規定如何編碼,並沒有規定如何傳輸、保存這個編碼。例如「漢」字的Unicode編碼是6C49,我可以用4個ascii數字來傳輸、保存這個編碼;也可以用utf-8編碼的3個連續的位元組E6 B1 89來表示它。關鍵在於通信雙方都要認可。因此Unicode編碼有不同的實現方式,比如:UTF-8、UTF-16等等。這裡的Unicode就像英語一樣,做為國與國之間交流世界通用的標準,每個國家有自己的語言,他們把標準的英文文檔翻譯成自己國家的文字,這是實現方式,就像utf-8。

UTF-8

UTF-8(Unicode Transformation Format)作為Unicode的一種實現方式,廣泛應用於互聯網,它是一種變長的字元編碼,可以根據具體情況用1-4個位元組來表示一個字元。比如英文字元這些原本就可以用ASCII碼錶示的字元用UTF-8表示時就只需要一個位元組的空間,和ASCII是一樣的。對於多位元組(n個位元組)的字元,第一個位元組的前n為都設為1,第n+1位設為0,後面位元組的前兩位都設為10。剩下的二進位位全部用該字元的unicode碼填充。

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

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

PYTHON字元編碼

現在總算把理論說完了。再來說說Python中的編碼問題。Python的誕生時間比Unicode要早很多,Python的默認編碼是ASCII

&>&>&> import sys
&>&>&> sys.getdefaultencoding()
"ascii"

所以在Python源代碼文件中如果不顯示地指定編碼的話,將出現語法錯誤

#test.py
print "你好"

上面是test.py腳本,運行 python test.py 就會包如下錯誤:

File 「test.py」, line 1 yntaxError: Non-ASCII character 『xe4′ in file test.py on line 1, but no encoding declared; see Welcome to Python.org ps/pep-0263.html for details

為了在源代碼中支持非ASCII字元,必須在源文件的第一行或者第二行顯示地指定編碼格式:

# coding=utf-8

或者是:

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

在python中和字元串相關的數據類型,分別是strunicode兩種,他們都是basestring的子類,可見str與unicode是兩種不同類型的字元串對象。

basestring
/
/
str unicode

對於同一個漢字「好」,用str表示時,它對應的就是utf-8編碼的"xe5xa5xbd",而用unicode表示時,他對應的符號就是u"u597d",與u"好"是等同的。需要補充一點的是,str類型的字元其具體的編碼格式是UTF-8還是GBK,還是其他格式,根據操作系統相關。比如在Windows系統中,cmd命令行中顯示的:

# windows終端
&>&>&> a = "好"
&>&>&> type(a)
&
&>&>&> a
"xbaxc3"

而在Linux系統的命令行中顯示的是:

# linux終端
&>&>&> a="好"
&>&>&> type(a)
&
&>&>&> a
"xe5xa5xbd"

&>&>&> b=u"好"
&>&>&> type(b)
&
&>&>&> b
u"u597d"

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

str與unicode的轉換

那麼在Python中str和unicode之間是如何轉換的呢?這兩種類型的字元串類型之間的轉換就是靠這兩個方法decode和encode。

#從str類型轉換到unicode
s.decode(encoding) =====&> & to &
#從unicode轉換到str
u.encode(encoding) =====&> & to &

&>&>&> c = b.encode("utf-8")
&>&>&> type(c)
&
&>&>&> c
"xe5xa5xbd"

&>&>&> d = c.decode("utf-8")
&>&>&> type(d)
&
&>&>&> d
u"u597d"

這個"xe5xa5xbd"就是unicode u"好"通過函數encode編碼得到的UTF-8編碼的str類型的字元串。反之亦然,str類型的c通過函數decode解碼成unicode字元串d。

str(s)與unicode(s)

str(s)和unicode(s)是兩個工廠方法,分別返回str字元串對象和unicode字元串對象,str(s)是s.encode(『ascii』)的簡寫。實驗:

&>&>&> s3 = u"你好"
&>&>&> s3
u"u4f60u597d"
&>&>&> str(s3)
Traceback (most recent call last):
File "&", line 1, in &
UnicodeEncodeError: "ascii" codec can"t encode characters in position 0-1: ordinal not in range(128)

上面s3是unicode類型的字元串,str(s3)相當於是執行s3.encode(『ascii』)因為「你好」兩個漢字不能用ascii碼來表示,所以就報錯了,指定正確的編碼:s3.encode(『gbk』)或者s3.encode("utf-8")就不會出現這個問題了。類似的unicode有同樣的錯誤:

&>&>&> s4 = "你好"
&>&>&> unicode(s4)
Traceback (most recent call last):
File "&", line 1, in &
UnicodeDecodeError: "ascii" codec can"t decode byte 0xc4 in position 0: ordinal not in range(128)
&>&>&>

unicode(s4)等效於s4.decode(『ascii』),因此要正確的轉換就要正確指定其編碼s4.decode(『gbk』)或者s4.decode("utf-8")。

亂碼

所有出現亂碼的原因都可以歸結為字元經過不同編碼解碼在編碼的過程中使用的編碼格式不一致,比如:

# encoding: utf-8

&>&>&> a="好"
&>&>&> a
"xe5xa5xbd"
&>&>&> b=a.decode("utf-8")
&>&>&> b
u"u597d"
&>&>&> c=b.encode("gbk")
&>&>&> c
"xbaxc3"
&>&>&> print c
??

utf-8編碼的字元『好』佔用3個位元組,解碼成Unicode後,如果再用gbk來解碼後,只有2個位元組的長度了,最後出現了亂碼的問題,因此防止亂碼的最好方式就是始終堅持使用同一種編碼格式對字元進行編碼和解碼操作。

其他技巧

對於如unicode形式的字元串(str類型):

s = "idpythonu003d215903184u0026indexu003d0u0026stu003d52u0026sid』

轉換成真正的unicode需要使用:

s.decode("unicode-escape")

測試:

&>&>&> s = "idu003d215903184u0026indexu003d0u0026stu003d52u0026sidu003d95000u0026i"
&>&>&> print(type(s))
&
&>&>&> s = s.decode("unicode-escape")
&>&>&> s
u"id=215903184index=0st=52sid=95000i"
&>&>&> print(type(s))
&
&>&>&>

以上代碼和概念都是基於Python2.x。

參考:

  • PEP 263 -- Defining Python Source Code Encodings
  • http://www.liaoxuefengcom/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819196283586a37629844456ca7e5a7faa9b94ee8000

說到底,覺得蛋疼只是因為自己編碼知識沒學好。

這裡一些答案說的不錯,但這種問題光從Python2本身的角度去回答意義並不大。如果沒有理解字元編碼的模型與原理,很難說以後換個語言換個環境就不會重蹈覆轍。進程、線程、Socket這些東西在各個語言中形式各異,但核心模型與原理都一樣。字元編碼亦然,它的核心知識並不是和Python綁定的。這裡把以前的筆記貼在這裡,供後人鑒之。

======Update

沒耐心看基礎的可以直接拉到最後,直接看Python2相關編碼問題的解釋。

現代編碼模型

Tags: 字符集

Abstract: 字元編碼,在計算機導論中經常作為開門的前幾個話題來講,然而很多CS教材對這個話題基本都是走馬觀花地幾頁帶過。導致了許多人對如此重要且基本的概念認識模糊不清。直到在實際編程中,尤其是遇到多語言、國際化的問題,被虐的死去活來之後才痛下決心去重新鑽研。諸如此類極其基礎卻又容易被人忽視的的知識點還有:大小端表示,浮點數細節,正則表達式,日期時間處理等。本文是系列的第一篇,旨在闡明字元編碼這個大坑中許多糾纏不清的概念。

基本概念

現代編碼模型自底向上分為五個層次:

  • 抽象字元表 ACR (Abstract Character Repertoire)

  • 編碼字符集 CCS (Coded Character Set)

  • 字元編碼表 CEF (Character Encoding Form)

  • 字元編碼方案 CES (Character Encoding Schema)

  • 傳輸編碼語法 TES (Transfer Encoding Syntax)

  • [現代編碼模型-Wiki](https://zh.wikipedia.org/wiki/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81#.E7.8E.B0.E4.BB.A3.E7.BC.96.E7.A0.81.E6.A8.A1.E5.9E.8B)

  • [Unicode術語表](Glossary)

抽象字符集 ACR

抽象字符集是現代編碼模型的最底層,它是一個集合,通過枚舉指明了所屬的所有抽象字元。但是要了解抽象字符集是什麼,我們首先需要了解什麼是**字元**與**抽象字元**

  • 字元 (character, char)

字元是指字母、數字、標點、表意文字(如漢字)、符號、或者其他文本形式的書寫「原子」。

例: `a`,`啊`,`あ`,` α`,`Д`等,都是抽象的字元。

  • 抽象字元 (Abstract Character)

抽象字元就是抽象的字元。

像`a`這樣的字元是有形的,但在計算機中,有許多的字元是空白的,甚至是不可列印的。比如ASCII字符集中的NULL,就是一個抽象字元。

注意x00,00,NULL,0 這些寫法都只是這個抽象字元的某種表現形式,而不是這個抽象字元本身。

  • 抽象字符集 ACR (Abstract Character Repertoire)

抽象字符集顧名思義,指的是**抽象字元的集合**。

已經有了很多標準的字符集定義: Character Sets

比如US-ASCII, UCS(Unicode), GBK這些我們耳熟能詳的名字,都是(或者至少是)抽象字符集。

US-ASCII定義了128個抽象字元的集合。GBK挑選了兩萬多個中日韓漢字和其他一些字元組成字符集,而UCS則嘗試去容納一切的抽象字元。它們都是抽象字符集。

抽象字元 英文字母`A`同時屬於US-ASCII, UCS, GBK這三個字符集。

抽象字元 中文文字`蛤`不屬於US-ASCII,屬於GBK字符集,也屬於UCS字符集。

抽象文字 Emoji ` `不屬於US-ASCII與GBK字符集,但屬於UCS字符集。

集合的一個重要特性,就是無序性。

集合中的元素都是無序的,所以抽象字符集中的字元都是**無序的**。

抽象字符集對應的是python中的set的概念。

例:我可以自己定義一個字元的集合,叫這個集合為haha字符集。

haha_acr = { "a", "吼", "あ", " α", "Д" }

大家覺得抽象字符集這個名字太啰嗦,所以有時候直接叫它字符集

最後需要注意一點的是,抽象字符集也是有開放封閉之分的。

ASCII抽象字符集定義了128個抽象字元,再也不會增加。這是一個封閉字符集。

Unicode嘗試收納所有的字元,一直在不斷地擴張之中。最近(2016.06)Unicode9.0.0已經收納了128,237個字元,並且未來仍然會繼續增長,這是一個開放的字符集。

  • 編碼字符集 CCS (Coded Character Set)

Coded Character Set. A character set in which each character is assigned a numeric code point. Frequently abbreviated as character set, charset, or code set; the acronym CCS is also used.

編碼字符集是一個每個所屬字元都分配了碼位的抽象字符集

編碼字符集(CCS)也經常簡單叫做字符集(Character Set)。這樣的叫法經常會將抽象字符集ACR編碼字符集CCS搞混。不過大多時候人們也不在乎這種事情。

抽象字符集是抽象字元的集合,而集合是無序的。

無序的抽象字符集並沒有什麼卵用,因為我們只能判斷某個字元是否屬於某個字符集,卻無法方便地引用,指稱這個集合中的某個特定元素。

以下兩個表述指稱了同一個字元,但哪一種更方便呢?

  • ASCII(抽象)字符集中的那個代表什麼都沒有的通常表示為NULL的抽象字元

  • ASCII(編碼)字符集中的0號字元

為了更好的描述,操作字元,我們可以為抽象字符集中的每個字元關聯一個數字編號,這個數字編號稱之為碼位(Code Point)

通常根據習慣,我們為字元分配的碼位通常都是非負整數,習慣上用十六進位表示。且一個編碼字符集中字元與碼位的映射是一一映射

舉個例子,為haha抽象字符集進行編碼,就可以得到haha編碼字符集。

haha_ccs = { "a" : 0x0, "吼":0x1 , "あ":0x2 , " α":0x3 , "Д":0x4 }

字元`吼`與碼位`0x1`關聯,這時候,在haha編碼字符集中,`吼`就不再是一個單純的抽象字元了,而是一個編碼字元(Coded Chacter),且擁有碼位 0x1。

如果說抽象字符集是一個Set,那麼編碼字符集就可以類比為一個Dict。

CCS = { k:i for i, k in enumerate(ACR)}

它的key是字元,而value則是碼位。至於碼位具體是怎樣分配的,這個規律就不好說了。比如為什麼我想給haha_ccs的`吼`字元分配碼位`0x1`而不是`0x23333`呢?因為這樣能續一秒,反映了CCS設計者的主觀趣味。

編碼字符集有許許多多,但最出名的應該就是US-ASCII和UCS了。ASCII因為太有名了,所以就不說了。

  • 統一字符集 UCS (Universal Character Set)

最常見的編碼字符集就是統一字符集 UCS

UCS. Acronym for Universal Character Set, which is specified by International Standard ISO/IEC 10646, which is equivalent in repertoire to the Unicode Standard.

UCS就是統一字符集,就是由 ISO/IEC 10646所定義的編碼字符集。通常說的「Unicode字符集」指的就是它。不過需要辨明的一點是,「Unicode」這個詞本身指的是一系列用於計算機表示所有語言字元的標準

基本上所有能在其他字符集中遇到的符號,都可以在UCS中找到,而一些新的不屬於任何傳統字符集的字元,例如Emoji,也會收錄於UCS中。這也是UCS地位超然的原因。

舉個例子,UCS中碼位為0x4E00~0x9FFF的碼位,就用於表示「中日韓統一表意文字

大家喜聞樂見的Emoji表情則位於更高的碼位,例如「哭笑」

在UCS中的碼位就是0x1F602。

(如果這個站點不支持Emoji,你就看不到這個字元了,上面那個是圖片…)

&>&>&> " ".decode("utf-8")
u"U0001f602"

關於CCS,這些介紹大抵足夠了。

不過還有一個細節需要注意。按照目前最新Unicode 9.0.0的標準,UCS理論上收錄了128,237個字元,也就是0x1F4ED個。不過如果進行一些嘗試會發現,實際能用的最大的碼位點在0x1F6D0 ,也就是128,720,竟然超過了收錄的字元數,這又是為什麼呢?

碼位是非負整數沒錯,但這不代表它一定是連續分配的。

出現這種情況只有一個原因,那就是UCS的碼位分配不是連續的,中間有一段空洞,即存在一段碼位,沒有分配對應的字元。實際上,UCS實際分配的碼位0x0000~0x0xD7FF 0xE000~0x10FFFF 這兩段。中間0xD800~0xDFFF這2048個碼位留作它用,並不對應實際的字元。如果直接嘗試去輸出這個碼位段的"字元",結果會告訴你這是個非法字元。例如在python2中嘗試列印碼位0xDDDD的字元:

&>&>&> print u"UDDDD"
File "&", line 1
SyntaxError: (unicode error) "unicodeescape" codec can"t decode bytes in position 0-5: truncated UXXXXXXXX escape

  • 0x0000~0xD7FF | 0xE000~0x10FFFF 稱為Unicode標量值(Unicode scala value)

  • 0xD800~0xDBFF 稱為High-surrogate

  • 0xDC00~0xDFFF 稱為Low-surrogate

Unicode標量值就是實際存在對應字元的碼位。

為什麼中間一端的碼位會留空,則是為了方便下一個層次的字元編碼表CEF的UTF-16而處理的。

  • 其他編碼字符集

除了ASCII與UCS,世界上還有許許多多的字符集。

在US-ASCII誕生與Unicode誕生之間,很多英語之外的字元無法在計算機中表示。

大家八仙過海各顯神通,定義了許許多多其他的字符集。

例如GBK字符集,以及其近似實現 Code Page 936。

這些字符集中的字元,最後都匯入了Unicode中。

  • 字元編碼表 CEF (Character Encoding Form)

Unicode Encoding Form. A character encoding form that assigns each Unicode scalar value to a unique code unit sequence. The Unicode Standard defines three Unicode encoding forms: UTF-8, UTF-16, and UTF-32

現在我們擁有一個編碼字符集了,Let"s say: UCS

這個字符集中的每個字元都有一個非負整數碼位與之一一對應。

看上去很好,既然計算機可以存儲整數,而現在字元已經能表示為整數,我們是不是可以說,用計算機存儲字元的問題已經得到了解決呢?

慢!還有一個問題沒有解決。

在講抽象字符集ACR的時候曾經提起,UCS是一個開放字符集,未來可能有更多的符號加入到這個字符集中來。也就是說UCS需要的碼位,理論上是無限的。

但計算機整形能表示的整數範圍是有限的。譬如,一個位元組的無符號單位元組整形(unsigned char, uint8)能夠表示的碼位只有0~0xFF,共256個;一個無符號短整形(unsigned short, uint16)的可用碼位只有0~0xFFFF,共65536個;而一個標準整形(unsigned int, uint32)能表示的碼位只有0~0xFFFFFFFF,共4294967296個。

雖然就目前來看,UCS收錄的符號總共也就十多萬個,用一個uint可以表示幾十億個字元呢。但誰知道哪天制定Unicode標準的同志們不會玩心大發造幾十億個Emoji加入UCS中。所以說到底,一對有限與無限的矛盾,必須通過一種方式進行調和。這個解決方案,就是字元編碼表(Character Encoding Form)

字元編碼表將碼位(Code Point)映射為碼元序列(Code Unit Sequences)。

對於Unicode而言,字元編碼表將Unicode標量值(Unicode scalar value)一一映射為碼元序列(Code Unit Sequences)

  • 碼元

Code unit: The minimal bit combination that can represent a unit of encoded text for processing or interchange.

碼元是能用於處理或交換編碼文本的最小比特組合。通常計算機處理字元的碼元為一位元組,即8bit。同時因為計算機中char其實是一種整形,而整形的計算往往以計算機的字長作為一個基礎單元,通常來講,也就是4位元組。

Unicode定義了三種不同的CEF,分別採用了1位元組,2位元組,4位元組的碼元,正好對應了計算機中最常見的三種整形長度:

在Unicode中,指定了三種標準的字元編碼表,UTF-8, UTF-16, UTF-32。分別將Unicode標量值映射為比特數為8、16、32的碼元的序列。

UTF-8的碼元為uint8, UTF-16的碼元為uint16, UTF-32的碼元為uint32。

當然也有一些非標準的CEF,如UCS-2,UCS-4,在此不多介紹。

需要注意一點的是,CEF將碼位映射為碼元序列。這個映射必須是一一映射(雙射)

因為當使用CEF進行編碼(Encode)時,是將碼位映射為碼元序列。

而當使用CEF進行解碼(Decode)時,是將碼元序列還原為碼位。

為了保證兩個過程都不出現歧義,必須保證CEF是一個雙射。

知道了字元編碼表CEF是什麼還不夠,我們還需要知道它是怎麼做的。

即:如何將一個無限大的整數,一一映射為指定字寬的碼元序列

這個問題可以通過變長編碼來解決:無論是UTF-8還是UTF-16,本質思想都是通過預留標記位來指示碼元序列的長度,從而實現變長編碼。

各個CEF的細節我建議參看維基百科

  • [UTF-8 ](https://zh.wikipedia.org/wiki/UTF-8 )
  • [UTF-16](https://zh.wikipedia.org/wiki/UTF-16 )
  • [UTF-32](https://zh.wikipedia.org/wiki/UTF-32)

寫的相當清楚,我就沒必要在此再寫一遍了。

更深入學習方式就是直接閱讀[Unicode9.0.0 Standard](Unicode 9.0.0)

舉個例子:

  • 字元編碼方案 CES (Character Encoding Schema)

Unicode encoding scheme: A specified byte serialization for a Unicode encoding

form, including the specification of the handling of a byte order mark (BOM), if

allowed.

簡單說,字元編碼方案 CES 等於 字元編碼表CEF 加上位元組序列化的方案。

通過字元編碼表CEF,我們已經可以將字元轉為碼元序列。無論是哪種UTF-X的碼元,都可以找到計算機中與之對應的整形存放。那麼現在我們能說存儲處理交換字元這個問題解決了嗎?

還不行。

假設一個字元按照UTF16拆成了若干個碼元組成的碼元序列,因為每個碼元都是一個unsigned short,實際上是兩個位元組。因此將碼元序列化為位元組序列的時候,就會遇到一些問題。

  • 大小端序問題:每個碼元究竟是高位位元組在前還是低位位元組在前呢?

  • 位元組序標記問題:另一個程序如何知道當文本是什麼端序的呢?

這些都是CEF需要操心的問題。

對於網路交換和本地處理,大小端序各有優劣。這個問題不屬於本文範疇。

位元組序標記BOM (Byte Order Mark),則是放置於編碼位元組序列開始處的一段特殊位元組序列,用於表示文本序列的大小端序。

對於這兩個問題的不同答案,在3種CEF:UTF-8,UTF-16,UTF-32上。

Unicode實際上定義了 7種 字元編碼方案CES

  1. UTF-8

  2. UTF-16LE

  3. UTF-16BE

  4. UTF-16

  5. UTF-32LE

  6. UTF-32BE

  7. UTF-32

其中UTF-8因為已經採用位元組作為碼元了,所以實際上不存在位元組序的問題。其他兩種CES嘛,都有一個大端版本一個小端版本,還有一個隨機應變大小端帶BOM的版本。

下面給一個Python編碼的小例子,將Emoji:哭笑 轉換為各種CES。

這裡也出現一個問題,歷史上字元編碼方案(Character Encoding Schema)曾經就是指UTF(Unicode Transformation Formats)。所以UTF-X到底是屬於字元編碼方案CES還是屬於字元編碼表CEF是一個模稜兩可的問題。UTF-X可以同時指代字元編碼表CEF或者字元編碼方案CES。

UTF-8問題還好,因為UTF-8的位元組序列化方案太樸素了,以至於CES和CEF都沒什麼區別。但其他兩種:UTF-16,UTF-32,就比較棘手了。當我們說UTF-16時,既可以指代UTF-16字元編碼表,又可以指代UTF-16字元編碼方案。所以當有人說「這個字元串是UTF-16編碼的」時,鬼知道他到底說的到底是一個(UTF-16 encoding form的)碼元序列還是(UTF-16 encoding schema 的)位元組流

簡單的說,字元編碼表CEF和字元編碼方案CES區別如下:

c ∈ CCS ---CEF--&> Code Unit Sequence

c ∈ CCS ---CES--&> Byte Sequence

字元編碼表CEF碼位映射為碼元序列,而字元編碼方案CES碼位序列化為位元組流。

我們通常所說的動詞編碼(Encode)就是指使用CES,將CCS中字元組成的字元串轉變為位元組序列。

解碼(Decode)就是反過來,將 編碼位元組序列 通過CES的一一映射還原為CCS中字元的序列。

除了Unicode標準定義的七中CES,還有兩種CES: UCS-2,UCS-4 。嚴格來說,UCS-2和UCS-4屬於字元編碼表CEF的層次,不過鑒於其樸素的序列化方案,也可以理解為CES。這兩種CES的特點是採用定長編碼,比如UCS-2直接把碼位序列化為unsigned short。之前一直很流行,但當UCS中字元越來越多,超過65536個之後,UCS-2就GG了。至於UCS-4,基本和UTF-32差不多。雖說有生之年基本不可能看到UCS大小超出四位元組的表示範圍,但每個字元統一用4位元組來存儲這件事本身就很蠢了……。

當然除了UCS,其他字符集,例如US-ASCII,GBK,也會有自己的字元編碼方案,只不過我們很少聽說,一個很重要的原因是,這些字符集的編碼方案太簡單了,以至於CCS,CEF,CES三層直接合一了。

例如US-ASCII的CES,因為ASCII就128個字元,只要直接把其碼位轉換成(char),就完成了編碼。如此簡單的編碼,直接讓CCS,CEF,CES三層合一。很多其他的字符集也與之類似。

  • 傳輸編碼語法(Transfer Encoding Syntax)

通過CES,我們已經可以將一個字元表示為一個位元組序列。

但是有時候,位元組序列表示還不夠。比如在HTTP協議中,在URL里,一些字元是不允許出現的。這時候就需要再次對位元組流進行編碼。

著名的Base64編碼,就是把位元組流映射成了一個由64個安全字元組成字符集所表示的字元流。從而使位元組流能夠安全地在Web中傳輸。

不過這一塊的內容已經離我們討論的主題太遠了。

  • Python2中的編碼問題

回到正題,出現各種編碼問題,無非是哪裡的編碼設置出錯了

常見編碼錯誤的原因有:

  • Python解釋器的默認編碼

  • Python源文件文件編碼

  • Terminal使用的編碼

  • 操作系統的語言設置

當然要我說,最嚴重的問題其實是:

  • Python2的默認編碼方案的非常不合理。

  • Python2的字元串類型與字元串字面值很容易讓人混淆。

第一個問題:

Python2解釋器的默認編碼方案(CES)是US-ASCII (WTF!!!)

Java,C#,Javascript等語言內部的默認編碼方案都是UTF-16,Go語言的內部默認編碼方案使用UTF-8。與之相比,默認使用US-ASCII的python2簡直是骨骼清奇,當然,這也有一部分歷史原因在裡頭。Python3就乖乖地改成UTF-8了。

第二個問題:

python的默認"字元串類型"&與其叫字元串,不如叫位元組串,用下標去訪問的每一個元素都是一個位元組。而&類型才是真正意義上的字元串,用下標去訪問的每一個元素都是一個字元(雖然底下可能每個字元長度不同)。

字元串&
位元組串& 的關係為:

  • 字元串& 通過 字元編碼方案(CES)編碼(Encode) 得到位元組串&

  • 位元組串& 通過 字元編碼方案(CES)解碼(Decode) 得到字元串&

位元組串就位元組串,為啥要起個類型名叫str呢?另外,字面值語法用一對什麼前綴都沒有的引號表示str,這樣的設計非常反直覺。

當然,&與&這樣的類型設計以及兩者的關係設計本身是無可厚非的。該黑的應該是這兩個類型起的名字字面值表示方法。至於怎麼改進是好的,Python3已經給出答案。

以前我曾用IPython Notebook記過一個筆記,可惜知乎的顯示效果太差了。只能弄成圖片貼上來了。


注意:如下內容我來自之前的一篇博客,與題主遇到的實際問題並不完全匹配,但是目的依然是為了解決 Python2 中的編碼問題。

--------------

在 Python 尤其是 Python2 中,編碼問題是困擾開發者尤其初學者的一大問題。什麼 Unicode/UTF-8/str,又是 decode/encode 的,搞得人頭都大了。其實不然,這有點類似 Java 中 http://java.io 包一樣,看似龐大難懂,但是可以非常精細地定製需求。

編碼

計算機只可以存儲和處理二進位數據,所以從文字到計算機可以識別的二進位之間需要一道對應關係。於是便有了 ASCII(American Standard Code for Information Interchange,美國標準信息交換代碼),ASCII 使用 7 位二進位數標記了 128(2**7) 字元(符號、字母、控制符等),由於1byte=8bit,所以乾脆最高位補個 0 ,湊夠 8 位以方便計算和處理。

接著,拉丁語系的技術宅們發現,這 128 個字元的高位空著的,那麼乾脆用來表示拉丁語系的主要符號吧,還是使用單位元組,但是可以表示的字元數量增加了一倍。這套編碼叫做 latin-1(iso-8859-1)。

這些宅男們沒有想到,區區一個位元組 256 個符號,對於東亞國家來講,簡直呵呵了。拿中文為例,僅常用字元就幾千個,於是中國國家標準總局制定了一套中文編碼——GB2312。GB2312 通過兩個位元組表示一個漢字,且最高位為 0 的部分兼容 ASCII,最高位為 1 的部分則通過連續的兩個位元組表示一個中文字。後來又出現了兼容 GB2312 的 GBK 編碼。

到這裡,依然面臨了一個問題:GB2312 或者 GBK 編碼中,僅可以表示漢字和英文字元,無法做到多語言文字同時表示。這時候,Unicode (又稱萬國碼)出現了。Unicode 採用 32 位二進位( 4 位元組)表示一個字元,這樣便可以一套編碼對應多種不同語言。Unicode 是一種編碼,它的作用是指定字元到二進位數之間的對應關係。但是對於存儲和傳輸,Unicode 有幾種不同的實現,比較常用的是 UTF-8、UTF-16、UTF-32。UTF-32 中,每個字元固定占 4 位元組,按照 Unicode 編碼完全映射。而 UTF-8 和 UTF-16 則屬於變長編碼,分別使用最少 1 個 ( UTF-8 ) 或 2 個 ( UTF-16 ) 位元組到最多 4 個位元組來編碼。

Python 源碼的編碼

Python2 中,如果在源碼首行(或在指定 sha-bang 時的第二行)不顯式指定編碼,則無法在源碼中出現非 ASCII 字元。這是由於 Python 解釋器默認將源碼認作 ASCII 編碼格式。PEP263 (點擊這裡查看)中約定,可以通過如下方式之一來聲明源碼的編碼格式:

# coding=&

# or
# -*- coding: & -*-

# or
# vim: set fileencoding=& :

Python 中的編碼

Python 中有兩個常用的由 basestring 派生出來的表示字元串的類型:str, unicode。其中,str 類似於 C 中的字元數組或者 Java 中的 byte 數組,事實上你可以將它理解為一個存儲二進位內容的容器,str 不存儲編碼信息,如果對 str 類型的字元串迭代的話,則會按照其在內存中的位元組序依次迭代,意味著如果這個字元串存儲的是多位元組字元(Unicode/GBK等),則會截斷這個字元,演示如下:

而對於 unicode 類型,Python 在內存中存儲和使用的時候是按照 UTF-8 格式,在代碼中的表示為字元串前加 u,如:

而 unicode 與 str 之間的轉換,則用到了 encode 和 deocde 方法。decode 表示將一個 (str) 字元串按照給定的編碼解析為 unicode 類型,encode 則表示將一個 unicode 字元串按照指定編碼解析為位元組數組 (str):

文件讀寫

內置的 open 函數打開文件時,read 方法讀取的是一個 str (私以為叫做位元組數組更合適),如果讀取的是其它編碼的文字,則需要 decode 之後再做使用。

對於使用 open 函數打開文件之後的寫操作(多位元組編碼的字元串),則需要將需要寫入的字元串按照其編碼 encode 為一個 str ,如果直接寫入,則會引發如下錯誤(如果在代碼中加入了 encoding 聲明,則會按照聲明的編碼格式 encode 後寫入):

除此以外,codecs 模塊也提供了一個 open 函數,可以直接指定好編碼打開一個文本文件,那麼讀取到的文件內容則直接是一個 unicode 字元串。對應的指定編碼後的寫入文件,則可以直接將 unicode 寫到文件中。通過 codecs.open 可以避免很多編碼問題:

建議

對於 Python 代碼中避免遇到編碼問題,有一些小建議:

  • 字元編碼聲明:在代碼開頭聲明編碼格式
  • 使用 codecs 的 open 函數處理文本文件
  • 儘可能使用 unicode 而不是 str

之前有過一片分享,這裡是鏈接:五分鐘戰勝Python字元編碼 - LittleCoder的文章 - 知乎專欄

希望能有幫助

------------------------------------------------------------------------------------------------------------------------------------

本文不談複雜的理論,就經驗教你字元處理八字真言:確定編碼,同類交互。

文章針對Python 2.7,主要因為3對的編碼已經有了很大的改善並且實際原理一樣,更改一下操作命令即可。

了解完本文,你可以輕鬆解決文字處理,特殊平台(Windows?)下的編碼,爬蟲編碼等問題。

閱讀建議

本文分為如下幾個部分:

  • 原理
  • 具體操作
  • 建議的使用習慣
  • 疑難問題解答

如果想要了解我給出的使用習慣,可以直接跳到建議的使用習慣。

如果只想要解決相關問題可以直接跳到疑難問題解答。

希望本文能夠幫到你。

原理

為了理解方便,這裡不談理論只做類比,具體想要進一步了解各種編碼的理論的搜狗一下好了。

首先說一下我們為什麼會碰到各式各樣的編碼問題:

  • 因為我們沒有統一編碼
  • 因為我們沒有用對命令(傳對數據)

再說一下編碼是什麼,Python的編碼看似複雜,實際上可以看做只有兩類編碼:Unicode,二進位

  • Unicode 相信都很熟悉:,就是u0000這樣的
  • 二進位編碼也很簡單,就是x00x00這樣的,平常看到的utf-8,cp936都是二進位編碼
  • 二進位編碼是具象的,10001100原樣就可以存儲,而Unicode是抽象的,不能這樣存

#coding=utf8

# Unicode編碼演示
print("Unicode:")
print(repr(u"Unicode編碼"))`

# 二進位編碼演示
print(u"二進位編碼:")
print(repr("Unicode編碼"))`

# 只是看個樣子,代碼不必去深究

再說怎麼做,就是只有同種編碼之間才可以操作

  • 舉個簡單的類比

    就把一串數據比為烤鴨,我們作為人和鴨子不同種看待烤鴨的態度完全不一樣。
    我們看到的是晚上的配菜,鴨子看到的是自己二舅。
    那麼我在逛烤鴨店的時候用錯編碼就會報錯。
    因為我在烤鴨店看到了滿世界的二舅。

  • 這裡說的同種就是我們熟悉的各種編碼方式:utf-8,unicode,ucs-bom
  • 這也就是編碼問題的核心,非常重要。

最後說一下Python的環境

  • 本身代碼是用Ascii解碼的,文件里有Ascii無法解碼的內容的話要告知Python怎麼解碼
  • 內部大量命令都是默認接受Unicode

# 告知的命令就是下面這一行,刪掉就會報錯
#coding=utf8
print(u"測試編碼")

具體操作

拿到各種編碼的內容自然是不用說,那麼如果我們想要自己構造怎麼做呢,看下面:

#coding=utf8

# 字元串前面加u會默認構造出Unicode的字元串
unicodeString = u"Unicode字元串"

# 字元串前面什麼都不加會構造出默認編碼(首行限定了現在的utf8)的字元串
utf8String = "Utf-8字元串"

# 當然,沒有首行,默認的編碼是Ascii

那麼他們之間怎麼轉換呢,同樣很簡單:

# 接上一段程序

# Unicode轉化為二進位編碼中的一種:utf8
unicodeString.encode("utf8")

# 二進位編碼根據自己的編碼種類轉化為Unicode
utf8String.decode("utf8")

# 如果二進位編碼中混進了奇怪的東西可以根據需求用特殊的decode策略
print(repr("u8字x00符串".decode("utf8", "replace")))

那麼怎麼樣會出現問題呢:

# 接上一段程序

# 如果我們把他們轉化成同樣的編碼方式就可以操作(例如相加)
print(repr(unicodeString + utf8String.decode("utf8")))
print(repr(unicodeString.encode("utf8") + utf8String))

# 但如果不轉化,當然就會出現滿世界的烤鴨二舅啦
unicodeString + utf8String

# 所以另一方面也發現,編碼轉換是需要我們告訴程序怎麼做的
# 所有`decode`操作都會生成Unicode編碼,這是為了方便我之前說的大量接受Unicode的內部命令

所以我們需要確定程序使用的編碼,這是我們需要告訴程序的東西

  • 一方面在操作字元串的時候確定是同種編碼
  • 另一方面在使用非自己寫的命令時,一般使用Unicode,或者使用接收二進位編碼的命令

#coding=utf8
# 這裡拿寫入文件舉例

# 一般使用Unicode
with open("Unicode.txt", "w") as f: f.write(u"Unicode測試")

# 或者使用接收二進位編碼的命令
with open("Utf8.txt", "wb") as f: f.write("Utf8測試")

# 你可以反過來做個測試,自然會報錯
# 二進位的命令方便了在不知道怎麼解碼的情況下也能進行操作(寫入文件)

我建議的使用習慣

相信到這裡我已經把我對於編碼的理解講完了。

我們為什麼會碰到各式各樣的編碼問題:

  • 因為我們沒有統一編碼
  • 因為我們沒有用對命令(傳對數據)

所以這裡再重申一下八字真言:確定編碼,同類交互

  • 碰到問題,問一下自己,我現在是哪種編碼
  • 同一種編碼才能交互,那我應該是哪種編碼

這裡給出我的使用習慣:

  • 確定一種內部編碼
  • 內部編碼的選擇優先順序如下:程序必須使用的編碼、第三方包使用的編碼、你喜歡的編碼、Unicode
  • 在輸出時再更改到特定的編碼

記得在開始整個程序之前確定內部的編碼,否則編碼一團糟會產生很多不必要的bug。

不要迷信內部Unicode,例如Evernote開發就應該根據第三方包使用的Utf8確定內部編碼。

疑難問題解答編碼識別

說了要確定編碼,那麼拿到一串二進位要怎麼確定編碼呢?

最簡單的方法是chardet:(需要安裝)

python -m pip install chardet

使用非常簡單:

#coding=utf8

from chardet import detect
print(detect("這是一串utf8的測試字元"))

# 結果:`{"confidence": 0.99, "encoding": "utf-8"}`

另外例如抓取網站,那麼頭文件中很有可能有提示如何解碼,記得不要忘記了。

編碼轉換

很可能因為字元串中參雜了奇怪的東西,導致即使編碼種類正確,依舊無法解碼。

我知道我之前講過了,但可能有人直接跳疑難問題解答嘛。

這裡可以使用decode的第二個參數:

#coding=utf8

# 字元串中混進了x00
rubbishUtf8String = "Utf-8字x00符串"

print(repr(rubbishUtf8String.decode("utf8", "replace")))

print(repr(rubbishUtf8String.decode("utf8", "ignore")))

特殊平台下編碼

很多人都說Windows是個坑,即使在Python 3下面也一樣。

因為中文文件名出來都是亂碼。

這裡使用一個取巧的方法:平台編碼再特殊,起碼命令行讀取和創建一個文件夾不會出亂碼吧。

import sys, os

for folder in os.walk(".").next()[1]:
print(folder.decode(sys.stdin.encoding))

同樣的輸入輸出也可以這樣做優化:

import sys

def sys_print(msg):
print(msg.encode(sys.stdin.encoding))

def sys_input(msg):
return raw_input(msg.encode(sys.stdin.encoding)).decode(sys.stdin.encoding)

文件寫入

如果抓下來一個內容不知道怎麼解碼,但還是想要寫入文件怎麼辦

寫入文件的時候制定用二進位命令即可:

#coding=utf8
import urllib

with open("Utf8.txt", "wb") as f: f.write("Utf8測試")

# 比如抓了個網頁,不知道編碼也可以寫入文件進行一系列操作

content = urllib.urlopen("http://www.baidu.com").read()
with open("baidu.txt", "wb") as f: f.write(content)

裸Unicode字元

Unicode存成六個Ascii字元怎麼辦?其實也可以decode

#coding=utf8
# 這是普通的Unicode
s = u"測"
for i in s: print(i)
print(repr(s))

# 這是裸Unicode,實際存成了六個Ascii
s = repr(s)[2:-1]
for i in s: print(i)
print(repr(s))

# 轉化其實也很簡單
s = s.decode("unicode-escape")
for i in s: print(i)
print(repr(s))

結束語

希望讀完這篇文章能對你有幫助,有什麼不足之處萬望指正(鞠躬)。


覺得Python蛋疼的。

沒考慮C++的感受?


由於每天都能看到說Py編碼問題的東西,之前我還很有耐心地寫了段說明,有人提問就複製粘貼一下……但實在是太多了……

為什麼大家會覺得Python的編碼問題很複雜?

因為:1.Python門檻很低;2.有問題的這幫人中很大一部分,可以說連「編碼」是個啥都不知道,問他中文是怎麼存儲的搞不好都說不清楚,對於一切非中文輸出一概用「亂碼」來描述,並且拒絕聽懂或者想明白,只希望得到一套可以複製粘貼的萬能辦法來解決問題。

這樣你就是給他C/C++,搞不好人家也不會用wchar、wstring,給個默認utf8的玩意,他們會覺得要讀gbk編碼的東西就必須先把文件轉換成utf8,說utf8字元不等長位元組數,會疑惑半天並表示沒辦法對字元進行計數;你告訴他py2的str是py3的bytes、py2的unicode是py3的str,人家會疑惑半天然後表示哦一定是py3默認支持utf8所以就是好呀就是好;你告訴他utf8轉gbk的時候會出現無法轉換因為覆蓋的字符集不同,他會覺得卧槽就是會報錯所以需要ignore。

勞煩學點兒基礎好么……

下面簡短說明:

1. py2里,你代碼里硬生生寫了中文的情況下,需要告訴解釋器你的代碼本身是怎麼編碼的,即那個文件頭部的#coding: xxx。對於py3,如果代碼中是utf8遍嗎的可以不標註。

2.py2的str(py3的bytes)是單純的以單位元組劃分的玩意,說白了應該算是位元組序列。unicode是經過解碼統一轉換為unicode字元存儲的非等長字元序列,一個字元是可能對應多位元組的。輸出unicode時會根據需要編碼為位元組流交給os處理,當然你也可以手動完成這一步。

3.decode是把str按照指定編碼變為unicode對象,encode是把unicode對象變為指定編碼的位元組流str,由於py2里unicode和str中存在隱式轉換,你會發現你可以對str進行encode也可以對unicode進行decode,然而這個用法就是不正確的。


@xlzd 的答案很棒,補充一點很多新手會遇到的另一個編碼問題,就是通過SecureCRT或其他客戶端工具連接遠程機器,可能會發現讀寫文件都沒有問題,理論上用法也正確,但就是print出來有亂碼。 這個原因可能是因為你的客戶端編碼設置與print出來的數據編碼不一致導致的,很容易讓人誤以為程序出了問題,實際上可以置之不理或者修改一下客戶端工具的編碼即可。


用python3


真正的原因是因為使用Python的開發者數量少,且Python的開發與維護機構以及涉及到的相關開發者窮。這裡說的【少】與【窮】,是相對其他一線語言的比較。

少,則註定Python很少有遍布世界的開發者,導致很少有人會在多國語言的環境下使用Python。

窮,則註定Python的開發組織沒辦法分出時間去處理編碼問題。

不要說什麼Python誕生年代早,這麼多年,你怎麼不知道升級或補丁?

不要說什麼初學者不懂編碼問題,為什麼C#、Java就連VC在控制台下都沒亂碼問題?

所以Python自己的問題,造成初學者的學習成本提高,也造成了Python開發者遇到這類問題的處理成本增高。

排名靠前的答主,雖然花費了很大篇幅介紹了編碼,但三觀有問題。試想一下,如果這些人的滑鼠壞了,他們到底是會去學習滑鼠內部的電路原理?還是去京東重新買個滑鼠?


雖然我感覺 python 的編碼問題確實是一個門檻,但是我覺得也不是特有的,只不過新手不太分得清,什麼是編碼,例如向文筆文件中存儲uinicode 之類的。存儲的時候存儲的需要有編碼的字元串,而讀入的時候需要讀成無編碼格式的。另外一些比較特殊的問題,很多都是存儲讀入的時候沒有按照相應格式化而已。


你用python3,這些問題就都沒有了


抓取網頁時,之前我也經常遇到編碼問題,後來不斷摸索,已經找到所有這類問題的解決方法了。首先,linux默認是utf編碼,windows默認是gbk編碼。然後網頁也有編碼方式。Unicode作為中間編碼,首先把網頁編碼解碼為Unicode,然後編碼為你的系統編碼.。問題解決!!


因為 python 寫屏用的是 WriteConsoleOutputA,而不是 WriteConsoleOutputW


就題目標題來回答:

因為python出來的時候,還沒有unicode。

所以你強行不進化到python3,就只能忍受編碼問題。


我一度以為我對py2的編碼問題已經很熟悉了,後來爬一個中文的網站,依舊歇菜~

我遇到的問題:

頁面實際是使用GB18030編碼的,但是 charset=gb2312。所以用gb2312解碼出錯,提示:

UnicodeDecodeError: "gb2312" codec can"t decode bytes in position xxxx-xxxx: illegal multibyte sequence

用gb18030就OK。

========================================================================

中文編碼的幾個字符集:

GB2312: GB2312標準共收錄6763個漢字。1981年5月1日實施。

GBK: GBK共收入21886個漢字和圖形符號,是對GB2312-80的擴展,K即(擴展)。1995年12月1日制訂

GB18030:共有70,244個漢字,2000年3月17日發布。


和樓上說的一樣,是要將Unicode當作中間碼,python2我是這樣解決的:

def do_request(req):
res.urllib2.urlopen(req, timeout=10)
encoding = res.headers["content-type"].split("charset=")[-1]
content = res.read()
return unicode(content, encoding)

讀下來的就是編碼好的unicode,再轉其它的就OK了。

你也可以直接用瀏覽器打開url看一下charset,然後按照charset的編碼轉unicode:


用Python3。原生支持unicode。方便很多。


只要確認幾個點:

1. 所使用的操作系統 std out(標準輸出) 默認用的是什麼編碼?

import sys
print sys.stdout.encoding

2. 你代碼字元串用的是什麼編碼? (python 2字元串只有 unicode 和 str, str 可以直接當作 byte 數組)

3. 外部獲取的字元串是什麼編碼?(如抓取的網頁, 讀取的文本文件)

4. python 2.x 內部默認是 ascii

5. python 2.x 源碼裡面第一行或者第二行 #coding=utf8 是告訴解釋器以什麼樣的編碼讀取源代碼

比方抓網頁, 可以通過 Content-Type 獲取網頁字元編碼, 瀏覽器會根據這個編碼來顯示, 如果 Content-Type 缺失或者設置錯誤, 瀏覽器也會顯示出亂碼的

具體點:

python 抓到數據後可以用 GitHub - chardet/chardet: Python 2/3 compatible character encoding detector. 來檢測編碼, 然後再 decode 成 unicode


似乎windows 默認解碼方式是gbk,而你抓取的網頁不是。這就導致錯誤,gbk無法解碼blabla…

先看下網頁編碼方式是什麼,然後用這種編碼方式解碼為unicode就可以了。

另外,這個問題很普遍,你上網一搜一大堆。粗略看下很可能你解決不了。要有耐心。


if system.type == "Windows":
exit()
else:
fly()


推薦閱讀:

如何維護爬蟲程序中的代理ip庫?
python模擬登錄知乎,captcha是手工輸入,為什麼也提示captcha錯了?
如何解析網頁視頻的原始地址?
anaconda是幹什麼的,是 python的第三方解釋環境嗎?
Chrome的開發者工具怎麼查看錶單數據?網路選項卡里的參數一項在哪裡?

TAG:程序員 | Python | 計算機網路 | 爬蟲計算機網路 |