引言
在Python中处理JSON数据往往颇具挑战。基础的json.loads()函数仅能满足简单需求。
API响应、配置文件和数据导出文件中的JSON常常杂乱无章、结构混乱。你可能需要展平嵌套对象、安全提取值以避免键错误(KeyError)、合并多个JSON文件,或在JSON与其他格式间转换。这些任务在网页爬虫、API集成和数据处理中频繁出现。本文将带你逐一掌握5个实用函数,应对JSON解析与处理的常见场景。
这些函数的完整代码可在GitHub上获取。
1. 安全提取嵌套值
JSON对象的嵌套层级往往较深,使用方括号语法访问深层嵌套值会迅速变得棘手——只要有一个键缺失,就会触发键错误(KeyError)。
以下函数支持通过点号分隔语法访问嵌套值,并为缺失键提供默认值兜底:
def get_nested_value(data, path, default=None):
"""
通过点号分隔语法安全提取JSON中的嵌套值。
参数:
data: 字典或JSON对象
path: 点号分隔的字符串,例如 "user.profile.email"
default: 路径不存在时返回的默认值
返回:
路径对应的 值,若路径不存在则返回默认值
"""
keys = path.split('.')
current = data
for key in keys:
if isinstance(current, dict):
current = current.get(key)
if current is None:
return default
elif isinstance(current, list):
try:
index = int(key)
current = current[index]
except (ValueError, IndexError):
return default
else:
return default
return current
我们用复杂的嵌套结构测试该函数:
# 示例JSON数据
user_data = {
"user": {
"id": 123,
"profile": {
"name": "Allie",
"email": "allie@example.com",
"settings": {
"theme": "dark",
"notifications": True
}
},
"posts": [
{"id": 1, "title": "First Post"},
{"id": 2, "title": "Second Post"}
]
}
}
# 提取值
email = get_nested_value(user_data, "user.profile.email")
theme = get_nested_value(user_data, "user.profile.settings.theme")
first_post = get_nested_value(user_data, "user.posts.0.title")
missing = get_nested_value(user_data, "user.profile.age", default=25)
print(f"邮箱:{email}")
print(f"主题:{theme}")
print(f"第一篇文章:{first_post}")
print(f"年龄(默认值):{missing}")
输出结果:
邮箱:allie@example.com
主题:dark
第一篇文章:First Post
年龄(默认值):25
该函数按点号拆分路径字符串,逐层遍历数据结构。在每一层,它会检查当前值是否为字典或列表:若为字典,使用.get(key)方法(缺失键时返回None而非触发错误);若为列表,尝试将键转换为整数索引。
默认参数为路径不存在时提供兜底,可避免代码在处理API返回的不完整或不一致JSON数据时崩溃。
此模式在处理API响应时尤为实用——这类场景中部分字段常为可选,或仅在特定条件下存在。
2. 将嵌套JSON展平为单层级字典
机器学习模型、CSV导出和数据库插入操作通常需要扁平数据结构,但API响应和配置文件多采用嵌套JSON。将嵌套对象转换为扁平键值对是常见需求。
以下函数可将嵌套JSON展平,并支持自定义分隔符:
def flatten_json(data, parent_key='', separator='_'):
"""
将嵌套JSON展平为单层级字典。
参数:
data: 嵌套字典或JSON对象
parent_key: 键的前缀(用于递归)
separator: 连接嵌套键的字符串
返回:
键名拼接后的扁平字典
"""
items = []
if isinstance(data, dict):
for key, value in data.items():
new_key = f"{parent_key}{separator}{key}" if parent_key else key
if isinstance(value, dict):
# 递归展平嵌套字典
items.extend(flatten_json(value, new_key, separator).items())
elif isinstance(value, list):
# 展平列表并添加索引键
for i, item in enumerate(value):
list_key = f"{new_key}{separator}{i}"
if isinstance(item, (dict, list)):
items.extend(flatten_json(item, list_key, separator).items())
else:
items.append((list_key, item))
else:
items.append((new_key, value))
else:
items.append((parent_key, data))
return dict(items)
我们用复杂嵌套结构测试展平功能:
# 复杂嵌套JSON
product_data = {
"product": {
"id": 456,
"name": "笔记本电脑",
"specs": {
"cpu": "英特尔i7",
"ram": "16GB",
"storage": {
"type": "固态硬盘(SSD)",
"capacity": "512GB"
}
},
"reviews": [
{"rating": 5, "comment": "表现极佳"},
{"rating": 4, "comment": "性价比高"}
]
}
}
flattened = flatten_json(product_data)
for key, value in flattened.items():
print(f"{key}: {value}")
输出结果:
product_id: 456
product_name: 笔记本电脑
product_specs_cpu: 英特尔i7
product_specs_ram: 16GB
product_specs_storage_type: 固态硬盘(SSD)
product_specs_storage_capacity: 512GB
product_reviews_0_rating: 5
product_reviews_0_comment: 表现极佳
product_reviews_1_rating: 4
product_reviews_1_comment: 性价比高
该函数通过递归处理任意深度的嵌套。遇到字典时,遍历每个键值对,将父键与当前键用分隔符拼接生成新键;遇到列表时,将索引作为键的一部分,保留数组元素的顺序和结构(例如reviews_0_rating表示第一篇评论的评分)。
分隔符参数支持自定义输出格式:根据需求可使用点号(对应点号语法)、下划线(对应蛇形命名法)或斜杠(对应路径式键名)。
此函数在将JSON API响应转换为数据框(Datafr ame)或CSV行时特别实用——这类场景要求每列拥有唯一名称。
3. 深度合并多个JSON对象
配置管理常需合并多个JSON文件,包括默认设置、环境专属配置、用户偏好等。简单的dict.update()仅能处理顶层键值,需通过深度合并递归组合嵌套结构。
以下函数实现JSON对象的深度合并:
def deep_merge_json(base, override):
"""
深度合并两个JSON对象,覆盖字典(override)优先级更高。
参数:
base: 基础字典
override: 用于覆盖/新增值的字典
返回:
合并后的新字典
"""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
# 递归合并嵌套字典
result[key] = deep_merge_json(result[key], value)
else:
# 覆盖或新增值
result[key] = value
return result
我们测试配置文件的合并场景:
import json
# 默认配置
default_config = {
"database": {
"host": "localhost",
"port": 5432,
"timeout": 30,
"pool": {
"min": 2,
"max": 10
}
},
"cache": {
"enabled": True,
"ttl": 300
},
"logging": {
"level": "INFO"
}
}
# 生产环境覆盖配置
prod_config = {
"database": {
"host": "prod-db.example.com",
"pool": {
"min": 5,
"max": 50
}
},
"cache": {
"ttl": 600
},
"monitoring": {
"enabled": True
}
}
merged = deep_merge_json(default_config, prod_config)
print(json.dumps(merged, indent=2, ensure_ascii=False))
输出结果:
{
"database": {
"host": "prod-db.example.com",
"port": 5432,
"timeout": 30,
"pool": {
"min": 5,
"max": 50
}
},
"cache": {
"enabled": true,
"ttl": 600
},
"logging": {
"level": "INFO"
},
"monitoring": {
"enabled": true
}
}
该函数递归合并嵌套字典:当基础字典和覆盖字典在同一键下均为字典时,会合并这两个子字典而非直接覆盖,从而保留未被显式覆盖的值。
从结果可见,database.port和database.timeout保留默认配置,而database.host被覆盖;嵌套的连接池(pool)配置完全合并,min和max均更新为生产环境值。同时,函数会新增基础字典中不存在的键(如生产环境配置中的monitoring模块)。
可链式调用多次合并以实现多层级配置叠加:
final_config = deep_merge_json(
deep_merge_json(default_config, prod_config),
user_preferences # 用户偏好配置
)
此模式在应用配置中极为常见,适用于默认配置、环境专属配置和运行时覆盖配置的多层级组合场景。
4. 按Schema或白名单过滤JSON
API返回的数据往往超出实际需求,庞大的JSON响应会降低代码可读性。有时你仅需特定字段,或在日志记录前移除敏感数据。
以下函数可按指定规则过滤JSON,仅保留所需字段:
def filter_json(data, schema):
"""
按Schema规则过滤JSON,仅保留指定字段。
参数:
data: 待过滤的字典或JSON对象
schema: 定义保留字段的字典
用True表示保留该字段,用嵌套字典表示嵌套过滤规则
返回:
仅包含指定字段的过滤后字典
"""
if not isinstance(data, dict) or not isinstance(schema, dict):
return data
result = {}
for key, value in schema.items():
if key not in data:
continue
if value is True:
# 保留该字段原值
result[key] = data[key]
elif isinstance(value, dict):
# 递归过滤嵌套对象
if isinstance(data[key], dict):
filtered_nested = filter_json(data[key], value)
if filtered_nested:
result[key] = filtered_nested
elif isinstance(data[key], list):
# 过滤列表中的每个元素
filtered_list = []
for item in data[key]:
if isinstance(item, dict):
filtered_item = filter_json(item, value)
if filtered_item:
filtered_list.append(filtered_item)
else:
filtered_list.append(item)
if filtered_list:
result[key] = filtered_list
return result
我们用示例API响应测试过滤功能:
import json
# 示例API响应
api_response = {
"user": {
"id": 789,
"username": "Cayla",
"email": "cayla@example.com",
"password_hash": "secret123", # 敏感字段
"profile": {
"name": "Cayla Smith",
"bio": "软件开发工程师",
"avatar_url": "https://example.com/avatar.jpg",
"private_notes": "内部备注" # 敏感字段
},
"posts": [
{
"id": 1,
"title": "Hello World",
"content": "我的第一篇文章",
"views": 100,
"internal_score": 0.85 # 内部字段
},
{
"id": 2,
"title": "Python技巧",
"content": "一些实用技巧",
"views": 250,
"internal_score": 0.92 # 内部字段
}
]
},
"me tadata": {
"request_id": "abc123",
"server": "web-01"
}
}
# 定义保留字段的Schema
public_schema = {
"user": {
"id": True,
"username": True,
"profile": {
"name": True,
"avatar_url": True
},
"posts": {
"id": True,
"title": True,
"views": True
}
}
}
filtered = filter_json(api_response, public_schema)
print(json.dumps(filtered, indent=2, ensure_ascii=False))
输出结果:
{
"user": {
"id": 789,
"username": "Cayla",
"profile": {
"name": "Cayla Smith",
"avatar_url": "https://example.com/avatar.jpg"
},
"posts": [
{
"id": 1,
"title": "Hello World",
"views": 100
},
{
"id": 2,
"title": "Python技巧",
"views": 250
}
]
}
}
Schema本质是字段白名单:将字段设为True表示保留该字段,使用嵌套字典可定义嵌套对象的过滤规则,函数会递归将规则应用于嵌套结构。
对于列表,Schema规则会应用于每个元素。示例中posts列表被过滤后,仅保留id、title和views字段,content和internal_score等内部字段被移除。
注意,password_hash和private_notes等敏感字段未出现在输出中。该函数适用于日志记录前的数据脱敏,或向前端应用传输数据时的字段筛选。
可根据不同场景创建多个Schema:例如列表页用的精简Schema、详情页用的完整Schema,以及管理员专用的全量Schema。
5. JSON与点号语法的双向转换
部分系统使用扁平键值存储,但代码中需操作嵌套JSON。在扁平点号语法键与嵌套结构间转换可解决这一矛盾。
以下两组函数实现双向转换:
将JSON转换为点号语法
def json_to_dot_notation(data, parent_key=''):
"""
将嵌套JSON转换为扁平点号语法字典。
参数:
data: 嵌套字典
parent_key: 键的前缀(用于递归)
返回:
点号语法键的扁平字典
"""
items = {}
if isinstance(data, dict):
for key, value in data.items():
new_key = f"{parent_key}.{key}" if parent_key else key
if isinstance(value, dict):
items.update(json_to_dot_notation(value, new_key))
else:
items[new_key] = value
else:
items[parent_key] = data
return items
将点号语法转换为JSON
def dot_notation_to_json(flat_data):
"""
将扁平点号语法字典转换为嵌套JSON。
参数:
flat_data: 点号语法键的扁平字典
返回:
嵌套字典
"""
result = {}
for key, value in flat_data.items():
parts = key.split('.')
current = result
for i, part in enumerate(parts[:-1]):
if part not in current:
current[part] = {}
current = current[part]
current[parts[-1]] = value
return result
我们测试完整的往返转换:
import json
# 原始嵌套JSON
config = {
"app": {
"name": "MyApp",
"version": "1.0.0"
},
"database": {
"host": "localhost",
"credentials": {
"username": "admin",
"password": "secret"
}
},
"features": {
"analytics": True,
"notifications": False
}
}
# 转换为点号语法(适用于环境变量等场景)
flat = json_to_dot_notation(config)
print("扁平格式:")
for key, value in flat.items():
print(f" {key} = {value}")
print("\n" + "="*50 + "\n")
# 转换回嵌套JSON
nested = dot_notation_to_json(flat)
print("嵌套格式:")
print(json.dumps(nested, indent=2, ensure_ascii=False))
输出结果:
扁平格式:
app.name = MyApp
app.version = 1.0.0
database.host = localhost
database.credentials.username = admin
database.credentials.password = secret
features.analytics = True
features.notifications = False
==================================================
嵌套格式:
{
"app": {
"name": "MyApp",
"version": "1.0.0"
},
"database": {
"host": "localhost",
"credentials": {
"username": "admin",
"password": "secret"
}
},
"features": {
"analytics": true,
"notifications": false
}
}
json_to_dot_notation函数通过递归遍历嵌套字典,用点号连接键名生成扁平结构。与前文展平函数不同,该函数不处理列表,专为纯键值结构的配置数据优化。
dot_notation_to_json函数则反向操作:按点号拆分键名,通过循环创建中间嵌套层级(处理除最后一段外的所有键片段),最终将值赋给最内层键。
这种双向转换可在扁平键值存储的约束下,保持配置数据的可读性和可维护性。
总结
JSON处理远不止基础的json.loads()函数。多数项目中,你需要工具来导航嵌套结构、转换数据形态、合并配置、过滤字段及实现格式转换。
本文中的技术同样适用于其他数据处理场景,可修改这些模式以适配xm l、YAML或自定义数据格式。
建议先集成安全提取函数以避免代码中的键错误,再根据实际需求添加其他函数。祝你编码愉快!
推荐学习书籍 《CDA一级教材》适合CDA一级考生备考,也适合业务及数据分析岗位的从业者提升自我。完整电子版已上线CDA网校,累计已有10万+在读~ !



雷达卡





京公网安备 11010802022788号







