在Python中使用JSON
1周前 • 54次点击 • 来自 其他
收录专题: Python边学边译
标签: Python
原文链接: Working With JSON Data in Python
JSON 已迅速成为信息交换的事实上的标准。也许您是通过API收集信息或将数据存储在文档数据库中,不管是在何种场景下使用,JSON都是您在Python中的最佳选择。
我们只使用JSON来存储和交换数据吗? 是又不是,它不过是社区用来传递数据的标准格式。请记住,JSON不是可用于此类工作的唯一格式,XML和YAML可能是唯一值得一提的其他格式。
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)
此方法的结果可能会从转换表中返回任何允许的数据类型,在大多数情况下,根对象将是dict
或list
。
如果您已经从另一个程序中提取了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常规任务的工作流程:
- 导入
json
包 - 使用
load()
或者loads()
读取数据 - 处理数据
- 使用
dump()
或者dumps()
写入更改后的数据