unicode和utf8 —— 從一個遍歷文件名的腳本,談談對Python2和Python3中字符編碼差別的理解

對編碼問題一直只知其一;不知其二,以前也是得過且過,正好有個同事要我幫忙寫個腳本,涉及這方面的問題,借這個契機研究了一下.html

先貼幾篇比較好的:python

1.阮老師的上古文章(07年…),雖然古老但對理解幫助很大,從最基礎講起,邏輯清晰易理解. (ps: 阮老師的博客都有此特色, 在這裏推薦一波, 從js到linux, 精通先後端, 是能夠當文檔看的博客): http://www.ruanyifeng.com/blo...linux

2.最好看了上一篇再看這篇(解釋了py2中爲何不能用 setdefaultencoding): https://blog.ernest.me/post/p...windows

3.關於UnicodeDecodeError: https://stackoverflow.com/que...後端

以及Python3的官方文檔:https://docs.python.org/relea...app

=============================================================================
建議以上幾篇理解的差很少後再看正文:函數

簡單說一下:
2.x中的編碼概念是不夠清晰的,str類型的對象會被賦予默認編碼,且既能夠對其編碼又能夠對其解碼(單這一點就足夠形成不少混亂…),而咱們在代碼中常直接使用帶編碼的str進行os庫相關的操做,就容易致使不少問題。對於python內部來講,解釋器處理操做系統的文件目錄相關的東西時,必須使用unicode。新手若是要讀取文件名並進行一些處理時,常常遇到亂碼,以及windows和linux下效果不一樣的問題。另一個主要場景就是stream,流處理,這個就是寫文件或者先後端通訊之類,這個相對前面問題來講其實還算好處理的。而後還有字符串拼接。post

3.x去掉了 unicode類型 和 unicode()函數,(也就沒有u'xxx'這種寫法了),區分出str類型和bytes類型,並且str再也不同時有encodedecode方法,bytes只有decodestr只有encode
3.x中,沒有了unicode這個類型,能夠理解爲str成爲了unicode類型,"All text is Unicode"。而帶編碼的字符串則由bytes類型來處理。但也不能簡單地理解爲3.x的str和bytes分別對應2.x的unicode和str。編碼

因此2.x處理字符串原則其實也很簡單,就是把str當成bytes,內部只用unicode,外部進的和出的都編碼成str。spa

這裏可能有個疑問就是,按以前的理解(假設已經讀了第1篇)unicode是編碼規則,但不是存儲方式,uft8纔是它的實現,才能用來存儲,那麼若是python內部是用unicode方式處理文本,在內存中python解釋器如何正確讀取字符呢?這裏要理解清楚所謂實現,其實多的就是一個字節數的信息,unicode和utf8本質上都是一串0和1,只是缺一個字節數量的區分,即,從信息量上來講: unicode + 自身長度 = utf8。這樣,在python解釋器的處理過程當中,python天然有辦法用本身的標記來正確讀寫「自身長度」這個信息,由於這裏不須要和外界交互,不須要相似utf8這樣的約定規則,本身內部能正確獲取信息便可。utf8是爲了省硬盤空間,內存中不太須要這樣的東西。(這段屬於我的想固然的理解,僅供參考)

重點,重點,重點,貼一下py2中處理編碼的原則(摘自上面第3篇),也就是我上面那句總結的完整版,若是你理解了爲何有這個原則說明差很少理解了py2的編碼:

·全部 text string 都應該是 unicode 類型,而不是 str,若是你在操做 text,而類型倒是 str,那就是在製造 bug。
·在須要轉換的時候,顯式轉換。從字節解碼成文本,用 var.decode(encoding),從文本編碼成字節,用 var.encode(encoding)。
·從外部讀取數據時,默認它是字節,而後 decode 成須要的文本;一樣的,當須要向外部發送文本時,encode 成字節再發送。

除了上面幾篇,百度還有無數其餘的講解,本篇就再也不贅述原理之類的,上腳本講下實際應用,腳本功能是遞歸遍歷目錄下全部文件:

#-*- coding:utf-8 -*-
'''
Description :  
遞歸遍歷目錄下全部文件(排除目錄),並逐行寫入到指定文件中。
能夠分別用py2或py3來執行,結果相同。
能夠不帶參數,或者 python xxxx <path> <writepath>

主要幹兩件事:
第一步,把文件路徑解碼成unicode,傳給os用來遍歷 (僅py2)
第二步,把文件名編碼後寫入文件
這樣正好覆蓋了上面提到的兩個主要場景。
'''

