此次又遇到了 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
,在它们二者之间不要对字符串进操做。