
简介一套基于QT5.15.2构建的QTCOMTRADE源码项目专门用来解析电力系统COMTRADE标准格式的二进制DAT暂态数据文件适用于电力系统故障分析、保护设备调试以及相关软件开发者参考学习。zip压缩包仅337KB共包含16个文件其中7个C源文件、6个头文件另含Qt工程文件(.pro)、界面文件(.ui)和用户配置文件(.user)代码体量精炼结构清晰。目前已有402人学习下载是一份实用的工程参考。源码覆盖二进制流读取、COMTRADE数据结构解析、时间戳转换、QCustomPlot波形绘制与界面交互等关键环节通过图形界面加载数据后可同时查看模拟量、数字量通道的波形与数值直观对照故障暂态过程。深入阅读代码能理解COMTRADE文件格式的完整解析思路并掌握QT在文件处理、数据可视化、跨平台发布等方面的综合应用对电力系统二次开发或QT学习都很有帮助。 做电力二次、搞故障分析的同行手机里没几个COMTRADE文件说不过去。继保装置动作、故障录波器启动、电能质量监测终端报警导出来的都是这个格式。COMTRADE全称是Common Format for Transient Data Exchange也就是暂态数据交换通用格式由IEEE标准C37.111定义最新版本对应IEC 60255-24。这个格式解决的核心问题只有一个让不同厂家、不同型号的录波装置导出的数据能在同一个工具里打开、分析、对比。我见过太多人在这一步卡住——装置厂家的专用软件只能看自家文件换个厂家就得装另一套软件电脑里塞了五六个播放器就为了看个波形。COMTRADE的价值恰恰就在这里它是电力系统暂态数据交换的“普通话”把厂家绑定彻底打破。无论你是做故障分析、保护定值校验、波形回放仿真还是搞电能质量评估只要把COMTRADE文件的解析逻辑搞明白就掌握了打开几乎所有录波数据的钥匙。这篇文章我会从文件格式的底层结构讲起把配置文件逐行掰开再把数据文件的读取逻辑和实际代码实现完整走一遍最后聊聊我这些年处理COMTRADE文件时踩过的坑。不管你是刚接触录波数据的新人还是被各种格式折腾到头疼的老手看完都能自己动手写一个可用的解析工具。1. 先把COMTRADE文件家族认全拿到一个录波文件包解压后经常会看到四个同名的文件后缀分别是.cfg、.dat、.hdr、.inf。很多新手只盯着.dat文件看以为数据都在里面结果用记事本打开全是乱码。实际上这四个文件各管一摊缺了谁都不完整。1.1 四个文件各管什么cfg文件配置文件纯ASCII文本是整个文件包的“说明书”。里面写了采样率、通道数量、通道名称、比例因子、偏移量这些关键信息。解析一切数据的起点必须先读它。dat文件数据文件保存真正的采样值可以是ASCII文本也可以是二进制。这个文件怎么读完全取决于cfg文件怎么描述。hdr文件头文件可选。一般放一些人工输入的说明性文字比如故障原因、天气状况、运行方式等解析数据时用不上但值得保留归档。inf文件信息文件可选。属于补充说明性质有的厂家会放一些私有扩展信息进去标准不强求。打个比方cfg是“体检表上的项目说明”dat是“体检数据本身”hdr和inf属于“备注栏”。没有体检说明你就不知道数据哪一项对应什么含义没有数据本身说再多都是空的。1.2 版本差异先认清COMTRADE标准经历了1991、1999、2013三个主要版本。1991年版本最老通道定义里没有时间基准time base字段解析老文件时要注意。1999版本补上了这个字段同时增加了对采样率变化表的支持。2013版本改动最大加入了XML格式的方案但实际工作中99%的录波文件还是传统文本/二进制格式所以把传统格式吃透才是重点。我建议你拿到文件第一件事先看cfg文件的第一行确认它是哪个版本。version字段在文件头里一般长这样STATION_NAME,DEVICE_ID,1999看到1999就知道这是1999版的配置格式后面的解析逻辑就可以放心按这一版的标准来。有些老装置导出的是1991格式第一行没有逗号分隔直接是一串字符串那就得按老标准的字段顺序来读通道信息。2. 配置文件逐行拆解cfg文件是整个解析流程的“总开关”每一行都有固定含义顺序不能乱。我把一个典型的1999版cfg文件拆开讲对照着看你会更清楚。220kV某变电站,录波器#2,1999 8,6A,2D 1,UA,V,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 2,UB,V,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 3,UC,V,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 4,IA,A,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 5,IB,A,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 6,IC,A,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0 1,断路器位置,D,,,,1.0,0.0,0.0,1.0,0.0,1.0 2,保护动作,D,,,,1.0,0.0,0.0,1.0,0.0,1.0 1,10000,0.499950 2,4000,0.500000 ASCII 2024/05/20,14:33:25.123456 0.999950,5.0000002.1 头部信息第一行220kV某变电站,录波器#2,1999三个字段分别是站点名称、设备标识、标准版本号。注意版本号是1991时这一行的第三字段可能没有直接就是站点名和设备名。第二行8,6A,2D分别是总通道数8个其中模拟通道6个、状态通道2个。这个关系要留意总通道数必须等于模拟通道加状态通道解析时用这个来校验文件是否完整。2.2 通道定义部分从第三行开始先是6行模拟通道定义每行固定12个字段通道序号,通道名称,相别,被测对象,单位,比例因子a,偏移量b,最小值,最大值,时间基准,保留字段举例1,UA,V,,,,0.010000,0.0,-32768.0,32767.0,0.0,1.0通道序号1从1开始递增通道名称UAA相电压相别V表示电压A表示电流、D表示状态量被测对象空单位空比例因子a0.010000用于把存储的原始整数值换算成实际工程值偏移量b0.0实际值 存储值 × a b最小值-32768.0最大值32767.0时间基准0.0表示该通道的时间基准与全局时间基准一致保留字段1.0模拟通道定义完后是状态通道定义。状态通道只有8个字段通道序号,通道名称,相别,被测对象,状态类型,比例因子a,偏移量b,最小值,最大值,保留字段1,断路器位置,D,,,,1.0,0.0,0.0,1.0,0.0,1.0状态通道的数值含义和模拟通道完全不同它表示的是开关量状态0或1不做线性换算。解析时直接把原始值当整型处理结合通道名称判断对应哪个开关或保护信号。2.3 采样率表cfg文件里采样率表是很多初学解析的人容易忽略的地方。录波器不是全程用同一个采样率采样的——故障前可能用低速比如1000Hz记录波形故障发生后自动切换成高速比如10000Hz捕捉细节。采样率表就是为了描述这种切换关系1,10000,0.499950 2,4000,0.500000这里的含义是从文件起点开始前0.499950秒按10000Hz采样后面的0.500000秒按4000Hz采样。两行加起来约1秒正好是录波时长。采样率表是计算时间轴的关键依据解析时不能简单认为每个采样点间隔相等。很多人画出来的波形时间轴是歪的多半就是没处理这个采样率切换。2.4 数据格式与时间戳cfg文件最后几行还藏着几个重要参数ASCII 2024/05/20,14:33:25.123456 0.999950,5.000000第一行ASCII表示dat文件是ASCII文本格式如果是BINARY则代表二进制格式后续读取方式完全不同。第二行2024/05/20,14:33:25.123456是录波启动时刻精确到微秒。第三行0.999950,5.000000分别是抽样速率sample rate和总时间长度抽样速率是名义值总时长是实际记录时长这两个值可以用来校验采样率表是否自洽。3. 数据文件读取的核心逻辑cfg文件读透了dat文件就是“按图索骥”。dat文件每行或每条记录对应一个采样时刻的所有通道数据读取逻辑取决于cfg里声明的是ASCII还是BINARY。3.1 ASCII格式的数据文件ASCII格式的dat文件每行是一条完整采样记录大概长这样1,0,1234,1235,1236,2345,2346,2347,0,1 2,0,1233,1235,1236,2344,2346,2347,0,1每行的第一个字段是采样点序号从1开始第二个字段是时间戳单位微秒通常相对文件起始时刻后面的字段按cfg里通道定义的顺序排列模拟通道在前、状态通道在后。模拟通道的值是整型需要用比例因子a和偏移量b换算成实际值状态通道的值表示开关量状态直接使用。3.2 二进制格式的读取与大小端二进制格式的dat文件不是每行一条记录而是每条采样记录按固定字节长度紧密排列。1999版标准里每条记录的结构是采样序号4字节整数时间戳4字节整数单位微秒模拟通道数据每个2字节顺序与cfg通道定义一致有6个模拟通道就占12字节状态通道数据每个2字节顺序与cfg通道定义一致有2个状态通道就占4字节所以一条记录的总字节数 4 4 2×模拟通道数 2×状态通道数。按这个固定长度顺序读取即可。读的时候注意字节序大小端问题大部分装置用little-endian但个别老设备用big-endian读出来数值异常大或出现NaN时先怀疑字节序是不是反了。3.3 比例因子和偏移量为什么不能忽略cfg里明确写着比例因子a和偏移量b有的人图省事直接拿原始整型值画波形画出来形状看不出来问题但纵轴数值完全不对。比如电流互感器二次额定值1A对应存储值10000左右比例因子0.0001实际工程值就是1.0A。不换算是10000A单位对不上做故障分析根本没法判断短路电流大小。换算公式很简单实际值 存储值 × a b。这个a和b在通道定义行里顺序靠前解析时务必按字段顺序取千万别搞混。3.4 时间轴计算与采样率切换时间戳字段在ASCII和二进制里都有单位微秒。正常情况下第一个采样点的时间戳是0后面每个采样点的时间戳按采样周期递增。但如果存在采样率切换时间戳的递增间隔会在切换点发生变化——先用10000Hz采样两个采样点间隔100微秒切到4000Hz后间隔变成250微秒。解析时按cfg里的采样率表来判断当前采样点落在哪个区间用对应的采样周期计算时间轴。更稳妥的办法是直接读每条记录的时间戳字段不自己推算这样无论采样率怎么切都不会错位。4. 动手写一个可用的解析器理论讲完上实操。我用Python写一个COMTRADE解析器代码不做过度封装重点是把读取流程讲清楚。环境需要Python 3.8只需要标准库不需要额外安装第三方包。4.1 解析cfg配置文件先写一个读取cfg文件的函数把通道定义、采样率表、数据格式等关键信息提取出来import os from dataclasses import dataclass, field from typing import List dataclass class AnalogChannel: index: int name: str phase: str circuit: str unit: str a: float b: float min_val: float max_val: float time_base: float reserved: float dataclass class StatusChannel: index: int name: str phase: str circuit: str status_type: str a: float b: float min_val: float max_val: float reserved: float dataclass class SampleRateEntry: rate: float end_time: float dataclass class ComtradeConfig: station_name: str device_id: str version: int total_channels: int analog_count: int status_count: int analog_channels: List[AnalogChannel] field(default_factorylist) status_channels: List[StatusChannel] field(default_factorylist) sample_rates: List[SampleRateEntry] field(default_factorylist) data_format: str start_time: str def parse_cfg(cfg_path): with open(cfg_path, r) as f: lines [line.strip() for line in f if line.strip()] header lines[0].split(,) cfg ComtradeConfig() if len(header) 3: cfg.station_name, cfg.device_id, cfg.version header cfg.version int(cfg.version) else: cfg.station_name, cfg.device_id header[0], header[1] cfg.version 1991 channel_info lines[1].split(,) cfg.total_channels int(channel_info[0]) cfg.analog_count int(channel_info[1].replace(A, )) cfg.status_count int(channel_info[2].replace(D, )) line_idx 2 for _ in range(cfg.analog_count): parts lines[line_idx].split(,) ch AnalogChannel( indexint(parts[0]), nameparts[1], phaseparts[2], circuitparts[3], unitparts[4], afloat(parts[5]), bfloat(parts[6]), min_valfloat(parts[7]), max_valfloat(parts[8]), time_basefloat(parts[9]), reservedfloat(parts[10]) ) cfg.analog_channels.append(ch) line_idx 1 for _ in range(cfg.status_count): parts lines[line_idx].split(,) ch StatusChannel( indexint(parts[0]), nameparts[1], phaseparts[2], circuitparts[3], status_typeparts[4], afloat(parts[5]), bfloat(parts[6]), min_valfloat(parts[7]), max_valfloat(parts[8]), reservedfloat(parts[9]) ) cfg.status_channels.append(ch) line_idx 1 sample_rate_count int(lines[line_idx].split(,)[0]) line_idx 1 for _ in range(sample_rate_count): parts lines[line_idx].split(,) cfg.sample_rates.append(SampleRateEntry( ratefloat(parts[1]), end_timefloat(parts[2]) )) line_idx 1 cfg.data_format lines[line_idx] cfg.start_time lines[line_idx 1] return cfg这段代码把cfg文件映射成了Python对象通道信息直接按字段名访问后续解析数据时用起来会很顺手。解析时有个细节模拟通道和状态通道的行数不一定等于cfg第二行声明的数量稳妥做法是解析完通道定义后校验一下len(analog_channels)和len(status_channels)是否与声明一致不一致说明文件被截断或损坏。4.2 读取ASCII格式的数据文件ASCII格式的dat文件读取比较简单逐行split即可def parse_dat_ascii(dat_path, cfg): analog_data [[] for _ in range(cfg.analog_count)] status_data [[] for _ in range(cfg.status_count)] timestamps [] index [] with open(dat_path, r) as f: for line in f: if not line.strip(): continue parts line.split(,) index.append(int(parts[0])) timestamps.append(int(parts[1])) for i in range(cfg.analog_count): raw_val int(parts[2 i]) ch cfg.analog_channels[i] actual_val raw_val * ch.a ch.b analog_data[i].append(actual_val) offset 2 cfg.analog_count for i in range(cfg.status_count): status_data[i].append(int(parts[offset i])) return index, timestamps, analog_data, status_data这个函数里最关键的一行就是actual_val raw_val * ch.a ch.b。不要小看这一步所有通道的数值换算都靠它。对于状态通道直接取整数值别做浮点运算否则0和1可能会变成0.9999999或1.0000001后续做布尔判断会出问题。4.3 读取二进制格式的数据文件二进制格式稍微复杂一点需要用struct模块按固定长度解包import struct def parse_dat_binary(dat_path, cfg): channel_count cfg.total_channels record_size 4 4 2 * cfg.analog_count 2 * cfg.status_count analog_data [[] for _ in range(cfg.analog_count)] status_data [[] for _ in range(cfg.status_count)] timestamps [] index [] with open(dat_path, rb) as f: data f.read() for offset in range(0, len(data), record_size): record data[offset:offset record_size] if len(record) record_size: break sample_index, timestamp struct.unpack(II, record[:8]) index.append(sample_index) timestamps.append(timestamp) pos 8 for i in range(cfg.analog_count): raw_val struct.unpack(h, record[pos:pos 2])[0] pos 2 ch cfg.analog_channels[i] actual_val raw_val * ch.a ch.b analog_data[i].append(actual_val) for i in range(cfg.status_count): status_val struct.unpack(H, record[pos:pos 2])[0] pos 2 status_data[i].append(status_val) return index, timestamps, analog_data, status_data二进制解析的三个关键点集中在格式控制上II两个4字节无符号整数用小端序读取采样序号和时间戳h2字节有符号短整型对应模拟通道的原始值。之所以用有符号是因为录波器的AD转换结果是有符号的负值表示反向H2字节无符号短整型对应状态通道。状态通道只看位标志无符号更安全如果读取出来的模拟值出现明显异常比如电压值在几百万可以尝试把h改成h即big-endian方式重新读取。绝大多数现代装置都遵循little-endian但老设备偶尔会有例外。4.4 时间轴统一计算不管哪种格式最终都要把时间戳数组转换成秒为单位便于画图和分析def build_time_axis(timestamps): # timestamps是微秒整数数组 base timestamps[0] if timestamps else 0 time_sec [(ts - base) / 1e6 for ts in timestamps] return time_sec时间轴归零后波形图的横轴从0开始单位秒。如果要叠加多个文件做对比这一步尤其重要——不同录波器的启动时刻有细微差异如果不把起始时刻归零直接叠加会导致波形错位。4.5 快速验证解析结果解析完数据后建议先做一轮自检再进入分析阶段检查采样点数是否与cfg里声明的总时长乘采样率一致有采样率切换时按各区间分别计算检查模拟通道的数值范围电压通道应该在额定电压附近波动电流通道在故障时可能出现数十倍额定值如果出现天文数字说明比例因子或字节序有问题检查状态通道的跳变点断路器位置、保护动作这些开关量正常情况只出现有限次跳变如果像噪声一样频繁抖动说明状态通道读取位置错位这三步能过滤掉90%以上的低级错误别急着画图先自检省得后面返工。5. 常见问题与排查技巧实录解析COMTRADE文件这事儿看起来不复杂但实际操作中坑不少。我整理几个高频问题附上排查思路和解决方案都是自己踩过的坑。5.1 文件能读但数值明显不对现象电压波形画出来是几百万伏或者电流直接到几十万安培。排查步骤先看比例因子a和偏移量b是否被正确读取。有的cfg文件里a和b位置是空的要用0和1填充解析时如果没做默认值处理就会出现float()异常再看字节序。二进制文件先用小端序解包如果数值大得离谱切换成大端序试试最后检查数据类型。模拟通道用h有符号短整型还是H无符号短整型取决于装置手册。有些装置把模拟通道存成无符号数用有符号解包会出现负的电压值5.2 采样率切换导致波形时间轴错位现象波形图前半段正常后半段时间轴拉长或压缩像是录制速度变了。这个问题的根源是采样率表没被正确处理。比如一个文件前0.5秒用10000Hz、后0.5秒用4000Hz如果你一直用10000Hz去算时间后半段波形的时间间隔会被压缩成原来的40%。解决方法有两种读时间戳字段每条记录自带时间戳直接用时间戳除以1e6得到秒完全绕过采样率计算读采样率表按cfg的采样率表计算每个采样点的理论时间需要额外维护一个“当前采样率”状态遇到切换点就更新第一种方法更省事推荐优先使用。前提是时间戳字段可靠实际上标准对时间戳有强制要求大多数装置都会正确写入。5.3 状态通道值解析错位现象断路器位置、保护动作这些状态量要么全部是0要么跳变规律完全不符合实际。这个问题的常见原因是通道顺序没搞对。cfg文件里模拟通道和状态通道是分开定义的但dat文件里它们混在同一条记录里——顺序一定先是模拟通道后是状态通道。如果你按通道定义顺序循环读取时没区分模拟和状态的边界状态通道就会从错误的位置取值。排查思路先打印cfg里通道定义的名称顺序再打印dat文件第一条记录的原始字段值人工对照一下。正常情况如果定义顺序是UA、UB、UC、IA、IB、IC、断路器、保护dat文件的字段顺序也应该完全一致。一旦错位用第一条记录手动对一次就能发现。调试小技巧写解析器时加一个debug参数把第一条记录的原始数值和换算后的数值同时打印出来。这个功能在排查通道混淆问题时特别好用建议保留在代码里。5.4 文件编码和中文乱码现象cfg文件里的中文通道名变成乱码。大部分录波装置的cfg文件是GBK或GB2312编码写入的Python用open()默认utf-8读会乱码。解决办法是读取时显式指定编码with open(cfg_path, r, encodinggbk, errorsignore) as f:注意有些老装置甚至用gb18030如果gbk报错就换成gb18030试试。实在不行用errorsignore跳过无法解码的字节至少保证解析流程不会中断。还有个更隐蔽的情况cfg文件末尾的换行符是\r\nWindows风格读取时要strip掉否则行尾会带\r影响字段解析。5.5 大文件的性能问题一个典型的故障录波文件10000Hz采样率、持续5秒、20个通道数据量就是10万条记录、200万个采样值ASCII格式文件体积轻松超过20MB。如果逐行用Python字符串split处理加载可能要几十秒体验很差。性能优化层面我试过几种方式二进制格式比ASCII快很多因为不需要做字符串解析直接按固定字节数struct解包即可用numpy的fromfile或memmap直接读取整个二进制文件再批量做数值换算速度能提升一个数量级ASCII格式可以考虑用pandas.read_csv配合enginec比标准库快3-5倍如果是做实时处理或批量分析建议优先考虑二进制格式和numpy方案。普通单文件分析用标准库也能接受具体选型看应用场景。6. 从解析到应用COMTRADE数据的实际使用场景解析出波形数据只是第一步真正有价值的是把数据用起来。我已经把COMTRADE解析器封装成工具类后日常不少场景都靠它支撑。故障分析是最常见的场景解析出三相电压、三相电流波形用代码自动判断故障相、故障时刻、故障持续时间。相比人工翻波形代码可以批量处理几十个文件效率完全不在一个量级。保护装置动作行为分析也离不开COMTRADE。比如某条线路发生区内故障保护动作时间是否满足要求需要精确到毫秒级又比如某个保护出现误动要通过波形回放看启动元件是否动作、动作顺序是否合理。这些分析都要求对数据的时间分辨率足够高COMTRADE 10000Hz的采样率完全够用。电能质量分析是另一个重要方向。电压暂降、暂升、谐波、闪变这些暂态事件录波器记录下来后用Python的numpy和scipy做频谱分析、小波变换可以识别扰动源类型和严重程度。我做过一个项目把连续三个月的录波文件批量导入数据库自动生成电压暂降事件统计报表定位到具体是哪条馈线、哪个时间段频繁发生暂降帮用户找到了一条老化的电缆接头替换后故障频次直线下降。仿真回放也是COMTRADE的热门应用。把真实故障波形导入电力系统仿真软件比如PSCAD、Simulink可以验证保护定值是否合理、断路器动作时序是否正确。这比纯理论的整定计算更贴近实际因为波形里包含了线路分布电容、CT饱和、谐波畸变等真实因素。这些场景的底层逻辑都一样先把COMTRADE解析成标准的数据结构再喂给不同的分析工具。所以别小看解析器这一步它是整个电力数据分析链路的“地基”。写完这些我把解析器的代码放在个人仓库里平时遇到新厂家的录波文件先用自检脚本跑一遍验证通道映射和数值换算都正确再放进批量分析流程。COMTRADE这个格式虽然谈不上新但它稳定、标准、覆盖范围广在电力行业的数据交换中依然无可替代。把它的解析逻辑吃透你手里就等于掌握了一个通用的录波数据“万能钥匙”换装置、换厂家、换软件都不用再从零开始折腾。本文还有配套的精品资源点击获取