从ChatGPT日志到数据集:用Python把JSONL文件清洗成标准JSON的保姆级教程
·
从ChatGPT日志到数据集:用Python把JSONL文件清洗成标准JSON的保姆级教程
JSONL(JSON Lines)格式因其逐行存储的特性,成为AI日志、API响应和爬虫数据的常见载体。但当我们需要将这些数据用于模型训练、可视化分析或系统集成时,标准JSON格式往往更为合适。本文将手把手教你用Python实现JSONL到JSON的转换,涵盖异常处理、性能优化和实际应用场景。
1. 理解JSONL与JSON的核心差异
JSONL文件本质上是多个JSON对象的串联,每行一个独立对象。这种格式特别适合流式数据处理,比如:
{"id": 1, "text": "第一条数据"}
{"id": 2, "text": "第二条数据"}
而标准JSON则要求所有数据封装在统一结构中,常见两种形式:
-
对象集合 (格式1):
{ "1": "第一条数据", "2": "第二条数据" } -
数组集合 (格式2):
[ {"id": 1, "text": "第一条数据"}, {"id": 2, "text": "第二条数据"} ]
提示:选择格式时需考虑下游应用。对象集合适合键值查询,数组集合更适合顺序处理。
2. 基础转换:从简单JSONL到标准JSON
2.1 转换为JSON对象集合
以下代码实现最基本的转换逻辑:
import json
def jsonl_to_dict(jsonl_path, json_path):
result = {}
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
try:
data = json.loads(line)
result.update(data)
except json.JSONDecodeError as e:
print(f"解析失败的行:{line.strip()},错误:{e}")
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
关键点说明:
ensure_ascii=False保留非ASCII字符(如中文)indent=2使输出JSON具有可读性格式- 异常捕获避免单行错误导致整个处理中断
2.2 转换为JSON数组
对于需要保留原始行顺序的场景:
def jsonl_to_array(jsonl_path, json_path):
result = []
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
try:
data = json.loads(line)
result.append(data)
except json.JSONDecodeError:
continue # 静默跳过错误行
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2)
3. 处理复杂场景与脏数据
3.1 非标准JSONL格式处理
实际数据中常遇到以下问题:
-
单引号代替双引号 :
# 预处理方案 line = line.replace("'", '"') -
尾部逗号问题 :
if line.strip().endswith(','): line = line[:-1] -
BOM头问题 (常见于Windows生成文件):
if line.startswith('\ufeff'): line = line[1:]
3.2 结构化异常处理框架
建议采用分级错误处理策略:
ERROR_LOG = "conversion_errors.log"
def process_line(line):
try:
# 尝试标准解析
return json.loads(line)
except json.JSONDecodeError:
try:
# 尝试修复常见问题
fixed = line.replace("'", '"').strip()
if not (fixed.startswith('{') and fixed.endswith('}')):
fixed = '{' + fixed + '}'
return json.loads(fixed)
except:
# 记录无法修复的行
with open(ERROR_LOG, 'a') as f:
f.write(f"原始内容:{line}\n")
return None
4. 性能优化与大数据处理
4.1 内存友好型处理
对于GB级大文件,建议使用生成器逐行处理:
def stream_jsonl(jsonl_path):
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
yield process_line(line) # 使用前文的处理函数
# 使用示例
for item in stream_jsonl('large_file.jsonl'):
if item: # 过滤掉处理失败的行
process_item(item)
4.2 并行处理加速
利用多核CPU加速处理:
from multiprocessing import Pool
def parallel_convert(jsonl_path, json_path, workers=4):
with Pool(workers) as pool:
with open(jsonl_path, 'r') as f:
results = pool.imap(process_line, f)
valid_data = [r for r in results if r]
with open(json_path, 'w') as f:
json.dump(valid_data, f)
注意:并行处理时需确保每个行的处理是独立的,避免共享状态。
5. 实战应用场景
5.1 构建微调数据集
处理LLM输出日志时,常需要提取特定字段:
def create_finetune_dataset(input_path, output_path):
dataset = []
with open(input_path, 'r') as f:
for line in f:
try:
data = json.loads(line)
dataset.append({
"prompt": data['input'],
"completion": data['output']
})
except (KeyError, json.JSONDecodeError):
continue
with open(output_path, 'w') as f:
json.dump({"version": "1.0", "data": dataset}, f)
5.2 生成可视化数据
为Echarts等工具准备数据:
def prepare_echarts_data(jsonl_path):
categories = {}
with open(jsonl_path, 'r') as f:
for line in f:
data = json.loads(line)
cat = data.get('category', '其他')
categories[cat] = categories.get(cat, 0) + 1
return {
"xAxis": list(categories.keys()),
"series": [{
"name": "数量",
"type": "bar",
"data": list(categories.values())
}]
}
6. 高级技巧与最佳实践
6.1 数据校验模式
使用JSON Schema验证转换结果:
from jsonschema import validate
schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"text": {"type": "string"}
},
"required": ["id", "text"]
}
}
def validate_conversion(json_path):
with open(json_path) as f:
data = json.load(f)
validate(instance=data, schema=schema)
6.2 增量式处理
处理持续增长的日志文件:
class JSONLProcessor:
def __init__(self, state_file='state.json'):
self.state_file = state_file
self.last_position = self._load_state()
def _load_state(self):
try:
with open(self.state_file) as f:
return json.load(f).get('position', 0)
except FileNotFoundError:
return 0
def process_new_lines(self, jsonl_path):
with open(jsonl_path, 'r') as f:
f.seek(self.last_position)
for line in f:
yield process_line(line)
self.last_position = f.tell()
self._save_state()
def _save_state(self):
with open(self.state_file, 'w') as f:
json.dump({"position": self.last_position}, f)
在实际项目中,我发现最常遇到的问题是不规范的换行符导致解析失败。特别是在Windows和Linux系统间传输文件时,建议统一转换为 \n 格式:
import re
def normalize_lines(content):
return re.sub(r'\r\n?', '\n', content)
更多推荐

所有评论(0)