''' 
Python2: str -> (decode) -> unicode -> (encode) -> str
Python3: bytes -> (decode) -> str(unicode) -> (encode) -> bytes 
'''

import sys
import os

try:
    PATH = sys.argv[1]
except IndexError:
    # 在這裏寫一個你能找到的名字最亂,裏面文件名最雜的文件夾
    PATH = r'./'  # raw string, 表示不進行轉義, 若是複製一個帶反斜槓後面帶數字或字母的路徑, 不加上這個r就會出錯
    
try:
    WRITE_PATH = sys.argv[2]
except IndexError:
    WRITE_PATH = 'abc'   # 指定要寫入的文件名


PY2 = sys.version.startswith('2')


if PY2:
    # 不理解編碼的人常常用這個當作萬能藥,這個確實也有用,但嚴重不推薦使用,見第3篇
    # import sys
    # reload(sys)  
    # sys.setdefaultencoding('utf8')
    # PATH = PATH.decode()  # 這樣就默認以utf8解碼,因爲上面的代碼致使傳進來的PATH會被默認編碼爲utf8


    # 記住原則,在python內處理文本字符串,永遠保證是unicode類型,因此這裏要進行解碼。關於'ignore'參數見第4篇
    # 這裏PATH不帶中文時,不管哪一種都會默認爲ascii編碼,帶其餘非ascii文字時,根據來源若是是:
    # 1. sys.argv傳入,那麼PATH的編碼跟操做系統有關。若是傳一箇中文,windows下和linux下編碼分別是ISO-8859-1和utf8,能夠本身用chardet打印看看
    # 2. 文件中寫死,原本理解是跟這個文件自己編碼有關,但文件編碼一樣是utf8的狀況下,windows下打印了Windows-1252(ISO-8859-1的超集),linux下仍然是utf8。因此仍是跟操做系統有關
    # 這裏默認在linux系統下執行,因此直接用utf8解了,若是要兼容,能夠用chardet獲取編碼類型後指定進行解碼
    PATH = PATH.decode('utf8', 'ignore')

# if PY3,不管傳入仍是寫死PATH都將會是```str```類型,固然也就不須要也不能進行解碼啦


def getf(path):
    l = []
    res = os.listdir(path)
    for each in res:
        subpath = os.path.join(path, each)
        if os.path.isdir(subpath):
            l.extend(getf(subpath))
        else:
            l.append(each)

    return l

res = getf(PATH)



if PY2:
    # Python2, 因爲py2中概念的模糊, 能夠直接用'w'打開去寫,而不須要'wb'
    # 不過不編碼成utf8的話也是會拋UnicodeDecodeError的,寫文件須要編碼這個原則py2仍是有的。能夠檢查一下 "%s\n" % each 的類型毫無疑問是unicode
    with open(WRITE_PATH, 'w') as f:
        for each in res:
            f.write(("%s\n" % each).encode('utf8'))

else:
    # Python3, 能夠用w打開而後不編碼直接寫string(即unicode),也是能夠成功寫的,不過那樣結果是非ascii都亂碼。
    # 而編了碼就轉爲了bytes類型,因此Python3想正確實現就必須用二進制方式打開 (wb)
    # 若是打開方式和寫入類型不對應,會拋TypeError,很明確
    with open(WRITE_PATH, 'wb') as f:
        for each in res:
            f.write(("%s\n" % each).encode('utf8'))

總結下代碼,首先能夠看到py3是沒毛病的,對編碼的操做概念清晰,沒有任何困擾。

py2這塊確實有硬傷,雖然不少時候也無所謂,但在要嚴謹或者專門處理編碼的代碼中,必定要記住開頭貼的原則。

關於setdefaultencoding,除非實在不重要的場景,又須要臨時簡單解決,能夠湊合一下,普通場景不建議用.
緣由第3篇解釋得很清楚。另附官方文檔的說明以下:
set the current default string encoding used by the Unicode implementation. If name does not match any available encoding, LookupError is raised. This function is only intended to be used by the site module implementation and, where needed, by sitecustomize. Once used by the site module, it is removed from the sys module’s namespace.

相關文章
相關標籤/搜索