JSON 的正確用法:Python喷兼、MongoDB篮绰、JavaScript與Ajax

作者:rainy.im
原文地址:http://blog.rainy.im/2016/05/14/json-the-right-way/

關(guān)于本文

本文主要總結(jié)網(wǎng)站編寫以來在傳遞 JSON 數(shù)據(jù)方面遇到的一些問題,以及目前采用的解決方案褒搔。網(wǎng)站數(shù)據(jù)庫采用 MongoDB阶牍,后端是 Python,前端采用“半分離”形式的 Riot.js星瘾,所謂半分離走孽,是說第一頁數(shù)據(jù)是通過服務(wù)器端的模板引擎直接渲染到 HTML 中,從而避免首頁兩次加載的問題琳状,而其它動(dòng)態(tài)內(nèi)容則采用 Ajax 加載磕瓷。整個(gè)流程中數(shù)據(jù)都是通過 JSON 格式傳遞的,但是念逞,在不同的環(huán)節(jié)中困食,需要采用不同的方式,并遇到一些不同的問題翎承。本文主要做記錄硕盹、總結(jié)。

json
  1. What is JSON?
    --

JSON(JavaScript Object Notation) 是一種由道格拉斯·克羅克福特構(gòu)想設(shè)計(jì)叨咖、輕量級的數(shù)據(jù)交換語言瘩例,它的前輩 XML 可能更早被人們所熟知。當(dāng)然 JSON 并不是為了取代 XML 而存在的甸各,只是相比于 XML 它更小巧垛贤、更適合在網(wǎng)頁開發(fā)中用作數(shù)據(jù)傳遞(JSON 之于 JavaScript 就像 XML 之于 Lisp)。從名字上可以看出趣倾,JSON 的格式符合 JavaScript 語言中“對象”的語法格式聘惦,除了 JavaScript 之外,很多其他語言中也具有類似的類型儒恋,例如 Python 中的字典(dict)善绎,除了編程語言之外脚线,一些基于文檔存儲的 NoSQL 非關(guān)系型數(shù)據(jù)庫也選擇 JSON 作為其數(shù)據(jù)存儲格式张漂,例如 MongoDB。

總的來說侍芝,JSON 定義一種標(biāo)記格式箱锐,可以非常方便地在編程語言中的變量數(shù)據(jù)與字符串文本數(shù)據(jù)之間相互轉(zhuǎn)換。JSON 描述的數(shù)據(jù)結(jié)構(gòu)包括以下這幾種形式:

  1. 對象:{key: value}
  2. 列表:[obj, obj,...]
  3. 字符串:"string"
  4. 數(shù)字:數(shù)字
  5. 布爾值:true/false

了解了 JSON 的基本概念之后劳较,下面分別針對上圖中的幾個(gè)數(shù)據(jù)交互環(huán)節(jié)進(jìn)行總結(jié)驹止。

  1. Python <=> MongoDB
    --

Python 與 MongoDB 之間的交互主要由現(xiàn)有的驅(qū)動(dòng)庫提供支持浩聋,包括 PyMongo、Motor 等臊恋,而這些驅(qū)動(dòng)所提供的接口都是非常友好的衣洁,我們不需要了解任何底層的實(shí)現(xiàn),只要對 Python 原生的字典類型進(jìn)行操作即可:

import motor  
client = motor.motor_tornado.MotorClient()  
db     = client['test']

user_col = db['user']  
user_col.insert(dict(  
  name = 'Yu',
  is_admin = True,
))

唯一需要注意的是 MongoDB 中的索引項(xiàng)_id是通過ObjectId("572df0b78a83851d5f24e2c1")存儲的抖仅,而對應(yīng)的 Python 對象為bson.objectid.ObjectId坊夫,因此在查詢時(shí)需要以此對象的實(shí)例進(jìn)行:

from bson.objectid import ObjectId  
user = db.user.find_one(dict(  
  _id = ObjectId("572df0b78a83851d5f24e2c1")
  ))
  1. Python <=> Ajax
    --

