iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
0
Data Technology

30天python雜談系列 第 9

UnicodeError雜談之三———快點投靠python3啦啦啦

  • 分享至 

  • xImage
  •  

python UnicodeError雜談之三

囉唆一下:跟昨天的囉唆一樣,凡unicode編碼的字串因為比較符合字元的特性,所以統一叫他們為'字符串',而非unicode編碼的字串,因為適合作為儲存以及數據傳輸,因此統稱為'位元組字串'。

情境四:不同編碼文字出現在同一檔案的問題

說明:
這裡先假設大家已經知道如何使用open()函數的基本功能來開啟檔案,這裡要來探討一下比較麻煩的問題,假設你寫了一個爬取網頁的爬蟲,把各個原本不同編碼的網頁都抓下來然後合併到一個檔案裡,現在出現一個困難是如果要用最常見的utf-8編碼來解碼檔案內容,仍不可避免的會有解碼錯誤,那應該要怎麼辦呢?

In python3 shell:
>>> of = open('example_out','wb')
>>> a = '我是一個範例 '.encode('utf-8')
>>> b = '我也是一個範例'.encode('big5')
>>> of.write(a)
19
>>> of.write(b) # 先把一個utf-8編碼字串和big5編碼字串寫進example_out
14

In bash shell:
$ cat example_out
我是一個範例 �ڤ]�O�@�ӽd� # 如果直接用cat指令print出剛剛寫入的內容,因為預設編碼是utf-8,所以後面會呈現亂碼,但python要如何讀取這種檔案呢?

解決方案:
方案1. 當然若選擇直接用位元組字串下去處理,不對他作任何的解碼也是可以,python3可以在mode中設置成'rb',而python2的open原本就預設不會對檔案的資料做任何的解碼動作,直接open就行了:

In python3 shell:
>>> open('example_out','rb').read()
b'\xe6\x88\x91\xe6\x98\xaf\xe4\xb8\x80\xe5\x80\x8b\xe7\xaf\x84\xe4\xbe\x8b \xa7\xda\xa4]\xacO\xa4@\xad\xd3\xbdd\xa8\xd2'

In python2 shell:
>>> open('example_out','rt').read()
'\xe6\x88\x91\xe6\x98\xaf\xe4\xb8\x80\xe5\x80\x8b\xe7\xaf\x84\xe4\xbe\x8b \xa7\xda\xa4]\xacO\xa4@\xad\xd3\xbdd\xa8\xd2'

當然這種方法的缺點就是你沒有辦法輕易解讀他的文意,如果是做一些不會牽涉到文義的處理這不失為一個方法,但如果你想硬是用位元組的方式來解讀文義也不是不行拉...

方案2. 現在python3的open函數有一個很好的解決方法,除了本身的filename以及mode參數,還新增添了encoding以及errors參數:

In python3 shell:
>>> open('example_out','rt',encoding='utf-8').read() # 如果沒加errors參數會跑出UnicodeDecodeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/codecs.py", line 319, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec cant decode byte 0xa7 in position 19: invalid start byte
>>> open('example_out','rt',encoding='utf-8',errors='ignore').read() # errors = 'ignore' 會忽略掉無法解碼的字元
'我是一個範例 ڤ]O@ӽd'
>>> open('example_out','rt',encoding='utf-8',errors='replace').read() # errors = 'replace' 會把無法解碼的字元換成�字元
'我是一個範例 �ڤ]�O�@�ӽd��'

雖然這種方法無法讓你得知檔案所有正確內容,但能確保程式還能處理部份資料而不會出現Error,而如果是python2的話,可以使用io模組的open函數,這裡的open函數和python3是一樣的:

In python2 shell:
>>> from io import open
>>> open('example_out','rt',encoding='utf-8',errors='ignore').read() # python2 在這裡unicode文字輸出是以文字的unicode_ID加上前綴'\u'來表示,不會直接顯示出文字的樣子
u'\u6211\u662f\u4e00\u500b\u7bc4\u4f8b \u06a4]O@\u04fdd'
>>> open('example_out','rt',encoding='utf-8',errors='replace').read() # 在python3的�字元若在python2是用'\ufffd'表示,python2不顯示中文好煩阿ˊˋ
u'\u6211\u662f\u4e00\u500b\u7bc4\u4f8b \ufffd\u06a4]\ufffdO\ufffd@\ufffd\u04fdd\ufffd\ufffd'
>>> print(open('example_out','rt',encoding='utf-8',errors='ignore').read()) # 雖然改成用print就可以顯示中文了,但python2的這個問題真的很讓人討厭,詳細原理等改天有空再談談
我是一個範例 ڤ]O@ӽd
>>> print(open('example_out','rt',encoding='utf-8',errors='replace').read()) # 你還想被python2雷嗎?趕快換python3吧吧吧吧
我是一個範例 �ڤ]�O�@�ӽd��

情境五:檔名編碼問題(主要參考python cookbook chp5的範例)

說明:
感覺好像稍微解決了一些編碼問題了,只要

  1. 搞清楚外部檔案的編碼(包括執行檔的編碼)
  2. 正確的使用讀檔寫檔函數
  3. 然後確保字符串和位元組字串不會在程式中混用

就可以避掉大部分的UnicodeError。但只要有文字的地方,就可能會有編碼的坑讓你跳,大部份人沒有想到連檔名或路徑這種東西也會有編碼問題,這太蛋疼了,但沒辦法,誰教他也是文字呢?先來看看python2和python3預設以什麼編碼來解讀外部檔案的檔名路徑:

In python2 shell:
>>> import sys
>>> sys.getfilesystemencoding()
'UTF-8'

In python3 shell:
>>> import sys
>>> sys.getfilesystemencoding()
'utf-8'

假設有一個檔名或是路徑名不是用utf-8編碼的話,在python3會出現以下狀況:

In python3 shell:
>>> a = open(b'\xca\xcb','wt') # 先隨意建立一個無意義檔名的檔案

重啟python3 shell,以確保檔案有被建立出來:
>>> import os
>>> list = os.listdir('.') # 用list儲存當前目錄的檔名列表
>>> list
[...,'\udcca\udccb',...] # ...為當下目錄其他檔名,'\udcca\udccb'為剛剛建立的檔案檔名

因為python在解析當前目錄的檔名時,對於無法解碼的檔名,因為又無法丟棄,所以暫時用所謂的surroagate encoding把無法解碼的Bytes各自映射至相應的unicode字元,這暫時避免了UnicodeError的出現,同時surroagate encoding也可以讓一般的字串操作或是open函式順利進行,但若是要把他print出來,就行不通了。

In test.py:
import os
list = os.listdir('.') # 用list儲存當前目錄的檔名列表
print(list[2]) # 2為我的壞掉檔名在list所在的index值,讀者們就各自看自己的壞掉檔名是哪個index吧

In bash shell:
>>> python3 test.py
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    print(list[2]) # 2為壞掉檔名在list所在index
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcca' in position 0: surrogates not allowed

只能說python3在這裡有些無解,如果要把檔名print出來,只能利用try except的方式解決了,由於懶的再打更多字,就直接丟個網站大家自己進去看吧(逃):http://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p15_printing_bad_filenames.html

但如果是python2,在這方面反而不會出現問題,只需記得在test.py的第1行加上# -- coding: utf-8 --,因為test.py裏面有中文字(情境一議題):

In test.py:
# -*- coding: utf-8 -*-
import os
list = os.listdir('.') # 用list儲存當前目錄的檔名列表
print(list[2]) # 2為壞掉檔名在list所在index

In python2 shell:
>>> python test.py 
�

python2之所以不會出問題是因為這個版本的os.listdir再讀取檔名的時候,會直接讀取位元組字串,而不會事先轉為字符串,所以也不會有surroagate encoding,而python2的print函數的功能類似於python3的open([filename],[mode],encoding='utf-8',errors='replace'),若print的參數為位元組字串,會先將其轉為utf-8,然後無法編碼的位元組用�代替。

灑花花,UnicodeError雜談到此結束,總結一下吧(其實是來戰python2的)(根本離題):

  1. 首先在情境一因為python2老愛用ascii來做事,難怪會有Error,要多學學python3,直接就能支援內含中文py檔。
  2. 情境三python2默認可以將字符串和位元組字串相加,這在教壞小孩阿,讓人覺得兩種類型物件可以混合操作,尤其是在以英文字母為官方文字的python使用者,平時這樣相加根本不會像中文會出現UnicodeDecodeError,就更加寵壞他們了,到時候程式愈長愈大,然後開始參入其他文字看看要怎麼辦。
  3. 你看看情境四的讀檔,python2要實現同時讀檔與解碼還要借用io模組的open。
  4. 在python2的shell中很愛用原始編碼顯示,都要用print才會乖乖的把中文弄出來(雖然是有解決方法拉...以後再說)
  5. 可以趕快投靠python3了。
  6. (滅火)覺得這段文字不OK的地方的讀者,可以留言批評,我會理性的參考並試著討論,但不要打我(怕)

上一篇
UnicodeError雜談之二———初探字符串與位元組的交互作用
下一篇
repr與str雜談———暴風雨前的輕鬆小品技術文
系列文
30天python雜談30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言