TATECHATLAS
◎ 简体中文
编程

Python:从字符串和文件读取 JSON 并处理解码错误

了解如何使用内置的 `json` 模块在 Python 中从字符串文字和文件解析 JSON 数据,以及如何有效处理潜在的解码错误。

本文内容

Python 的 `json` 模块提供了 `json.loads()` 用于从字符串解析 JSON,以及 `json.load()` 用于从类文件对象解析。如果输入不是有效的 JSON,这两个函数都可能引发 `json.JSONDecodeError`,这可以通过 `try-except` 块来捕获,从而优雅地处理格式错误的 JSON 数据。

从字符串读取 JSON

json.loads()` 函数用于将 JSON 字符串反序列化为 Python 对象。它接受一个字符串作为输入,并返回相应的 Python 数据结构(例如,字典、列表、字符串、数字、布尔值、None)。当你需要在 Python 代码中嵌入 JSON 数据或从 API 响应中接收 JSON 字符串时,这个功能非常有用。

例如,要解析表示为字符串的简单 JSON 对象,你可以将该字符串传递给 `json.loads()`。

import json

json_string = '{"name": "Alice", "age": 30, "isStudent": false}'

try:
    data = json.loads(json_string)
    print(data)
    print(f"Name: {data['name']}, Age: {data['age']}")
except json.JSONDecodeError as e:
    print(f"Error decoding JSON string: {e}")

从文件读取 JSON

当你的 JSON 数据存储在文件中时,你应该使用 `json.load()` 函数。此函数接受一个类文件对象(以文本模式打开)作为输入,并从中读取 JSON 数据,将其反序列化为 Python 对象。正确打开文件至关重要,要确保它是以文本模式读取的。

`with open(...)` 语句是处理文件的推荐方式,因为它能确保即使发生错误,文件也能被正确关闭。然后 `json.load()` 函数会处理文件的内容。

import json

# 假设 'data.json' 包含:[{"id": 1, "value": "A"}, {"id": 2, "value": "B"}]
file_path = 'data.json'

try:
    with open(file_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
        print(data)
except FileNotFoundError:
    print(f"Error: File not found at {file_path}")
except json.JSONDecodeError as e:
    print(f"Error decoding JSON from file: {e}")

处理 JSON 解码错误

JSON 数据必须遵循严格的格式。如果输入字符串或文件内容不是有效的 JSON,Python 的 `json` 模块将引发 `json.JSONDecodeError`。此异常提供了有关解析失败位置的详细信息,例如位置和错误的性质。

为了优雅地处理这些错误,你应该将 `json.loads()` 或 `json.load()` 调用包装在 `try...except json.JSONDecodeError` 块中。这允许你的程序继续运行,或者向用户提供信息性消息,而不是崩溃。

import json

malformed_json_string = '{"key": "value", "another_key": }' # 缺少值

try:
    json.loads(malformed_json_string)
except json.JSONDecodeError as e:
    print(f"Caught a JSONDecodeError: {e}")
    print(f"Error occurred at line {e.lineno}, column {e.colno}")
    print(f"Error message: {e.msg}")

理解 JSONDecodeError 详细信息

json.JSONDecodeError` 异常对象包含有用的属性,可以帮助精确定位 JSON 数据中的确切问题。这些属性包括 `msg`(错误消息)、`doc`(正在解析的输入字符串)、`pos`(发生错误的文档中的字符索引)、`lineno`(行号)和 `colno`(列号)。

通过检查这些属性,你可以提供关于格式错误的 JSON 的更具体反馈,有助于调试或告知用户数据质量问题。

import json

json_string_with_error = '''
{
    "name": "Bob",
    "details": {
        "city": "New York",
        "zip": 10001 # 缺少逗号
        "country": "USA"
    }
}
'''

try:
    json.loads(json_string_with_error)
except json.JSONDecodeError as e:
    print(f"Error: {e.msg}")
    print(f"Document: {e.doc[max(0, e.pos-10):e.pos+10]}...") # 显示上下文
    print(f"Position: {e.pos}, Line: {e.lineno}, Column: {e.colno}")

自定义 JSON 解码

