在Python中使用JSON

2个月前 147次点击 来自 其他

收录专题: Python边学边译

标签: Python

原文链接: Working With JSON Data in Python

JSON 已迅速成为信息交换的事实上的标准。也许您是通过API收集信息或将数据存储在文档数据库中,不管是在何种场景下使用,JSON都是您在Python中的最佳选择。

我们只使用JSON来存储和交换数据吗? 是又不是,它不过是社区用来传递数据的标准格式。请记住,JSON不是可用于此类工作的唯一格式,XMLYAML可能是唯一值得一提的其他格式。

JSON简史

最初,JSON只是JavaScript中用于处理对象的文本语法。只不过到如今,JSON已成为语言无关语言以来就很久了,并且作为其自己的标准存在,因此,为了便于讨论,我们可以避免使用JavaScript。

以下是一段简短的JSON:

{
    "firstName": "Jane",
    "lastName": "Doe",
    "hobbies": ["running", "sky diving", "singing"],
    "age": 35,
    "children": [
        {
            "firstName": "Alice",
            "age": 6
        },
        {
            "firstName": "Bob",
            "age": 8
        }
    ]
}

如您所见,JSON支持基本类型,例如字符串和数字,以及嵌套列表和对象。

Python原生支持JSON

Python带有一个内置程序包json用于编码和解码JSON数据:

import json

编码JSON的过程通常称为序列化。该术语指的是将数据转换为一系列字节(因此称为serial)以在网络上存储或传输。自然,反序列化是对已经以JSON标准存储或传递的数据进行解码的对等过程。

序列化JSON

计算机处理大量信息后会发生什么?它需要进行数据转储。因此,json库dump()函数是用于将数据写入文件的方法(而dumps()是用于写入字符串的方法)。

根据相当直观的转换,将简单的Python对象转换为JSON。

Python JSON
dict object
list, tuple array
str string
int,long,float number
True true
False false
None null

一个简单的序列化示例

想象一下,您正在使用内存中的Python对象,看起来有点像这样:

data = {
    "president": {
        "name": "Zaphod Beeblebrox",
        "species": "Betelgeusian"
    }
}

将这些信息保存到磁盘至关重要,因此您的任务是将其写入文件。

使用Python的上下文管理器,您可以创建一个名为的文件data_file.json并以写入模式打开它:

with open("data_file.json", "w") as write_file:
    json.dump(data, write_file)

dump()带有两个位置参数:

  • 1.要序列化的数据对象
  • 2.将要写入的类文件字节对象(file-like object)

或者,如果您倾向于继续在程序中使用此序列化的JSON数据,则可以将其写入本地Python str对象:

json_string = json.dumps(data)

请注意,由于您实际上并未将数据写入磁盘,因此不存在类文件字节对象(file-like object)。除此之外,dumps()dump()功能类似。

一些有用的关键字参数

请记住,JSON代码格式易于人类阅读,但是如果将所有语法压缩在一起,那么语法可读性就不那么明显。如果,您拥有与我不同的编程风格,并且按自己的喜好格式化代码后,阅读起来可能会更容易。

您可以使用indent关键字参数来指定嵌套结构的缩进大小。通过使用data上面定义的,并在控制台中运行以下命令,查看异同:

json.dumps(data)
json.dumps(data, indent=4)

另一个格式化选项是 separators关键字参数。默认情况下,这是字符串的2个分隔符字符(", ", ": "),但是紧凑型JSON的常见替代方法是(",", ":")。再次查看示例JSON,看看这些分隔符在哪里起作用。

反序列化JSON

在json库中,您将找到load()loads(),它们是将JSON编码的数据转换为Python对象的方法。

与序列化一样,有一个简单的反序列化转换表,尽管您可能已经猜到了它的样子。

Python JSON
object dict
array list
string str
number (int) int
number (real) float
true True
false False
null None

从技术上讲,这种转换并不是对序列表的完美逆转。这基本上意味着,如果您现在编码一个对象,然后在以后再次对其进行解码,则可能无法获得完全相同的对象。

实际上,这更像是让一个朋友将某物翻译成日语,而另一个朋友将其翻译成英语。无论如何,最简单的示例是,对一个tuple进行编码,然后在解码后则返回的是list ,如下所示:

