3. 日志解析:Python读取CSV日志、解析JSON日志、处理二进制日志

飞行器落地后,第一件事是什么?不是看姿态曲线,不是算轨迹偏差。我个人习惯,先看一眼原始日志能不能正常解析。日志打不开,后面全是白搭。

这一章,咱们就聊聊三种最常见的日志格式——CSV、JSON、二进制。每种格式我都踩过坑,也总结了一些实用技巧。你想想看,飞控日志动不动就几百兆,解析效率差一点,调试节奏就全乱了。

3.1 CSV日志:最朴素的表格数据

CSV格式在飞控日志里太常见了。传感器校准数据、电机输出值、GPS时间戳……很多飞控系统默认就输出CSV。说白了,就是逗号分隔的文本表格。

Python自带的csv模块就能搞定。但我建议你用pandas,尤其是数据量大的时候。为什么?因为pandas底层用C语言实现,读取速度能快一个数量级。

核心要点:CSV日志解析的关键在于列名对齐和时间戳处理。飞控日志的CSV文件经常出现列数不一致的情况——某个传感器临时掉线,那一行就少了几列。

来看一段实际代码。这是我处理PX4飞控CSV日志的惯用写法:

import pandas as pd
import numpy as np

def parse_flight_csv(filepath):
    """
    读取飞控CSV日志,自动处理列数不一致的问题
    """
    # 先读前几行,看看列名
    with open(filepath, 'r') as f:
        header = f.readline().strip().split(',')
    
    # 用pandas读取,跳过坏行
    df = pd.read_csv(
        filepath, 
        skipinitialspace=True,
        on_bad_lines='skip',  # 跳过格式错误的行
        low_memory=False       # 避免类型推断警告
    )
    
    # 时间戳转成datetime
    if 'timestamp' in df.columns:
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='us')
    
    return df

小技巧:飞控日志的时间戳通常是微秒级整数。用pd.to_datetime(..., unit='us')直接转成可读时间,后续画图、对齐数据都方便。

我在项目中遇到过一个问题:某型无人机在高温环境下飞行,CSV日志里偶尔会出现乱码字符。原因是SD卡写入时电压不稳,导致个别字节出错。解决方案就是加一个字符过滤层:

def clean_csv_line(line):
    """过滤掉非ASCII字符,避免解析崩溃"""
    return ''.join(c for c in line if ord(c) < 128)

3.2 JSON日志:结构化数据的王者

JSON格式在新型飞控系统里越来越流行。为什么?因为它自带层级结构。一个JSON对象可以同时包含姿态、位置、传感器状态,嵌套关系一目了然。

但JSON也有个毛病——冗余字符太多。同样的数据,JSON文件体积可能是CSV的3到5倍。所以飞行器上的JSON日志通常会被压缩,或者只记录关键事件。

Python解析JSON用json模块就够了。不过要注意,飞控日志里的JSON经常是一行一个JSON对象,而不是一个完整的JSON数组。这叫JSON Lines格式(.jsonl)。

import json

def parse_jsonl_log(filepath):
    """
    解析JSON Lines格式的飞控日志
    每行一个独立的JSON对象
    """
    events = []
    with open(filepath, 'r') as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                event = json.loads(line)
                events.append(event)
            except json.JSONDecodeError as e:
                print(f"第{line_num}行解析失败: {e}")
                # 我曾经遇到过:某次飞行中日志缓冲区溢出,
                # 导致最后一行被截断。这里跳过就好,别让一个错误毁了全部数据。
                continue
    return events

注意:JSON日志里经常出现NaN或Infinity,标准JSON不支持这些值。解析前最好做预处理,或者用json.loads()的parse_float参数自定义解析逻辑。

我个人习惯把JSON日志解析后转成pandas.DataFrame,方便后续分析。但JSON嵌套结构需要先展平:

def flatten_json_event(event, parent_key='', sep='_'):
    """
    展平嵌套JSON,把 {'attitude': {'roll': 0.1, 'pitch': 0.2}}
    变成 {'attitude_roll': 0.1, 'attitude_pitch': 0.2}
    """
    items = []
    for k, v in event.items():
        new_key = f"{parent_key}{sep}{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(flatten_json_event(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

3.3 二进制日志:效率与复杂度的博弈