json.load()` 和 `json.loads()` 函数提供了 `object_hook`、`parse_float`、`parse_int` 和 `parse_constant` 等参数,用于自定义 JSON 数据的解码方式。例如,`parse_float` 可用于将浮点数反序列化为 `Decimal` 对象而不是标准浮点数,这对于需要精确十进制算术的金融应用程序非常有用。

同样,`object_hook` 可以将解码的 JSON 对象(Python 字典)转换为自定义类实例,从而实现更面向对象的**数据处理**。

import json
import decimal

json_string_with_floats = '{"price": "19.99", "tax": "1.50"}'

# 使用 Decimal 进行精确的浮点数解析
data = json.loads(json_string_with_floats, parse_float=decimal.Decimal)

print(f"Parsed data: {data}")
print(f"Type of price: {type(data['price'])}")

# 使用 object_hook 创建自定义对象的示例
def dict_to_person(d):
    if '__class__' in d and d['__class__'] == 'Person':
        return Person(d['name'], d['age'])
    return d

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

json_person = '{"__class__": "Person", "name": "Charlie", "age": 25}'
person_obj = json.loads(json_person, object_hook=dict_to_person)
print(f"Decoded object: {person_obj}")

将 Python 对象编码为 JSON

虽然重点是读取 JSON,但值得注意的是其逆向操作:分别使用 `json.dumps()` 和 `json.dump()` 将 Python 对象编码为 JSON 字符串或文件。这些函数负责将 Python 数据类型转换为相应的 JSON 等价物。

它们还支持自定义,例如用于处理不可序列化类型的 `default`、用于美化打印的 `indent` 以及用于排序输出的 `sort_keys`,这对于调试和可读性非常有帮助。

import json

python_data = {
    "name": "David",
    "scores": [95, 88, 76],
    "isActive": True,
    "metadata": None
}

# 使用美化打印编码为 JSON 字符串
json_output_string = json.dumps(python_data, indent=4, sort_keys=True)
print("--- JSON String Output ---")
print(json_output_string)

# 编码到文件(使用 StringIO 进行演示的示例)
from io import StringIO
output_file = StringIO()
json.dump(python_data, output_file, indent=4)
output_file.seek(0) # 回绕以读取内容
print("\n--- JSON File Output (simulated) ---")
print(output_file.read())

安全注意事项

解析来自不可信来源的 JSON 数据需要谨慎。恶意构造的 JSON 可能被设计为消耗过多的 CPU 和内存资源,从而可能导致拒绝服务攻击。`json` 模块包含 `check_circular`(默认启用)等检查,以防止循环引用的无限递归。

建议在处理外部或不可信输入时限制要解析的数据的大小。对于非常大的 JSON 文件,如果可能,请考虑使用流式解析器或分块处理数据,尽管 Python 的标准 `json` 模块主要处理整个文档。

import json

# 潜在资源密集型 JSON 结构的示例(简化)
# 一个真正恶意的字符串会复杂得多。
# json 模块有一些内置保护,但大输入仍然存在风险。

# 循环引用的示例(如果 check_circular 为 True,将引发 RecursionError)
# data = {}
# data['self'] = data
# try:
#     json.dumps(data, check_circular=True)
# except RecursionError as e:
#     print(f"Caught expected error for circular reference: {e}")

# 限制输入大小是实用的安全措施
max_size = 1024 * 1024 # 1 MB

json_input = '{"key": "value"}' # 替换为实际输入源

if len(json_input.encode('utf-8')) > max_size:
    print("Error: Input JSON data exceeds maximum allowed size.")
else:
    try:
        data = json.loads(json_input)
        print("JSON parsed successfully within size limits.")
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON: {e}")

使用 `pathlib` 处理文件路径

在处理 JSON 文件时,通常需要管理文件路径。Python 的 `pathlib` 模块提供了面向对象的文件系统路径方法,比使用 `os.path` 进行字符串操作更简洁、更健壮。

你可以创建 `Path` 对象并直接将它们与 `open()` 和 `json.load()` 一起使用。这可以无缝地集成文件处理和 JSON 解析。`pathlib` 可跨不同操作系统正确处理路径分隔符。

import json
from pathlib import Path

# 使用 pathlib 定义 JSON 文件路径
file_path = Path('config.json')

# 确保文件存在并包含有效的 JSON,例如:
# {"database": "localhost", "port": 5432}

try:
    # 直接将 Path 对象与 open() 一起使用
    with file_path.open('r', encoding='utf-8') as f:
        config_data = json.load(f)
        print(f"Configuration loaded: {config_data}")
        print(f"Database host: {config_data.get('database')}")
except FileNotFoundError:
    print(f"Error: Configuration file not found at {file_path}")
except json.JSONDecodeError as e:
    print(f"Error decoding JSON from {file_path}: {e}")
except Exception as e:
    print(f"An unexpected error occurred: {e}")

检查清单

  • 验证 JSON 字符串或文件内容是否根据 JSON 规范正确格式化(例如,键和字符串使用双引号,正确使用逗号、花括号和方括号)。
  • 确保与 `json.load()` 一起使用的文件路径正确,并且文件具有适当的读取权限。
  • 在 `json.loads()` 和 `json.load()` 周围实现 `try-except` 块,以捕获 `json.JSONDecodeError` 并优雅地处理格式错误的 JSON。
  • 在打开文件读取 JSON 时,考虑使用 `encoding='utf-8'` 来避免潜在的编码问题。
  • 对于不可信的数据,实施输入 JSON 的大小限制以减轻拒绝服务风险。

该 `json` 模块将 JSON 解码为标准的 Python 类型。对于复杂的自定义类型或特定的数值精度(如金融计算),你可能需要使用 `object_hook` 或 `parse_float` 配合自定义函数(例如 `decimal.Decimal`)。该模块将整个 JSON 文档解析到内存中,这对于极大的文件可能会有问题;在这种情况下,可能需要使用替代的流式 JSON 解析器。关于不可信来源的安全警告很重要;在可能的情况下,请始终验证或清理输入。

参考来源

  1. Python: json ↗
  2. Python: pathlib ↗
返回顶部 ↑