此次又遇到了 Python 編碼致使的問題,與 PyTips 0x07~0x09 中解釋過的 Unicode - Bytes
不一樣,此次遇到的是另一種狀況。應用場景以下:爬蟲抓取網頁數據,經過 requests
模塊將數據 POST
到服務器,可是要去除數據中的空白符(包括'\r\n'
等)。python
問題出在 requests
模塊經過 JSON
格式傳遞數據:json
import requests as req import json import re title = '你好,\n世界' req.post(API, data=json.dumps({'title': title})) # API data = self.requests.body.decode() data = re.sub(r'\s', ' ', data) save_data(json.loads(data))
雖然 HTTP
是經過二進制(也就是 Bytes
)進行傳輸的,但經過 self.requests.body.decode()
仍然保持了 Unicode-Bytes-[HTTP]-Bytes-Unicode
的原則,所以實際上能夠判定問題不是出自 Unicode
編碼上,忽略掉中間傳輸過程,上面的代碼能夠簡化爲:服務器
import json import re title = '你好,\n世界' data = json.dumps({'title': title}) data = re.sub(r'\s', ' ', data) print(json.loads(data))
{'title': '你好,\n世界'}
問題出現了,re.sub(r'\s', ' ', data)
並無出去空白符,而實際上這樣作看起來是沒問題的:post
print(re.sub(r'\s', ' ', "{'title': '你好,\n世界'}"))
{'title': '你好, 世界'}
以前提到了只要保持 Unicode-Bytes-Unicode
的三明治形式就不會受到編碼問題的困擾(前提是 Python 3),通過和你們的討論和探索以後發現問題出在 json.dumps
:ui
print(json.dumps({'title': title}))
{"title": "\u4f60\u597d\uff0c\n\u4e16\u754c"}
根據經驗,在 Python 3 中若是出現 "\u4f60" 這樣的原始 Unicode
編碼就極可能意味着這並非你想要的結果,咱們只但願看到正常顯示的 Unicode
或二進制形式的字符:編碼
print("\u4f60") print("\u4f60".encode())
你 b'\xe4\xbd\xa0'
通過 json.dumps()
以後會將原來字典類型中的值變爲 ascii
編碼,且不是 encode()
這種編碼,而是 ascii()
式的編碼:code
help(ascii)
Help on built-in function ascii in module builtins: ascii(obj, /) Return an ASCII-only representation of an object. As repr(), return a string containing a printable representation of an object, but escape the non-ASCII characters in the string returned by repr() using \\x, \\u or \\U escapes. This generates a string similar to that returned by repr() in Python 2.
其中的區別能夠經過下面的例子說明:圖片
def print_code_and_size(s): print(s, type(s), len(s)) yu = '雨' print_code_and_size(yu) print_code_and_size(yu.encode()) print_code_and_size(ascii(yu)) print_code_and_size(json.dumps(yu))
雨 <class 'str'> 1 b'\xe9\x9b\xa8' <class 'bytes'> 3 '\u96e8' <class 'str'> 8 "\u96e8" <class 'str'> 8
也就是說 json.dumps()
將本來的 Unicode
字符拆分紅一個個單獨的 ASCII
碼,而不是正常的 encode()
,不過該方法提供了一個參數 ensure_ascii = False
能夠避免這種拆分:ip
print_code_and_size(json.dumps(yu, ensure_ascii=False))
"雨" <class 'str'> 3
雖然原理是更清楚了,不過惋惜的是這樣並無解決咱們當前的問題,由於換行符自己就是 ASCII
碼,並不會受到 ensure_ascii
參數的影響:ci
r = '\n' print_code_and_size(json.dumps(r, ensure_ascii=False)) print(list(json.dumps(r, ensure_ascii=False)))
"\n" <class 'str'> 4 ['"', '\\', 'n', '"']
仍是被拆分紅了單獨的字符,所以仍然沒法對 json.dumps()
返回的字符串進行去空白符的操做。所以針對這一問題正確的作法應該是在 json.dumps()
以前先去除空格:
import json import re title = '你好,\n世界' title = re.sub(r'\s', ' ', title) data = json.dumps({'title': title}) print(json.loads(data))
{'title': '你好, 世界'}
這個問題本不應浪費這麼多時間,緣由是與編碼問題糾纏在一塊兒,致使一開始的思路就是跑偏的。總結下來有兩點:
Unicode-Bytes-[[===]]-Bytes-Unicode
的模式能夠解決絕大部分編碼問題;json.dumps
與 ascii
這種形式的編碼對應的解碼分別爲json.loads
和eval
,在它們二者之間不要對字符串進操做。