本文主要总结网站编写以来在传递 JSON 数据方面遇到的一些问题以及目前采用的解决方案。网站数据库采用 MongoDB,后端是 Python,前端采用“半分离”形式的 Riot.js,所谓半分离,是说第一页数据是通过服务器端的模板引擎直接渲染到 HTML 中,从而避免首页两次加载的问题,而其它动态内容则采用 Ajax 加载。整个流程中数据都是通过 JSON 格式传递的,但是在不同的环节中需要采用不同的方式并遇到一些不同的问题,本文主要做记录、总结。

1. What is JSON?

JSON(JavaScript Object Notation) 是一种由道格拉斯·克罗克福特构想设计、轻量级的数据交换语言,它的前辈 XML 可能更早被人们所熟知。当然 JSON 并不是为了取代 XML 而存在的,只是相比于 XML 它更小巧、更适合在网页开发中用作数据传递(JSON 之于 JavaScript 就像 XML 之于 Lisp)。从名字上可以看出,JSON 的格式符合 JavaScript 语言中“对象”的语法格式,除了 JavaScript 之外,很多其他语言中也具有类似的类型,例如 Python 中的字典(dict),除了编程语言之外,一些基于文档存储的 NoSQL 非关系型数据库也选择 JSON 作为其数据存储格式,例如 MongoDB。

总的来说,JSON 定义一种标记格式,可以非常方便地在编程语言中的变量数据与字符串文本数据之间相互转换。JSON 描述的数据结构包括以下这几种形式:

  1. 对象:{key: value}
  2. 列表:[obj, obj,...]
  3. 字符串:"string"
  4. 数字:数字
  5. 布尔值:true/false

了解了 JSON 的基本概念之后,下面分别针对上图中的几个数据交互环节进行总结。

2. Python <=> MongoDB

Python 与 MongoDB 之间的交互主要由现有的驱动库提供支持,包括 PyMongo、Motor 等,而这些驱动所提供的接口都是非常友好的,我们不需要了解任何底层的实现,只要对 Python 原生的字典类型进行操作即可:

import motor
client = motor.motor_tornado.MotorClient()
db = client['test'] user_col = db['user']
user_col.insert(dict(
name = 'Yu',
is_admin = True,
))

唯一需要注意的是 MongoDB 中的索引项 _id 是通过 ObjectId("572df0b78a83851d5f24e2c1")存储的,而对应的 Python 对象为 bson.objectid.ObjectId,因此在查询时需要以此对象的实例进行:

from bson.objectid import ObjectId
user = db.user.find_one(dict(
_id = ObjectId("572df0b78a83851d5f24e2c1")
))

3. Python <=> Ajax

前端与后端之间的数据交流比较常用的是通过 Ajax 完成,这时遇到了第一个不大不小的坑。在之前的一篇文章中,我总结了一次 Python 编码的坑,我们知道 HTTP 传递过程中肯定不存在 JSON/XML ,一切都是二进制数据,但是我们可以选择让前端用什么样的方式解读这些数据,即通过设定 Header 中的 Content-Type,一般传递 JSON 数据时将其设定为 Content-Type: application/json,在 Tornado 最新版本中,只需要直接写入字典类型即可:

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

于是迎来了第一个错误:TypeError: ObjectId('572df0b58a83851d5f24e2b1') is not JSON serializable。追溯原因,虽然 Tornado 帮我们简化了操作,但在像 HTTP 中写入字典类型时仍然需要经历一次 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))

这次不会再出错了,我们自己的 JSONEncoder 可以应对 ObjectId 了,但另一个问题也出现了:

JSONEncoder.encode 之后字典类型被转换成字符串,写入 HTTP 之后 Content-Type 变为text/html,这时前端将认为接收的数据为字符串而不是可用的 JavaScript Object。当然还有进一步的弥补方案,那就是前端再进行一次转换:

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

问题暂时解决了,在整个过程中 JSON 的变换是这样的:

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

结果第二个问题来了,当数据中存在一些特殊字符时,JSON.parse 将出现错误:

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

这就是在遇到问题是只着眼解决眼前错误导致后续一连串改动所带来的弊病。我们沿着上面 JSON 变换的链条向上追溯,看有没有更好的解决方案。很简单,遵循传统规则,出现特例的时候,改变自身适应规则,而不是改变规则:

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

当然,如果是多条数据的列表形式,还需要进一步改造:

# 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

4. Python <=> HTML+Riot.js

如果上面的问题可以通过遵守规则来解决,那么接下来这个问题就是一个挑战规则的故事。除去 Ajax 动态加载部分,网页上的其他数据是通过后端模板引擎渲染得来的,也就是说是 Hard-coding 为 HTML 的。在浏览器加载并解析这个 HTML 文件之前它们只是纯文本文件,而我们需要的是直接将数据塞仅 <script> 标签在浏览器运行 JavaScript 时直接可用。严格意义上来说这并不算是 JSON 的应用,而是 Python 的 dict 与 JavaScript 的 Object 之间的直接转换,常规的方法应该这样写:

# 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 遇到错误之前):

# 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>

其实跟第 3 小节的问题一样,模板引擎渲染过程与 HTTP 传输过程是类似的,不同的是在模板中字符串变量就是纯粹的值(没有引号),因此完全可以用生成 JavaScript 脚本文件的形式渲染变量而无需顾虑特殊字符(下面的 {% raw ... %} 是 Tornado 模板用于防止特殊符号被 HTML 编码的语法):

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

