記一次 Python 編碼的坑

ilikepanda 8年前發布 | 16K 次閱讀 Python Unicode Python開發

這次又遇到了 Python 編碼導致的問題,與 PyTips 0x07~0x09 中解釋過的 Unicode - Bytes 不同,這次遇到的是另外一種情況。應用場景如下:爬蟲抓取網頁數據,通過 requests 模塊將數據 POST 到服務器,但是要去除數據中的空白符(包括 '\r\n' 等)。

問題出在 requests 模塊通過 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) 并沒有出去空白符,而實際上這樣做看起來是沒問題的:

print(re.sub(r'\s', ' ', "{'title': '你好,\n世界'}"))
{'title': '你好, 世界'}

之前提到了只要保持 Unicode-Bytes-Unicode 的 三明治 形式就不會受到編碼問題的困擾(前提是 Python 3),經過和大家的討論和探索之后發現問題出在 json.dumps :

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() 式的編碼:

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 可以避免這種拆分:

print_code_and_size(json.dumps(yu, ensure_ascii=False))
"雨" <class 'str'> 3

雖然原理是更清楚了,不過可惜的是這樣并沒有解決我們當前的問題,因為換行符本身就是 ASCII 碼,并不會受到 ensure_ascii 參數的影響:

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': '你好, 世界'}

總結

這個問題本不該浪費這么多時間,原因是與編碼問題糾纏在一起,導致一開始的思路就是跑偏的。總結下來有兩點:

  1. Unicode-Bytes-[[===]]-Bytes-Unicode 的模式可以解決絕大部分編碼問題;

  2. json.dumps 與 ascii 這種形式的編碼對應的解碼分別為 json.loads 和 eval ,在它們兩者之間不要對字符串進操作。

來自: weixin

 

 本文由用戶 ilikepanda 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!