前端與后端之間的數(shù)據(jù)交流比較常用的是通過 Ajax 完成,這時(shí)遇到了第一個(gè)不大不小的坑撤卢。在之前的一篇文章中环凿,我總結(jié)了一次 Python 編碼的坑,我們知道 HTTP 傳遞過程中肯定不存在 JSON/XML 放吩,一切都是二進(jìn)制數(shù)據(jù)智听,但是我們可以選擇讓前端用什么樣的方式解讀這些數(shù)據(jù),即通過設(shè)定 Header 中的 Content-Type渡紫,一般傳遞 JSON 數(shù)據(jù)時(shí)將其設(shè)定為Content-Type: application/json到推,在 Tornado 最新版本中,只需要直接寫入字典類型即可:

# Handler
async def post(self):  
  user = await self.db.user.find_one({})
  self.write(user)

于是迎來了第一個(gè)錯(cuò)誤:TypeError: ObjectId('572df0b58a83851d5f24e2b1') is not JSON serializable惕澎。追溯原因莉测,雖然 Tornado 幫我們簡化了操作,但在像 HTTP 中寫入字典類型時(shí)仍然需要經(jīng)歷一次json.dumps(user)操作唧喉,而對于json.dumps來說捣卤,ObjectId類型是非法的。于是我選擇了最直觀的解決方案:

import json  
from bson.objectid import ObjectId  
class JSONEncoder(json.JSONEncoder):  
  def default(self, obj):
    if isinstance(obj, ObjectId):
      return str(obj)
    return super().default(self, obj)

# Handler
async def post(self):  
  user = await self.db.user.find_one({})
  self.write(JSONEncoder.encode(user))

這次不會(huì)再出錯(cuò)了欣喧,我們自己的JSONEncoder可以應(yīng)對ObjectId了腌零,但另一個(gè)問題也出現(xiàn)了:

JSONEncoder.encode之后字典類型被轉(zhuǎn)換成字符串,寫入 HTTP 之后Content-Type變?yōu)?code>text/html唆阿,這時(shí)前端將認(rèn)為接收的數(shù)據(jù)為字符串益涧,而不是可用的 JavaScript Object。當(dāng)然還有進(jìn)一步的彌補(bǔ)方案驯鳖,那就是前端再進(jìn)行一次轉(zhuǎn)換:

$.post(API, {}, function(res){
  data = JSON.parse(res);
  console.log(data._id);
})

問題暫時(shí)解決了闲询,在整個(gè)過程中 JSON 的變換是這樣的:

Python ==> json.dumps ==> HTTP   ==> JavaScript  ==> JSON.parse  
dict   ==> str        ==> binary ==> string      ==> Object

結(jié)果第二個(gè)問題來了,當(dāng)數(shù)據(jù)中存在一些特殊字符時(shí)浅辙,JSON.parse將出現(xiàn)錯(cuò)誤:

JSON.parse("{'abs': '\n'}");  
// VM536:1 Uncaught SyntaxError: Unexpected token ' in JSON at position 1(…)

這就是在遇到問題時(shí)扭弧,只著眼解決眼前的錯(cuò)誤,導(dǎo)致后續(xù)一連串改動(dòng)所帶來的弊病记舆。我們沿著上面 JSON 變換的鏈條向上追溯鸽捻,看有沒有更好的解決方案。很簡單,遵循傳統(tǒng)規(guī)則御蒲,出現(xiàn)特例的時(shí)候衣赶,改變自身適應(yīng)規(guī)則,而不是改變規(guī)則

# Handler
async def post(self):  
  user = await self.db.user.find_one({})
  user['_id'] = str(user['_id'])
  self.write(user)

當(dāng)然厚满,如果是多條數(shù)據(jù)的列表形式府瞄,還需要進(jìn)一步改造:

# DB
async def get_top_users(self, n = 20):  
  users = []
  async for user in self.db.user.find({}).sort('rank', -1).limit(n):
    user['_id'] = str(user['_id'])
    users.append(user)
  return users
  1. Python <=> HTML+Riot.js
    --

