寫這篇文章的原因是我使用 reqeusts
庫請求接口的時候, 直接使用請求參數裏的 json
字段發送數據, 可是服務器沒法識別我發送的數據, 排查了很久才知道 requests
內部是使用 json.dumps
將字符串轉成 json
的, 而 json.dumps
默認狀況下會將 非ASCII
字符轉義, 也就是我發送數據中的中文被轉義了, 因此服務器沒法識別. 這篇文章雖然是 json.dumps
問題的總結, 但也會涉及到 字符編碼
問題, 因此就簡單先說一下 字符編碼
.html
Python
中的字符編碼在 Python3
中, 字符
在內存中是使用 Unicode
存儲的, 常規的字符使用 兩個字節
表示, 一些很生僻的字符就須要 四個字節
. 默認使用 Unicode
存儲是什麼意思呢, 那就是例子來解釋一下, 在 Python Shell
中輸入如下字符串 '\u4e2d\u6587'
, 觀察其輸出:python
In [51]: '\u4e2d\u6587'
Out[51]: '中文'
複製代碼
輸出的爲 中文
兩個字. 其實 \u4e2d
和 \u6587
分別表示 中
和 文
的 Unicode
編碼(術語稱爲 碼點
)的 十六進制
表示, 在 Python3
中以 \u
開頭的字符串被解析爲 Unicode
字符, 而後經過其十六進制 碼點
解析出具體的字符, 因此 中文
的內存表示即爲 \u4e2d\u6587
.json
Unicode
碼點標準庫提供了 ord
函數獲取一個字符的 Unicode
碼點, 使用 chr
函數將 碼點
轉換成 字符
, 下面是示例:bash
In [54]: ord('中')
Out[54]: 20013
In [56]: chr(20013)
Out[56]: '中'
複製代碼
輸出的 碼點
是使用 十進制
表示的, 可使用如下代碼將十進制數字格式化成十六進制字符串:服務器
'{0:04x}'.format(20013)
複製代碼
json.dumps
有了前面的鋪墊, 就能夠來講說 json.dumps
了. 下面以一個例子展開:網絡
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
才能正常顯示, 但參數名爲何叫 ensure_ascii
呢? 來看一下 官方文檔 對這個參數的解釋:函數
若是 ensure_ascii 是 true (即默認值),輸出保證將全部輸入的非 ASCII 字符轉義。若是 ensure_ascii 是 false,這些字符會原樣輸出。
複製代碼
如今稍微明白了, 在 ensure_ascii
爲 True
的狀況下, 若是字符串中存在 非ASCII
字符就將其轉義, 根據結果能夠知道這個字符被轉義爲 Unicode
編碼並格式化成了一個字符串, 注意 "\\u4e2d\\u6587"
與 "\u4e2d\u6587"
是不一樣的, 前者是長度爲 12
的字符串, 後者則會被 Python
直接解析爲 中文
, 長度爲 2
. 這也就是我一開始出現的問題, 直接將轉義的字符串在網絡上傳輸可能會沒法被識別. 好比 中文
被轉義成 \\u4e2d\\u6587
, 而服務器若是不知道它是被轉義過的字符串, 那它就是一個長度爲 12
的普通字符串, 確定會識別出錯. 而將 ensure_ascii
設爲 False
就不會進行轉義, 使用原始字符.編碼
若是服務器收到數據後發現是被轉化過的, 那怎麼識別呢? 其實被轉義字符串與使用 unicode_escape
對字符串進行編碼再使用 utf-8
進行解碼的結果一致, 代碼以下:spa
In [129]: msg
Out[129]: '中文'
In [130]: msg.encode('unicode_escape').decode('utf-8')
Out[130]: '\\u4e2d\\u6587'
複製代碼
因此識別只要反過來使用 utf-8
編碼再使用 unicode_escape
解碼就能夠了.調試
如今來看一下 json.dumps
究竟是怎麼對字符進行轉義的. 在 json.dumps
源碼中仔細調試的話會發現, 它調用的是 JSONEncoder.encode
方法, 而 encode
中的代碼片斷以下:
if self.ensure_ascii:
return encode_basestring_ascii(o)
else:
return encode_basestring(o)
複製代碼
它會根據 ensure_ascii
的值選擇調用函數. 而 encode_basestring_ascii
的值是 (c_encode_basestring_ascii or py_encode_basestring_ascii)
, 也就是默認是用 C
實現的版本, 其次使用 Python
實現的版本, 既然有 Python
版本, 固然要看一下是怎麼實現的, py_encode_basestring_ascii
能夠直接使用 from json.encoder import py_encode_basestring_ascii
導入, 直接在其內部就能夠調試. 下面是其源碼:
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
能夠看到它其實是 正則匹配替換
而後在先後添加 雙引號
. ESCAPE_ASCII
的定義以下:
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
複製代碼
其中 ([\\"]
用於匹配 \\
和 "
, 而 [^\ -~]
表示 \ -~
取反(這裏的反斜槓貌似是對空格進行轉義, 我不是很理解, 不進行轉義依舊能夠匹配到), 在 ASCII
表裏, 空格字符
對應十進制是 40
, ~
是 176
, 這是全部的 可打印字符
, 取反就是全部編碼不在 40 ~ 176
的字符, 因此中文就會被匹配到, 下面爲 ASCII表
:
對於匹配到的字符, 會傳入回調函數 replace
作轉義. replace
函數中的 ESCAPE_DCT
爲:
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
複製代碼
先從 ESCAPE_DCT
中獲取 製表符
、換行符
等經常使用字符的轉義, 若是失敗就獲取它的 Unicode
碼點, 而後判斷是否爲小於 0x10000
便是否爲 兩字節
字符(兩字節最大爲 0xFFFF
) , 若是是就格式化爲 Unicode
碼, 若是不是就使用 四字節
表示.
因此在使用 requests
時, 若是數據要使用 json
傳輸而且有 中文
, 那麼須要手動將 字典
進行 dump
.