>>> blackjack_hand = (8, "Q")
>>> encoded_hand = json.dumps(blackjack_hand)
>>> decoded_hand = json.loads(encoded_hand)

>>> blackjack_hand == decoded_hand
False
>>> type(blackjack_hand)
<class 'tuple'>
>>> type(decoded_hand)
<class 'list'>
>>> blackjack_hand == tuple(decoded_hand)
True

一个简单的反序列化示例

这次,想象一下您已经在磁盘上存储了一些要在内存中处理的数据。您仍将使用上下文管理器,但是这次您将以data_file.json读取模式打开现有的管理器:

with open("data_file.json", "r") as read_file:
    data = json.load(read_file)

此方法的结果可能会从转换表中返回任何允许的数据类型,在大多数情况下,根对象将是dictlist

如果您已经从另一个程序中提取了JSON数据,或者以其他方式在Python中获得了JSON格式的数据字符串,则可以轻松地使用loads()来反序列化它:

json_string = """
{
 "researcher": {
 "name": "Ford Prefect",
 "species": "Betelgeusian",
 "relatives": [
 {
 "name": "Zaphod Beeblebrox",
 "species": "Betelgeusian"
 }
 ]
 }
}
"""
data = json.loads(json_string)

实例

在这个例子中,您将使用 JSONPlaceholder服务,出于练习目的,它是伪造JSON数据的重要来源。

首先创建一个名为scratch.py或任何您想要的脚本文件。

您需要向JSONPlaceholder服务发出API请求,因此只需使用requests包即可完成繁重的工作。在文件顶部添加以下导入:

import json
import requests

JSONPlaceholder服务发起请求:

response = requests.get("https://jsonplaceholder.typicode.com/todos")
todos = json.loads(response.text)

现在让我们来检查一下todos的类型:

>>> todos == response.json()
True
>>> type(todos)
<class 'list'>
>>> todos[:10]
...

