## 对象的序列化和反序列化

### JSON 概述

通过上面的讲解，我们已经知道如何将文本数据和二进制数据保存到文件中，那么这里还有一个问题，如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢？在 Python 中，我们可以将程序中的数据以 JSON 格式进行保存。JSON 是“JavaScript Object Notation”的缩写，它本来是 JavaScript 语言中创建对象的一种字面量语法，现在已经被广泛的应用于跨语言跨平台的数据交换。使用 JSON 的原因非常简单，因为它结构紧凑而且是纯文本，任何操作系统和编程语言都能处理纯文本，这就是**实现跨语言跨平台数据交换**的前提条件。目前 JSON 基本上已经取代了 XML（可扩展标记语言）作为**异构系统间交换数据的事实标准**。可以在[ JSON 的官方网站](https://www.json.org/json-zh.html)找到更多关于 JSON 的知识，这个网站还提供了每种语言处理 JSON 数据格式可以使用的工具或三方库。

```JavaScript
{
    name: "骆昊",
    age: 40,
    friends: ["王大锤", "白元芳"],
    cars: [
        {"brand": "BMW", "max_speed": 240},
        {"brand": "Benz", "max_speed": 280},
        {"brand": "Audi", "max_speed": 280}
    ]
}
```

上面是 JSON 的一个简单例子，大家可能已经注意到了，它跟 Python 中的字典非常类似而且支持嵌套结构，就好比 Python 字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台（对于 Chrome 浏览器，可以通过“更多工具”菜单找到“开发者工具”子菜单，就可以打开浏览器的控制台），浏览器的控制台提供了一个运行 JavaScript 代码的交互式环境（类似于 Python 的交互式环境），下面的代码会帮我们创建出一个 JavaScript 的对象，我们将其赋值给名为`obj`的变量。

```JavaScript
let obj = {
    name: "骆昊",
    age: 40,
    friends: ["王大锤", "白元芳"],
    cars: [
        {"brand": "BMW", "max_speed": 240},
        {"brand": "Benz", "max_speed": 280},
        {"brand": "Audi", "max_speed": 280}
    ]
}
```

<img src="res/20210820143803.png" alt="image-20210820143756353" width="80%">

上面的`obj`就是 JavaScript 中的一个对象，我们可以通过`obj.name`或`obj["name"]`两种方式获取到`name`对应的值，如下图所示。可以注意到，`obj["name"]`这种获取数据的方式跟 Python 字典通过键获取值的索引操作是完全一致的，而 Python 中也通过名为`json`的模块提供了字典与 JSON 双向转换的支持。

<img src="res/20210820144411.png" width="85%">

我们在 JSON 中使用的数据类型（JavaScript 数据类型）和 Python 中的数据类型也是很容易找到对应关系的，大家可以看看下面的两张表。

表1：JavaScript 数据类型（值）对应的 Python 数据类型（值）

| JSON         | Python       |
| ------------ | ------------ |
| `object`      |`dict`|
| `array`      |`list`|
| `string`     | `str`        |
| `number ` |`int` / `float`|
| `number` (real)   |`float`|
| `boolean` (`true` / `false`) | `bool` (`True` / `False`) |
| `null`       | `None`       |

表2：Python 数据类型（值）对应的JavaScript数据类型（值）

| Python                      | JSON                         |
| --------------------------- | ---------------------------- |
| `dict`                      | `object`                     |
| `list` / `tuple`            | `array`                      |
| `str`                       | `string`                     |
| `int` / `float`             | `number`                     |
| `bool` （`True` / `False`） | `boolean` (`true` / `false`) |
| `None`                      | `null`                       |

### 读写 JSON 格式的数据

在 Python 中，如果要将字典处理成 JSON 格式（以字符串形式存在），可以使用`json`模块的`dumps`函数，代码如下所示。

```python
import json

my_dict = {
    'name': '骆昊',
    'age': 40,
    'friends': ['王大锤', '白元芳'],
    'cars': [
        {'brand': 'BMW', 'max_speed': 240},
        {'brand': 'Audi', 'max_speed': 280},
        {'brand': 'Benz', 'max_speed': 280}
    ]
}
print(json.dumps(my_dict))
```

运行上面的代码，输出如下所示，可以注意到中文字符都是用 Unicode 编码显示的。

```JSON
{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}
```

如果要将字典处理成 JSON 格式并写入文本文件，只需要将`dumps`函数换成`dump`函数并传入文件对象即可，代码如下所示。

```python
import json

my_dict = {
    'name': '骆昊',
    'age': 40,
    'friends': ['王大锤', '白元芳'],
    'cars': [
        {'brand': 'BMW', 'max_speed': 240},
        {'brand': 'Audi', 'max_speed': 280},
        {'brand': 'Benz', 'max_speed': 280}
    ]
}
with open('data.json', 'w') as file:
    json.dump(my_dict, file)
```

执行上面的代码，会创建`data.json`文件，文件的内容跟上面代码的输出是一样的。

`json`模块有四个比较重要的函数，分别是：

- `dump` - 将 Python 对象按照 JSON 格式序列化到文件中
- `dumps` - 将 Python 对象处理成 JSON 格式的字符串
- `load` - 将文件中的 JSON 数据反序列化成对象
- `loads` - 将字符串的内容反序列化成 Python 对象

这里出现了两个概念，一个叫序列化，一个叫反序列化，[维基百科](https://zh.wikipedia.org/)上的解释是：“序列化（serialization）在计算机科学的数据处理中，是指将数据结构或对象状态转换为可以存储或传输的形式，这样在需要的时候能够恢复到原先的状态，而且通过序列化的数据重新获取字节时，可以利用这些字节来产生原始对象的副本（拷贝）。与这个过程相反的动作，即从一系列字节中提取数据结构的操作，就是反序列化（deserialization）”。

我们可以通过下面的代码，读取上面创建的`data.json`文件，将 JSON 格式的数据还原成 Python 中的字典。

```python
import json

with open('data.json', 'r') as file:
    my_dict = json.load(file)
    print(type(my_dict))
    print(my_dict)
```

### 包管理工具 pip

Python 标准库中的`json`模块在数据序列化和反序列化时性能并不是非常理想，为了解决这个问题，可以使用三方库`ujson`来替换`json`。所谓三方库，是指非公司内部开发和使用的，也不是来自于官方标准库的 Python 模块，这些模块通常由其他公司、组织或个人开发，所以被称为三方库。虽然 Python 语言的标准库虽然已经提供了诸多模块来方便我们的开发，但是对于一个强大的语言来说，它的生态圈一定也是非常繁荣的。

之前安装 Python 解释器时，默认情况下已经勾选了安装 pip，大家可以在命令提示符或终端中通过`pip --version`来确定是否已经拥有了 pip。Pip 是 Python 的包管理工具，通过 pip 可以查找、安装、卸载、更新 Python 的三方库或工具，例如要安装替代`json`模块的`ujson`，可以使用下面的命令。

```Bash
pip install ujson
```

在默认情况下，pip 会访问`https://pypi.org/simple/`来获得三方库相关的数据，但是国内访问这个网站的速度并不是十分理想，因此国内用户可以使用豆瓣网或清华大学提供的镜像来替代这个默认的下载源，再执行安装三方库的操作，命令如下所示。

```Bash
pip config set global.index-url https://pypi.doubanio.com/simple
pip install ujson
```

可以通过`pip search`命令根据名字查找需要的三方库，可以通过`pip list`命令来查看已经安装过的三方库。如果想更新某个三方库，可以使用`pip install -U`或`pip install --upgrade`；如果要删除某个三方库，可以使用`pip uninstall`命令。

搜索`ujson`三方库。

```Bash
pip search ujson

micropython-cpython-ujson (0.2)  - MicroPython module ujson ported to CPython
pycopy-cpython-ujson (0.2)       - Pycopy module ujson ported to CPython
ujson (3.0.0)                    - Ultra fast JSON encoder and decoder for Python
ujson-bedframe (1.33.0)          - Ultra fast JSON encoder and decoder for Python
ujson-segfault (2.1.57)          - Ultra fast JSON encoder and decoder for Python. Continuing 
                                   development.
ujson-ia (2.1.1)                 - Ultra fast JSON encoder and decoder for Python (Internet 
                                   Archive fork)
ujson-x (1.37)                   - Ultra fast JSON encoder and decoder for Python
ujson-x-legacy (1.35.1)          - Ultra fast JSON encoder and decoder for Python
drf_ujson (1.2)                  - Django Rest Framework UJSON Renderer
drf-ujson2 (1.6.1)               - Django Rest Framework UJSON Renderer
ujsonDB (0.1.0)                  - A lightweight and simple database using ujson.
fast-json (0.3.2)                - Combines best parts of json and ujson for fast serialization
decimal-monkeypatch (0.4.3)      - Python 2 performance patches: decimal to cdecimal, json to 
                                   ujson for psycopg2
```

查看已经安装的三方库。

```Bash
pip list

Package                       Version
----------------------------- ----------
aiohttp                       3.5.4
alipay                        0.7.4
altgraph                      0.16.1
amqp                          2.4.2
...							  ...
```

更新`ujson`三方库。

```Bash
pip install -U ujson
```

删除`ujson`三方库。

```Bash
pip uninstall -y ujson
```

> **提示**：如果要更新`pip`自身，对于 macOS 系统来说，可以使用命令`pip install -U pip`。在 Windows 系统上，可以将命令替换为`python -m pip install -U --user pip`。

### 使用网络API获取数据

如果想在我们自己的程序中显示天气、路况、航班等信息，这些信息我们自己没有能力提供，所以必须使用网络数据服务。目前绝大多数的网络数据服务（或称之为网络 API）都是基于 [HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) 或 HTTPS 提供 JSON 格式的数据，我们可以通过 Python 程序发送 HTTP 请求给指定的 URL（统一资源定位符），这个 URL 就是所谓的网络 API，如果请求成功，它会返回 HTTP 响应，而 HTTP 响应的消息体中就有我们需要的 JSON 格式的数据。关于 HTTP 的相关知识，可以看看阮一峰的[《HTTP协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)一文。

国内有很多提供网络 API 接口的网站，例如[聚合数据](https://www.juhe.cn/)、[百度智能云](https://apis.baidu.com/)等，这些网站上有免费的和付费的数据接口，国外的[{API}Search](http://apis.io/)网站也提供了类似的功能，有兴趣的可以自行研究。下面的例子演示了如何使用[`requests`](http://docs.python-requests.org/zh_CN/latest/)库（基于 HTTP 进行网络资源访问的三方库）访问网络 API 获取国内新闻并显示新闻标题和链接。在这个例子中，我们使用了名为[天聚数行](https://www.tianapi.com/)的网站提供的国内新闻数据接口，其中的身份标识（API Key）需要自己到网站上注册申请。在天聚数行网站注册账号后会自动分配 API Key，但是要访问接口获取数据，需要绑定验证邮箱或手机，然后还要申请需要使用的接口，如下图所示。

<img src="res/20210820151134.png" alt="image-20210820151134034" width="100%">

Python 通过 URL 接入网络，我们推荐大家使用`requests`三方库，它简单且强大，但需要自行安装。

```Bash
pip install requests
```

获取国内新闻并显示新闻标题和链接。

```python
import requests

resp = requests.get('http://api.tianapi.com/guonei/?key=APIKey&num=10')
if resp.status_code == 200:
    data_model = resp.json()
    for news in data_model['newslist']:
        print(news['title'])
        print(news['url'])
        print('-' * 60)
```

上面的代码通过`requests`模块的`get`函数向天聚数行的国内新闻接口发起了一次请求，如果请求过程没有出现问题，`get`函数会返回一个`Response`对象，通过该对象的`status_code`属性表示 HTTP 响应状态码，如果不理解没关系，你只需要关注它的值，如果值等于`200`或者其他`2`字头的值，那么我们的请求是成功的。通过`Response`对象的`json()`方法可以将返回的 JSON 格式的数据直接处理成 Python 字典，非常方便。天聚数行国内新闻接口返回的 JSON 格式的数据（部分）如下图所示。

<img src="res/20210820154455.png" width="100%">

> **提示**：上面代码中的`APIKey`需要换成自己在天聚数行网站申请的API Key。天聚数行网站上还有提供了很多非常有意思的 API 接口，例如：垃圾分类、周公解梦等，大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档，文档中有关于如何使用接口的详细说明。

###  总结

Python 中实现序列化和反序列化除了使用`json`模块之外，还可以使用`pickle`和`shelve`模块，但是这两个模块是使用特有的序列化协议来序列化数据，因此序列化后的数据只能被 Python 识别，关于这两个模块的相关知识，有兴趣的读者可以自己查找网络上的资料。处理 JSON 格式的数据很显然是程序员必须掌握的一项技能，因为不管是访问网络 API 接口还是提供网络 API 接口给他人使用，都需要具备处理 JSON 格式数据的相关知识。

