记一次 Python 编码的坑

 

这次又遇到了Python编码导致的问题...



这次又遇到了 Python 编码导致的问题,与PyTips 0x07~0x09 中解释过的

Unicode - Bytes
不同,这次遇到的是另外一种情况。应用场景如下:爬虫抓取网页数据,通过
requests
模块将数据
POST
到服务器,但是要去除数据中的空白符(包括
'

'
等)。

问题出在
requests
模块通过
JSON
格式传递数据:

import requests as req
import json
import re

title = '你好,
世界'
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 = '你好,
世界'

data = json.dumps({'title': title})
data = re.sub(r's', ' ', data)
print(json.loads(data))
{'title': '你好,
世界'}
问题出现了,
re.sub(r's', ' ', data)
并没有出去空白符,而实际上这样做看起来是没问题的:

print(re.sub(r's', ' ', "{'title': '你好,
世界'}"))
{'title': '你好, 世界'}
之前提到了只要保持
Unicode-Bytes-Unicode
三明治形式就不会受到编码问题的困扰(前提是 Python 3),经过和大家的讨论和探索之后发现问题出在
json.dumps


print(json.dumps({'title': title}))
{"title": "u4f60u597duff0c
u4e16u754c"}
根据经验,在 Python 3 中如果出现 “u4f60” 这样的原始
Unicode
编码就很可能意味着这并不是你想要的结果,我们只希望看到正常显示的
Unicode
或二进制形式的字符:

print("u4f60")
print("u4f60".encode())

b'xe4xbdxa0'
经过
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))


1
b'xe9x9bxa8'

3
'u96e8'

8
"u96e8"

8
也就是说
json.dumps()
将原本的
Unicode
字符拆分成一个个单独的
ASCII
码,而不是正常的
encode()
,不过该方法提供了一个参数
ensure_ascii = False
可以避免这种拆分:

print_code_and_size(json.dumps(yu, ensure_ascii=False))
"雨"

3
虽然原理是更清楚了,不过可惜的是这样并没有解决我们当前的问题,因为换行符本身就是
ASCII
码,并不会受到
ensure_ascii
参数的影响:

r = '
'

print_code_and_size(json.dumps(r, ensure_ascii=False))
print(list(json.dumps(r, ensure_ascii=False)))
"
"

4
['"', '\', 'n', '"']
还是被拆分成了单独的字符,因此仍然无法对
json.dumps()
返回的字符串进行去空白符的操作。因此针对这一问题正确的做法应该是在
json.dumps()
之前先去除空格:

import json
import re
title = '你好,
世界'
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
    ,在它们两者之间不要对字符串进操作。



题图来源:UNSPLASH


    关注 PyHub


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册