二进制日志是飞行器上最常用的格式。为什么?因为体积小、写入快。一个浮点数在CSV里可能占10个字节,在二进制里只占4个字节。对于每秒记录上千条数据的飞控来说,这个差距是致命的。

但二进制日志的解析也最麻烦。没有固定的结构,全靠协议定义。我见过最头疼的情况:同一个飞控的不同固件版本,二进制日志的字段顺序居然不一样。

解析二进制日志的核心是理解数据结构定义。通常飞控厂商会提供结构体定义文档,或者直接给一个解析库。如果没有?那就得自己逆向。

Python的struct模块是解析二进制数据的利器。来看一个实际例子——解析MAVLink协议的二进制日志:

import struct

def parse_mavlink_binary(filepath):
    """
    解析MAVLink二进制日志
    MAVLink v2 消息头格式: 0xFD + 长度 + 不兼容标志 + 兼容标志 + 序列号 + 系统ID + 组件ID + 消息ID
    """
    HEADER_FORMAT = '<BBBBBBB'  # 小端序,7个字节
    HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
    
    messages = []
    with open(filepath, 'rb') as f:
        while True:
            # 找同步头 0xFD
            sync = f.read(1)
            if not sync:
                break
            if sync[0] != 0xFD:
                continue
            
            # 读消息头
            header_data = f.read(HEADER_SIZE - 1)  # 已经读了1个同步字节
            if len(header_data) < HEADER_SIZE - 1:
                break
            
            # 解析消息头
            payload_len, incompat, compat, seq, sys_id, comp_id, msg_id = \
                struct.unpack(HEADER_FORMAT, sync + header_data)
            
            # 读负载
            payload = f.read(payload_len)
            if len(payload) < payload_len:
                break
            
            # 读校验和(2字节)
            checksum = f.read(2)
            
            messages.append({
                'msg_id': msg_id,
                'sys_id': sys_id,
                'comp_id': comp_id,
                'seq': seq,
                'payload': payload
            })
    
    return messages

避坑指南:我曾经在解析某型飞控的二进制日志时,发现数据总是对不上。折腾了两天,最后发现是字节序问题——飞控用的是大端序,而我默认用了小端序。从此以后,我每次解析二进制日志,第一件事就是确认字节序。

二进制日志的另一个常见问题是对齐填充。C语言结构体为了内存对齐,会在字段之间插入填充字节。比如一个uint8_t后面跟一个uint32_t,中间会有3个填充字节。解析时如果不考虑这个,数据全错。

# 带填充的结构体示例
# C定义: struct { uint8_t flag; uint32_t value; }
# 实际内存布局: flag(1字节) + 填充(3字节) + value(4字节)
# Python解析:
flag, value = struct.unpack('<BxxxI', data)  # 'x'表示跳过1个字节

3.4 三种格式对比与选型建议

说了这么多,到底该用哪种格式?我整理了一张对比表,方便你快速决策:

特性 CSV JSON 二进制
可读性 高(文本,可直接打开) 中(文本,但嵌套结构) 低(需要工具解析)
存储效率 低(冗余逗号和换行) 低(冗余花括号和引号) 高(紧凑二进制)
解析速度 中(文本解析有开销) 中(JSON解析有开销) 高(直接内存映射)
扩展性 差(增删列需改解析器) 好(新增字段不影响旧解析) 差(结构体定义必须一致)
适用场景 地面测试、传感器校准 事件日志、配置参数 高速实时数据流

我的建议是:地面调试用CSV,事件记录用JSON,实时飞行用二进制。三种格式各有优劣,没有银弹。

3.5 本章知识体系

下面这张图总结了本章的核心逻辑。三种格式的解析路径、关键工具、常见陷阱一目了然:

日志解析知识体系 日志解析知识体系 飞行器日志解析 CSV日志 JSON日志 二进制日志 工具:pandas.read_csv() 陷阱:列数不一致、乱码 工具:json.loads() + 展平 陷阱:NaN/Infinity、嵌套 工具:struct.unpack() 陷阱:字节序、对齐填充 选型建议 地面调试→CSV | 事件记录→JSON | 实时飞行→二进制

嗯,到这里三种日志格式的解析方法就讲完了。CSV重在对齐,JSON重在展平,二进制重在协议理解。每种格式我都给出了实际可用的代码,你直接拿去用就行。