詳解Python在使用JSON時(shí)需要注意的編碼問(wèn)題
寫(xiě)這篇文章的緣由是我使用 reqeusts 庫(kù)請(qǐng)求接口的時(shí)候, 直接使用請(qǐng)求參數(shù)里的 json 字段發(fā)送數(shù)據(jù), 但是服務(wù)器無(wú)法識(shí)別我發(fā)送的數(shù)據(jù), 排查了好久才知道 requests 內(nèi)部是使用 json.dumps 將字符串轉(zhuǎn)成 json 的, 而 json.dumps 默認(rèn)情況下會(huì)將 非ASCII 字符轉(zhuǎn)義, 也就是我發(fā)送數(shù)據(jù)中的中文被轉(zhuǎn)義了, 所以服務(wù)器無(wú)法識(shí)別. 這篇文章雖然是 json.dumps 問(wèn)題的總結(jié), 但也會(huì)涉及到 字符編碼 問(wèn)題, 所以就簡(jiǎn)單先說(shuō)一下 字符編碼.
Python 中的字符編碼
在 Python3 中, 字符 在內(nèi)存中是使用 Unicode 存儲(chǔ)的, 常規(guī)的字符使用 兩個(gè)字節(jié) 表示, 一些很生僻的字符就需要 四個(gè)字節(jié). 默認(rèn)使用 Unicode 存儲(chǔ)是什么意思呢, 那就是例子來(lái)解釋一下, 在 Python Shell 中輸入以下字符串 '\u4e2d\u6587', 觀察其輸出:
In [51]: '\u4e2d\u6587' Out[51]: '中文'
輸出的為 中文 兩個(gè)字. 其實(shí) \u4e2d 和 \u6587 分別表示 中 和 文 的 Unicode 編碼(術(shù)語(yǔ)稱(chēng)為 碼點(diǎn))的 十六進(jìn)制 表示, 在 Python3 中以 \u 開(kāi)頭的字符串被解析為 Unicode 字符, 然后通過(guò)其十六進(jìn)制 碼點(diǎn) 解析出具體的字符, 所以 中文 的內(nèi)存表示即為 \u4e2d\u6587.
獲取字符 Unicode 碼點(diǎn)
標(biāo)準(zhǔn)庫(kù)提供了 ord 函數(shù)輸出一個(gè)字符的 Unicode 碼點(diǎn), 使用 chr 函數(shù)將 碼點(diǎn) 轉(zhuǎn)換成 字符, 下面是示例:
In [54]: ord('中') Out[54]: 20013 In [56]: chr(20013) Out[56]: '中'
輸出的 碼點(diǎn) 是使用 十進(jìn)制 表示的, 可以使用以下代碼將整數(shù)格式化成十六進(jìn)制字符串:
'{0:04x}'.format(20013)
使用 json.dumps
有了前面的鋪墊, 就可以來(lái)說(shuō)說(shuō) json.dumps 了. 下面以一個(gè)例子展開(kāi):
In [121]: json.dumps('中文', ensure_ascii=True) Out[121]: '"\\u4e2d\\u6587"' In [122]: json.dumps('中文', ensure_ascii=False) Out[122]: '"中文"'
可以看到, 在 ensure_ascii 為 True 的情況下, 中文 被編碼成了 Unicode 碼, 為 False 才能正常顯示, 但是這跟 ASCII 有什么關(guān)系呢? 來(lái)看一下官方文檔 對(duì)這個(gè)參數(shù)的解釋:
如果 ensure_ascii 是 true (即默認(rèn)值),輸出保證將所有輸入的非 ASCII 字符轉(zhuǎn)義。如果 ensure_ascii 是 false,這些字符會(huì)原樣輸出。
現(xiàn)在稍微明白了, 在 ensure_ascii 為 True 的情況下, 如果字符串中存在 非ASCII 字符就將其轉(zhuǎn)義, 根據(jù)結(jié)果可以知道這個(gè)字符被轉(zhuǎn)義為 Unicode 碼并格式化成了一個(gè)字符串, 注意 "\\u4e2d\\u6587" 與 "\u4e2d\\u6587" 是不同的, 前者是長(zhǎng)度為 12 的字符串, 后者會(huì)被 Python 直接解析為 中文, 長(zhǎng)度為 2. 這也就是我一開(kāi)始出現(xiàn)的問(wèn)題, 直接將轉(zhuǎn)義的字符串在網(wǎng)絡(luò)上傳輸可能會(huì)無(wú)法被識(shí)別. 比如 中文 被轉(zhuǎn)義成 \\u4e2d\\u6587, 而服務(wù)器如果不知道它是被轉(zhuǎn)義過(guò)的字符串, 那它就是一個(gè)長(zhǎng)度為 12 的普通字符串, 肯定會(huì)識(shí)別出錯(cuò). 而將 ensure_ascii 設(shè)為 False 就不會(huì)進(jìn)行轉(zhuǎn)義, 使用原始字符.
識(shí)別轉(zhuǎn)義字符
如果服務(wù)器收到數(shù)據(jù)后發(fā)現(xiàn)是被轉(zhuǎn)化過(guò)的, 那怎么識(shí)別呢? 其實(shí)被轉(zhuǎn)義字符串與使用 unicode_escape 對(duì)字符串進(jìn)行編碼再使用 utf-8 進(jìn)行解碼的結(jié)果一致, 代碼如下:
In [129]: msg Out[129]: '中文' In [130]: msg.encode('unicode_escape').decode('utf-8') Out[130]: '\\u4e2d\\u6587'
所以識(shí)別只要反過(guò)來(lái)使用 utf-8 編碼再使用 unicode_escape 解碼就可以了.
轉(zhuǎn)義是如何進(jìn)行的
現(xiàn)在來(lái)看一下 json 到底是怎么對(duì)字符進(jìn)行轉(zhuǎn)義的. 在 json.dumps 源碼中仔細(xì)調(diào)試的話會(huì)發(fā)現(xiàn), 它調(diào)用的是 JSONEncoder.encode 方法, 而 encode 中的代碼片段如下:
if self.ensure_ascii: return encode_basestring_ascii(o) else: return encode_basestring(o)
它會(huì)根據(jù) ensure_ascii 的值選擇調(diào)用函數(shù). 而 encode_basestring_ascii 的值是 (c_encode_basestring_ascii or py_encode_basestring_ascii), 也就是默認(rèn)是用 C 實(shí)現(xiàn)的版本, 其次使用 Python 實(shí)現(xiàn)的版本, 既然有 Python 版本, 當(dāng)然要看一下是怎么實(shí)現(xiàn)的, py_encode_basestring_ascii 可以直接使用 from json.encoder import py_encode_basestring_ascii 導(dǎo)入, 直接在其內(nèi)部就可以調(diào)試. 下面是其源碼:
def py_encode_basestring_ascii(s): """Return an ASCII-only JSON representation of a Python string """ def replace(match): s = match.group(0) try: return ESCAPE_DCT[s] except KeyError: n = ord(s) if n < 0x10000: return '\\u{0:04x}'.format(n) #return '\\u%04x' % (n,) else: # surrogate pair n -= 0x10000 s1 = 0xd800 | ((n >> 10) & 0x3ff) s2 = 0xdc00 | (n & 0x3ff) return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) return '"' + ESCAPE_ASCII.sub(replace, s) + '"'
從最后的 return 可以看到它實(shí)際上是 正則替換 最后在前后添加 雙引號(hào). ESCAPE_ASCII 的定義如下:
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
其中 ([\\"] 用于匹配 \\ 和 ", 而 [^\ -~] 表示 \ -~ 取反(這里的反斜杠貌似是對(duì)空格進(jìn)行轉(zhuǎn)義, 我不是很理解, 不進(jìn)行轉(zhuǎn)義依舊可以匹配到), 在 ASCII 表里, 空格字符 對(duì)應(yīng)十進(jìn)制是 40, ~ 是 176, 這是所有的可打印字符, 取反就是所有編碼不在 40 ~ 176 的字符, 所以中文就會(huì)被匹配到, 下面為 ASCII表:
對(duì)于匹配到的字符, 會(huì)傳入回調(diào)函數(shù) replace 做轉(zhuǎn)義. replace 函數(shù)中的 ESCAPE_DCT 為:
ESCAPE_DCT = { '\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', }
會(huì)對(duì)常用字符進(jìn)行轉(zhuǎn)義, 如果失敗就獲取它的 Unicode 碼點(diǎn), 然后判斷是否為小于 0x10000 即是否為 兩字節(jié) 字符(兩字節(jié)最大為0xFFFF) , 如果是就格式化為 Unicode 碼, 如果不是就使用 四字節(jié) 表示.
總結(jié)
記得使用 requests 發(fā)送 JSON 數(shù)據(jù)時(shí)將中文編碼.
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
opencv檢測(cè)動(dòng)態(tài)物體的實(shí)現(xiàn)
本文主要介紹了opencv檢測(cè)動(dòng)態(tài)物體的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07Python學(xué)習(xí)筆記之字符串和字符串方法實(shí)例詳解
這篇文章主要介紹了Python學(xué)習(xí)筆記之字符串和字符串方法,結(jié)合實(shí)例形式詳細(xì)分析了Python字符串相關(guān)操作函數(shù)與使用技巧,需要的朋友可以參考下2019-08-08Python實(shí)現(xiàn)圣誕樹(shù)的多種方法
這篇文章主要為大家介紹了Python實(shí)現(xiàn)圣誕樹(shù)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2021-12-12詳解利用OpenCV提取圖像中的矩形區(qū)域(PPT屏幕等)
這篇文章主要介紹了詳解利用OpenCV提取圖像中的矩形區(qū)域(PPT屏幕等),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-07-07Python疊加兩幅柵格圖像的實(shí)現(xiàn)方法
今天小編就為大家分享一篇Python疊加兩幅柵格圖像的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07詳解Pytorch+PyG實(shí)現(xiàn)GAT過(guò)程示例
這篇文章主要為大家介紹了Pytorch+PyG實(shí)現(xiàn)GAT過(guò)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04在pycharm中輸入import torch報(bào)錯(cuò)如何解決
這篇文章主要介紹了在pycharm中輸入import torch報(bào)錯(cuò)如何解決問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01