一、JSON 是什么
做爬虫这几年,JSON 是我打交道最多的数据格式。现在的网站,尤其是前后端分离的项目,数据接口几乎清一色返回 JSON。学会高效处理 JSON,是爬虫开发的基本功。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于 JavaScript 的对象字面量语法,但独立于语言,几乎所有编程语言都支持。
JSON 的特点
- 纯文本格式,人类可读
- 结构清晰,键值对形式
- 支持嵌套,能表示复杂数据结构
- 解析速度快,占用空间小
二、解析 JSON 数据
2.1 基础解析
import json
# JSON 字符串
json_str = '{"name": "张三", "age": 25, "city": "北京"}'
# 解析为 Python 字典
data = json.loads(json_str)
print(data['name']) # 张三
print(data['age']) # 25
2.2 从文件读取
# 从文件读取 JSON
with open('data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
print(data)
2.3 处理 API 返回的 JSON
import requests
response = requests.get('https://api.example.com/data')
# 直接解析响应内容为 JSON
data = response.json()
print(data)
2.4 安全解析
实际项目中,API 返回的数据不一定都是合法的 JSON。做好异常处理很重要。
import json
def safe_json_loads(json_str, default=None):
"""安全解析 JSON"""
try:
return json.loads(json_str)
except json.JSONDecodeError as e:
print(f"JSON 解析失败: {e}")
return default
except Exception as e:
print(f"其他错误: {e}")
return default
# 使用
result = safe_json_loads('{"name": "张三"}')
print(result) # {'name': '张三'}
result = safe_json_loads('不是 JSON')
print(result) # None
三、生成 JSON 数据
3.1 基础生成
import json
# Python 字典
data = {
'name': '张三',
'age': 25,
'city': '北京',
'hobbies': ['读书', '编程', '旅游']
}
# 转为 JSON 字符串
json_str = json.dumps(data)
print(json_str)
# {"name": "\u5f20\u4e09", "age": 25, "city": "\u5317\u4eac", "hobbies": ["\u8bfb\u4e66", "\u7f16\u7a0b", "\u65c5\u6e38"]}
3.2 格式化输出
# 格式化输出,带缩进
json_str = json.dumps(data, indent=2, ensure_ascii=False)
print(json_str)
# 输出:
# {
# "name": "张三",
# "age": 25,
# "city": "北京",
# "hobbies": [
# "读书",
# "编程",
# "旅游"
# ]
# }
3.3 保存到文件
# 保存到文件
with open('output.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
参数说明:
ensure_ascii=False 让中文字符正常显示,而不是转义成 Unicode 编码。indent=2 让输出有缩进,更易读。
四、处理复杂结构
4.1 嵌套数据访问
data = {
'user': {
'name': '张三',
'profile': {
'age': 25,
'address': {
'city': '北京',
'district': '朝阳区'
}
}
}
}
# 访问嵌套数据
city = data['user']['profile']['address']['city']
print(city) # 北京
4.2 安全访问嵌套数据
直接访问嵌套数据,如果某个层级不存在会报错。用 get 方法更安全。
def safe_get(data, *keys, default=None):
"""安全获取嵌套字典的值"""
for key in keys:
if isinstance(data, dict):
data = data.get(key, default)
if data is None:
return default
else:
return default
return data
# 使用
city = safe_get(data, 'user', 'profile', 'address', 'city')
print(city) # 北京
# 不存在的路径
province = safe_get(data, 'user', 'profile', 'address', 'province', default='未知')
print(province) # 未知
4.3 处理列表数据
data = {
'users': [
{'name': '张三', 'age': 25},
{'name': '李四', 'age': 30},
{'name': '王五', 'age': 28}
]
}
# 遍历列表
for user in data['users']:
print(f"{user['name']}: {user['age']}岁")
# 列表推导式
names = [user['name'] for user in data['users']]
print(names) # ['张三', '李四', '王五']
# 过滤
adults = [user for user in data['users'] if user['age'] >= 28]
print(adults) # [{'name': '李四', 'age': 30}, {'name': '王五', 'age': 28}]
五、数据验证
5.1 检查必需的字段
def validate_user_data(data):
"""验证用户数据"""
required_fields = ['name', 'age', 'email']
for field in required_fields:
if field not in data:
raise ValueError(f"缺少必需字段: {field}")
if not isinstance(data['age'], int):
raise ValueError("age 必须是整数")
if data['age'] < 0 or data['age'] > 150:
raise ValueError("age 范围无效")
return True
# 使用
try:
validate_user_data({'name': '张三', 'age': 25, 'email': 'zhangsan@example.com'})
print("验证通过")
except ValueError as e:
print(f"验证失败: {e}")
5.2 使用 JSON Schema
对于复杂的数据验证,可以使用 JSON Schema。
from jsonschema import validate, ValidationError
# 定义 Schema
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "format": "email"},
"hobbies": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["name", "age"]
}
# 验证
try:
validate(instance=data, schema=schema)
print("验证通过")
except ValidationError as e:
print(f"验证失败: {e.message}")
六、数据转换
6.1 JSON 转 CSV
import csv
import json
# JSON 数据
data = [
{'name': '张三', 'age': 25, 'city': '北京'},
{'name': '李四', 'age': 30, 'city': '上海'},
{'name': '王五', 'age': 28, 'city': '广州'}
]
# 转为 CSV
with open('output.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['name', 'age', 'city'])
writer.writeheader()
writer.writerows(data)
6.2 JSON 转 Excel
import pandas as pd
# JSON 数据转为 DataFrame
df = pd.DataFrame(data)
# 保存为 Excel
df.to_excel('output.xlsx', index=False)
6.3 扁平化嵌套 JSON
def flatten_json(data, parent_key='', sep='.'):
"""扁平化嵌套 JSON"""
items = []
for k, v in data.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(flatten_json(v, new_key, sep=sep).items())
elif isinstance(v, list):
for i, item in enumerate(v):
if isinstance(item, dict):
items.extend(flatten_json(item, f"{new_key}[{i}]", sep=sep).items())
else:
items.append((f"{new_key}[{i}]", item))
else:
items.append((new_key, v))
return dict(items)
# 使用
nested = {'user': {'name': '张三', 'age': 25}}
flat = flatten_json(nested)
print(flat) # {'user.name': '张三', 'user.age': 25}
七、常见问题
7.1 单引号问题
JSON 标准规定必须用双引号,但有时候会遇到单引号的"伪 JSON"。
# 错误的 JSON(单引号)
bad_json = "{'name': '张三', 'age': 25}"
# 解决方法1:替换单引号
good_json = bad_json.replace("'", '"')
data = json.loads(good_json)
# 解决方法2:使用 ast 模块(不推荐,有安全风险)
import ast
data = ast.literal_eval(bad_json)
7.2 中文乱码
# 确保文件编码正确
with open('data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# 输出时指定 ensure_ascii=False
json_str = json.dumps(data, ensure_ascii=False)
7.3 处理日期
JSON 没有日期类型,通常用字符串表示。
from datetime import datetime
# 自定义 JSON 编码器
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime('%Y-%m-%d %H:%M:%S')
return super().default(obj)
# 使用
data = {'name': '张三', 'created_at': datetime.now()}
json_str = json.dumps(data, cls=DateTimeEncoder)
print(json_str)
# {"name": "\u5f20\u4e09", "created_at": "2024-01-15 10:30:00"}
八、性能优化
8.1 使用 orjson
orjson 是一个高性能的 JSON 库,比标准库快很多。
# 安装:pip install orjson
import orjson
# 解析
data = orjson.loads(json_str)
# 生成
json_bytes = orjson.dumps(data)
json_str = json_bytes.decode('utf-8')
# 带选项的生成
json_bytes = orjson.dumps(data, option=orjson.OPT_INDENT_2)
8.2 流式处理大文件
import ijson
# 流式解析大 JSON 文件
with open('large_data.json', 'rb') as f:
# 逐个解析 items 数组中的对象
for item in ijson.items(f, 'items.item'):
print(item)
# 处理每个 item,内存占用很小
九、总结
JSON 处理看似简单,但实际项目中有很多细节需要注意。记住这几个核心要点:
| 场景 | 推荐做法 | 注意事项 |
|---|---|---|
| 解析 JSON | json.loads() |
做好异常处理 |
| 生成 JSON | json.dumps() |
设置 ensure_ascii=False |
| 嵌套数据 | 封装安全访问函数 | 避免 KeyError |
| 数据验证 | JSON Schema | 定义完整的 Schema |
| 性能优化 | orjson | 大量数据时使用 |
JSON 是爬虫开发中最常处理的数据格式。熟练掌握这些技巧,能让你在数据处理环节节省大量时间。