您可以通过在浏览器中访问(https://jsonplaceholder.typicode.com/todos) 来查看数据的结构,通过观察,您将看到TODO的示例如下:

{
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
}

有多个用户,每个用户都有一个唯一的userId,每个任务都有一个completed属性。

下面,我们通过编程来确定哪些用户完成了最多的任务吗:

# Map of userId to number of complete TODOs for that user
todos_by_user = {}

# Increment complete TODOs count for each user.
for todo in todos:
    if todo["completed"]:
        try:
            # Increment the existing user's count.
            todos_by_user[todo["userId"]] += 1
        except KeyError:
            # This user has not been seen. Set their count to 1.
            todos_by_user[todo["userId"]] = 1

# Create a sorted list of (userId, num_complete) pairs.
top_users = sorted(todos_by_user.items(), 
                   key=lambda x: x[1], reverse=True)

# Get the maximum number of complete TODOs.
max_complete = top_users[0][1]

# Create a list of all users who have completed
# the maximum number of TODOs.
users = []
for user, num_complete in top_users:
    if num_complete < max_complete:
        break
    users.append(str(user))

max_users = " and ".join(users)

编码和解码自定义Python对象

当我们尝试序列化Elf会发生什么?

class Elf:
    def __init__(self, level, ability_scores=None):
        self.level = level
        self.ability_scores = {
            "str": 11, "dex": 12, "con": 10,
            "int": 16, "wis": 14, "cha": 13
        } if ability_scores is None else ability_scores
        self.hp = 10 + self.ability_scores["con"]

结果是无法序列化:

>>> elf = Elf(level=4)
>>> json.dumps(elf)
TypeError: Object of type 'Elf' is not JSON serializable

尽管该json模块可以处理大多数内置的Python类型,但是对于自定义数据类型,则无法自动序列化。

简化数据结构

现在,问题是如何处理更复杂的数据结构。好吧,您可以尝试手动编码和解码JSON,但是有一个稍微聪明一点的解决方案可以为您节省一些工作。您可以直接执行一个中间步骤,而不是从自定义数据类型直接转换为JSON。

您需要做的就是用 json 内置类型表示数据。本质上,您将较复杂的对象转换为更简单的表示形式,然后 json 模块将其转换为JSON。就像数学中的传递性一样:如果A = B并且B = C,则A =C。

Python有一个内置类型复数(complex),它是用于表示复数的数据类型,并且默认情况下无法序列化:

>>> z = 3 + 8j
>>> type(z)
<class 'complex'>
>>> json.dumps(z)
TypeError: Object of type 'complex' is not JSON serializable

使用自定义类型时要问自己一个很好的问题是,重新创建此对象所需的最少信息量是多少?对于复数而言,您只需要知道实部和虚部:

>>> z.real
3.0
>>> z.imag
8.0

将相同的数字传递到一个复数(complex)构造函数中,使用__eq__比较运算符检查结果:

>>> complex(3, 8) == z
True

将自定义数据类型分解为基本组件对于序列化和反序列化过程都至关重要。

编码自定义类型

要将自定义对象转换为JSON,您所需要做的就是为该dump()方法的default参数提供编码功能。json模块将在本身不支持序列化的任何对象上调用这个函数。以下是解码函数:

def encode_complex(z):
    if isinstance(z, complex):
        return (z.real, z.imag)
    else:
        type_name = z.__class__.__name__
        raise TypeError(f"Object of type '{type_name}' is not JSON serializable")

请注意,如果您没有得到所期望的对象则会抛出TypeError错误。现在,您可以使用以下代码进行序列号复数:

>>> json.dumps(9 + 5j, default=encode_complex)
'[9.0, 5.0]'
>>> json.dumps(elf, default=encode_complex)
TypeError: Object of type 'Elf' is not JSON serializable

为什么我们将复数编码为 tuple? 好问题!那当然不是唯一的选择,也不一定是最好的选择。您可以根据自己的需求,任意切换,只不过tuple是更常用的选择。

另一种常见方法是继承JSONEncoder并覆盖其default()方法:

class ComplexEncoder(json.JSONEncoder):
    def default(self, z):
        if isinstance(z, complex):
            return (z.real, z.imag)
        else:
            return super().default(z)

您可以通过cls参数直接在dump()方法中使用ComplexEncoder,也可以通过创建编码器的实例并调用其encode()方法来使用:

>>> json.dumps(2 + 5j, cls=ComplexEncoder)
'[2.0, 5.0]'

>>> encoder = ComplexEncoder()
>>> encoder.encode(3 + 6j)
'[3.0, 6.0]'

解码自定义类型

尽管复数的实部和虚部绝对必要,但实际上它们不足以重新创建对象。当您尝试使用ComplexEncoder编码一个复数,然后解码时,会发生以下情况:

>>> complex_json = json.dumps(4 + 17j, cls=ComplexEncoder)
>>> json.loads(complex_json)
[4.0, 17.0]

您得到的只是一个列表,如果您再次想要该复数对象,则必须将值传递给构造函数。

那么,我们可以在complex_data.json添加复数标识,如下:

{
    "__complex__": true,
    "real": 42,
    "imag": 36
}

然后改写decode_complex()

def decode_complex(dct):
    if "__complex__" in dct:
        return complex(dct["real"], dct["imag"])
    return dct

如果"__complex__"不在字典中,则可以返回该对象,并让默认解码器处理它。

现在,您可以从完成对complex_data.json的数据解析:

>>> with open("complex_data.json") as complex_data:
...     data = complex_data.read()
...     z = json.loads(data, object_hook=decode_complex)
... 
>>> type(z)
<class 'complex'>

尝试将以下复数列表放入complex_data.json并再次运行脚本:

[
  {
    "__complex__":true,
    "real":42,
    "imag":36
  },
  {
    "__complex__":true,
    "real":64,
    "imag":11
  }
]

如果一切顺利,您将获得complex对象列表:

>>> with open("complex_data.json") as complex_data:
...     data = complex_data.read()
...     numbers = json.loads(data, object_hook=decode_complex)
... 
>>> numbers
[(42+36j), (64+11j)]

结论

恭喜,您现在可以在您的Python代码中加入JSON强大的功能。

我们来复习下JSON常规任务的工作流程:

  1. 导入 json
  2. 使用 load() 或者 loads()读取数据
  3. 处理数据
  4. 使用dump() 或者 dumps()写入更改后的数据
Card image cap
开发者雷

尘世间一个小小的开发者,每天增加一些无聊的知识,就不会无聊了

要加油~~~

技术文档 >> 系列应用 >>
热推应用
Let'sLearnSwift
学习Swift的入门教程
PyPie
Python is as good as Pie
标签