总结

JSON 是很好用的数据格式,但是在不同语言环境之间切换还是有很多细节问题需要注意。此外,遵循传统规则,出现特例的时候,改变自身适应规则,而不是试图改变规则,这一条不一定适应所有问题,但对于那些已被公认的规则,请勿轻易挑战。

JSON 的正确用法:Python、MongoDB、JavaScript与AjaxJSON 的正确用法:Python、MongoDB、JavaScript与Ajax的更多相关文章

  1. 浅谈Python在信息学竞赛中的运用及Python的基本用法

    浅谈Python在信息学竞赛中的运用及Python的基本用法 前言 众所周知,Python是一种非常实用的语言.但是由于其运算时的低效和解释型编译,在信息学竞赛中并不用于完成算法程序.但正如LRJ在& ...

  2. MongoDB 安装详细教程 + 常用命令 + 与 Python 的交互

    MongoDB 简介 MongoDB (名称来自 humongous/巨大无比的, 是一个可扩展的高性能,开源,模式自由,面向文档的NoSQL,基于 分布式 文件存储,由 C++ 语言编写,设计之初旨 ...

  3. Python爬虫进阶四之PySpider的用法

    审时度势 PySpider 是一个我个人认为非常方便并且功能强大的爬虫框架,支持多线程爬取.JS动态解析,提供了可操作界面.出错重试.定时爬取等等的功能,使用非常人性化. 本篇内容通过跟我做一个好玩的 ...

  4. Python爬虫利器六之PyQuery的用法

    前言 你是否觉得 XPath 的用法多少有点晦涩难记呢? 你是否觉得 BeautifulSoup 的语法多少有些悭吝难懂呢? 你是否甚至还在苦苦研究正则表达式却因为少些了一个点而抓狂呢? 你是否已经有 ...

  5. python中的 try...except...finally 的用法

    python中的 try...except...finally 的用法 author:headsen chen date:2018-04-09  16:22:11 try, except, final ...

  6. 孤荷凌寒自学python第五十七天初次尝试使用python来连接远端MongoDb数据库

    孤荷凌寒自学python第五十七天初次尝试使用python来连接远端MongoDb数据库 (完整学习过程屏幕记录视频地址在文末) 今天是学习mongoDB数据库的第三天.感觉这个东西学习起来还是那么困 ...

  7. 孤荷凌寒自学python第五十六天通过compass客户端和mongodb shell 命令来连接远端MongoDb数据库

    孤荷凌寒自学python第五十六天通过compass客户端和mongodb shell 命令来连接远端MongoDb数据库 (完整学习过程屏幕记录视频地址在文末) 今天是学习mongoDB数据库的第二 ...

  8. PYTHON 爬虫笔记七:Selenium库基础用法

    知识点一:Selenium库详解及其基本使用 什么是Selenium selenium 是一套完整的web应用程序测试系统,包含了测试的录制(selenium IDE),编写及运行(Selenium ...

  9. python浅学【网络服务中间件】之MongoDB

    一.关于MongoDB: MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统. 在高负载的情况下,添加更多的节点,可以保证服务器性能. MongoDB 旨在为WEB应用提供 ...

随机推荐

  1. Centos 6.X noVNC+websockify 实现webvnc

    文章参考:https://github.com/kanaka/noVNC http://www.cnblogs.com/yanghuahui/p/3574388.html 工作原理: noVNC 可以 ...

  2. WCF服务返回XML或JSON格式数据

    第一种方式public string GetData( string format) { string res = null; Student stu = new Student { StuID = ...

  3. Jmeter文章索引贴

    一.基础部分: 使用Jmeter进行http接口测试 Jmeter之Http Cookie Manager Jmeter之HTTP Request Defaults Jmeter之逻辑控制器(Logi ...

  4. Socket的错误码和描述

    //下面是Socket Error的错误码和描述: Socket error 0 - Directly send error  Socket error 10004 - Interrupted fun ...

  5. windowsphone8.1学习笔记之应用数据(二)

    上一篇说了应用数据的应用设置,这篇说说应用文件,应用文件主要分为三种:本地应用文件.漫游应用文件和临时应用文件. 获取根目录方法如下,都是返回一个StorageFolder对象(稍后介绍这个). // ...

  6. 中文WebFont探索

    本文主要讲中文webFont的相关知识,包含了业界现状.WebFont优势.实现方案等. 一 业界使用WebFont现状 1.1 英文WebFont使用现状 英文版已使用非常广泛.比较有名的字体库:G ...

  7. CALL FUNCTION 'BAPI_PO_CREATE1' 相关报错

    *&---------------------------------------------------------------------**& Report  ZQJ06*&am ...

  8. Memcached的优点

    核心知识点 memcached总的特点:简单.稳定.专注 1.简单的通信协议 a.通信协议:TCP b.序列化协议:文本的自定义协议 2.丰富的客户端程序:几乎支持所有的网络编程语言 3.高性能的网络 ...

  9. LeetCode:矩形区域【223】

    LeetCode:矩形区域[223] 题目描述 在二维平面上计算出两个由直线构成的矩形重叠后形成的总面积. 每个矩形由其左下顶点和右上顶点坐标表示,如图所示. 示例: 输入: -3, 0, 3, 4, ...

  10. turbolink 造成 link_to异常

    link_to  点击之后不刷新不加载文件 send_file 只能在浏览器中打开,而不能下载 解决方法:注释gem turbolink,application.js 中移除turbolink