从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格式处理

实际数据中常遇到以下问题:

  1. 单引号代替双引号

    # 预处理方案
    line = line.replace("'", '"')
    
  2. 尾部逗号问题

    if line.strip().endswith(','):
        line = line[:-1]
    
  3. 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)

更多推荐