如果上面的問題可以通過遵守規(guī)則來解決,那么接下來這個(gè)問題就是一個(gè)挑戰(zhàn)規(guī)則的故事碘箍。除去 Ajax 動(dòng)態(tài)加載部分遵馆,網(wǎng)頁上的其他數(shù)據(jù)是通過后端模板引擎渲染得來的,也就是說是 Hard-coding 為 HTML 的丰榴。在瀏覽器加載并解析這個(gè) HTML 文件之前货邓,它們只是純文本文件,而我們需要的是直接將數(shù)據(jù)塞僅<script>標(biāo)簽在瀏覽器運(yùn)行JavaScript時(shí)直接可用多艇。嚴(yán)格意義上來說逻恐,這并不算是 JSON 的應(yīng)用,而是 Python 的dict與 JavaScript 的Object之間的直接轉(zhuǎn)換峻黍。常規(guī)的方法應(yīng)該這樣寫:

# Handler
async def get(self):  
  users = self.db.get_top_users()
  render_data = dict(
    users = users
  )
  self.render('users.html', **render_data)
<!-- HTML + Riot.js -->  
<app></app>  
<script>  
  riot.mount('app', {
    users: [
        {% for user in users %}
          { name: "{{ user['name']}}", is_admin: "{{ user['is_admin']}}" },
        {% end %}
      ],
  })
</script>  

這樣寫是對的复隆,但是要解決上面提到的ObjectId()問題還是需要一些額外的處理(尤其是引號問題)。另外為了解決ObjectId的問題我還嘗試了一種比較蠢的方法(在上面的JSON.parse遇到錯(cuò)誤之前):

# Handler
async def get(self):  
  users = self.db.get_top_users()
  render_data = dict(
    users = JSONEncoder.encode(users)
  )
  self.render('users.html', **render_data)
<!-- HTML + Riot.js -->  
<app></app>  
<script>  
  riot.mount('app', {
    users: JSON.parse('{{ users }}'),
  })
</script>  

其實(shí)跟第 3 小節(jié)的問題一樣姆涩,模板引擎渲染過程與 HTTP 傳輸過程是類似的挽拂。不同的是,在模板中字符串變量就是純粹的值(沒有引號)骨饿,因此完全可以用生成 JavaScript 腳本文件的形式渲染變量亏栈,而無需顧慮特殊字符。(下面的 {% raw ... %}是 Tornado 模板宏赘,用于防止特殊符號被 HTML 編碼的語法):

<!-- HTML + Riot.js -->  
<app></app>  
<script>  
  riot.mount('app', {
    users: {% raw users %}),
  })
</script>  

總結(jié)

JSON 是很好用的數(shù)據(jù)格式绒北,但是在不同語言環(huán)境之間切換還是有很多細(xì)節(jié)問題需要注意。此外察署,遵循傳統(tǒng)規(guī)則闷游,出現(xiàn)特例的時(shí)候,改變自身適應(yīng)規(guī)則贴汪,而不是試圖改變規(guī)則脐往,這一條不一定適應(yīng)所有問題,但對于那些已被公認(rèn)的規(guī)則扳埂,請勿輕易挑戰(zhàn)业簿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市阳懂,隨后出現(xiàn)的幾起案子梅尤,更是在濱河造成了極大的恐慌柜思,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件克饶,死亡現(xiàn)場離奇詭異酝蜒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)矾湃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堕澄,“玉大人邀跃,你說我怎么就攤上這事⊥茏希” “怎么了拍屑?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坑傅。 經(jīng)常有香客問我僵驰,道長,這世上最難降的妖魔是什么唁毒? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任蒜茴,我火速辦了婚禮,結(jié)果婚禮上浆西,老公的妹妹穿的比我還像新娘粉私。我一直安慰自己,他們只是感情好近零,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布诺核。 她就那樣靜靜地躺著,像睡著了一般久信。 火紅的嫁衣襯著肌膚如雪窖杀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天裙士,我揣著相機(jī)與錄音入客,去河邊找鬼。 笑死潮售,一個(gè)胖子當(dāng)著我的面吹牛痊项,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酥诽,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鞍泉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肮帐?” 一聲冷哼從身側(cè)響起咖驮,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤边器,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后托修,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忘巧,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年睦刃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砚嘴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涩拙,死狀恐怖际长,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兴泥,我是刑警寧澤工育,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站搓彻,受9級特大地震影響如绸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旭贬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一怔接、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骑篙,春花似錦蜕提、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杨名,卻和暖如春脏榆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背台谍。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工须喂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人趁蕊。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓坞生,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掷伙。 傳聞我的和親對象是個(gè)殘疾皇子是己,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容