记一次 Python 编码的坑

ilikepanda 5年前
   <p>这次又遇到了 Python 编码导致的问题,与 <strong>PyTips 0x07~0x09</strong> 中解释过的 Unicode - Bytes 不同,这次遇到的是另外一种情况。应用场景如下:爬虫抓取网页数据,通过 requests 模块将数据 POST 到服务器,但是要去除数据中的空白符(包括 '\r\n' 等)。</p>    <p>问题出在 requests 模块通过 JSON 格式传递数据:</p>    <pre>  <code class="language-python">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))</code></pre>    <p>虽然 HTTP 是通过二进制(也就是 Bytes )进行传输的,但通过 self.requests.body.decode() 仍然保持了 Unicode-Bytes-[HTTP]-Bytes-Unicode 的原则,因此实际上可以断定问题不是出自 Unicode 编码上,忽略掉中间传输过程,上面的代码可以简化为:</p>    <pre>  <code class="language-python">import json  import re  title = '你好,\n世界'    data = json.dumps({'title': title})  data = re.sub(r'\s', ' ', data)  print(json.loads(data))</code></pre>    <pre>  <code class="language-python">{'title': '你好,\n世界'}</code></pre>    <p>问题出现了, re.sub(r'\s', ' ', data) 并没有出去空白符,而实际上这样做看起来是没问题的:</p>    <pre>  <code class="language-python">print(re.sub(r'\s', ' ', "{'title': '你好,\n世界'}"))</code></pre>    <pre>  <code class="language-python">{'title': '你好, 世界'}</code></pre>    <p>之前提到了只要保持 Unicode-Bytes-Unicode 的 <strong>三明治</strong> 形式就不会受到编码问题的困扰(前提是 Python 3),经过和大家的讨论和探索之后发现问题出在 json.dumps :</p>    <pre>  <code class="language-python">print(json.dumps({'title': title}))</code></pre>    <pre>  <code class="language-python">{"title": "\u4f60\u597d\uff0c\n\u4e16\u754c"}</code></pre>    <p>根据经验,在 Python 3 中如果出现 “\u4f60” 这样的原始 Unicode 编码就很可能意味着这并不是你想要的结果,我们只希望看到正常显示的 Unicode 或二进制形式的字符:</p>    <pre>  <code class="language-python">print("\u4f60")  print("\u4f60".encode())</code></pre>    <pre>  <code class="language-python">你  b'\xe4\xbd\xa0'</code></pre>    <p>经过 json.dumps() 之后会将原来字典类型中的值变为 ascii 编码,且不是 encode() 这种编码,而是 ascii() 式的编码:</p>    <pre>  <code class="language-python">help(ascii)</code></pre>    <pre>  <code class="language-python">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.</code></pre>    <p>其中的区别可以通过下面的例子说明:</p>    <pre>  <code class="language-python">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))</code></pre>    <pre>  <code class="language-python">雨 <class 'str'> 1  b'\xe9\x9b\xa8' <class 'bytes'> 3  '\u96e8' <class 'str'> 8  "\u96e8" <class 'str'> 8</code></pre>    <p>也就是说 json.dumps() 将原本的 Unicode 字符 <strong>拆分成</strong> 一个个单独的 ASCII 码,而不是正常的 encode() ,不过该方法提供了一个参数 ensure_ascii = False 可以避免这种拆分:</p>    <pre>  <code class="language-python">print_code_and_size(json.dumps(yu, ensure_ascii=False))</code></pre>    <pre>  <code class="language-python">"雨" <class 'str'> 3</code></pre>    <p>虽然原理是更清楚了,不过可惜的是这样并没有解决我们当前的问题,因为换行符本身就是 ASCII 码,并不会受到 ensure_ascii 参数的影响:</p>    <pre>  <code class="language-python">r = '\n'    print_code_and_size(json.dumps(r, ensure_ascii=False))  print(list(json.dumps(r, ensure_ascii=False)))</code></pre>    <pre>  <code class="language-python">"\n" <class 'str'> 4  ['"', '\\', 'n', '"']</code></pre>    <p>还是被拆分成了单独的字符,因此仍然无法对 json.dumps() 返回的字符串进行去空白符的操作。因此针对这一问题正确的做法应该是在 json.dumps() 之前先去除空格:</p>    <pre>  <code class="language-python">import json  import re  title = '你好,\n世界'  title = re.sub(r'\s', ' ', title)  data = json.dumps({'title': title})  print(json.loads(data))</code></pre>    <pre>  <code class="language-python">{'title': '你好, 世界'}</code></pre>    <h3>总结</h3>    <p>这个问题本不该浪费这么多时间,原因是与编码问题纠缠在一起,导致一开始的思路就是跑偏的。总结下来有两点:</p>    <ol>     <li> <p>Unicode-Bytes-[[===]]-Bytes-Unicode 的模式可以解决绝大部分编码问题;</p> </li>     <li> <p>json.dumps 与 ascii 这种形式的编码对应的解码分别为 json.loads 和 eval ,在它们两者之间不要对字符串进操作。</p> </li>    </ol>    <p>来自: <a href="http://mp.weixin.qq.com/s?__biz=MzI0NjIxMzE5OQ==&mid=2656697790&idx=1&sn=a51122c40801815302003f138cd2e3c8#rd" rel="nofollow">weixin</a></p>    <p> </p>