COMTRADE文件解析全攻略:电力故障录波数据处理实战 简介QTCOMTRADE 是一套基于 Qt 5.15.2 编写的 COMTRADE 暂态数据解析工具面向电力系统继电保护、故障分析与录波数据研究场景能直接读取 DAT 二进制文件并还原电压、电流、功率等电气量波形适合具备一定 C 基础、希望深入掌握标准数据格式解析的工程人员。压缩包共 16 个文件归档后大小约 337KB主要包括 7 个 C 源文件与 6 个头文件另有工程配置、界面布局和用户设置等辅助文件源码中同时带有 QCustomPlot 绘图组件方便做曲线展示与交互。已有四百余人浏览学习。通过阅读这份工程可以理解 COMTRADE 的记录结构、时间戳转换、多通道数据组织方式并掌握二进制流读取、结构化解码、异常处理以及跨平台用户界面设计的完整思路对后续开发电力系统数据分析工具或分析实际录波文件有直接的参考价值。 搞电力系统这行不管是做继电保护、故障分析还是自动化运维迟早都得跟COMTRADE文件打交道。这玩意儿标准化程度高但它毕竟是上世纪90年代定下来的老格式真拿它干活儿的时候总会遇到一些文档里没写明白、只能靠实战摸索的细节。我这些年用这个格式处理过不少故障录波数据踩过的坑不少也沉淀了一些能直接落地的经验今天就把它从头到尾掰开揉碎了聊一聊。COMTRADE全称是Common Format for Transient Data Exchange for Power Systems通俗点理解它就是电力系统暂态数据的“通用交换格式”。不管是故障录波器、继电保护装置还是测控装置只要录波基本都能导出这个格式。它能解决什么问题一句话让不同厂家、不同型号的设备录下来的数据能在同一个软件平台里被解析、分析和共享。这个标准由IEEE制定目前主流是1991版、1999版和2013版老装置多偏91版新合并单元和数字化变电站则常见99或13版。适合谁看继保调试人员、变电站运维人员、电力数据分析工程师、二次设备研发测试人员都值得花十分钟把这套格式的逻辑捋顺。1. 内容整体设计与思路拆解1.1 为什么解析COMTRADE不能只盯着数据文件很多新手拿到一个录波文件压缩包第一时间就盯着主数据文件看打开全是二进制乱码或者是一堆密密麻麻的数字瞬间头大。实际上COMTRADE标准设计的核心思想是“配置数据分离”这个设计思路一旦理解了整个解析过程就通了一半。一个标准完整的COMTRADE记录通常由四个文件组成配置文件.cfg、数据文件.dat、信息文件.inf和头文件.hdr。其中.cfg和.dat是灵魂.inf和.hdr是辅助。配置文件里写的是“这段录波里有哪些通道、采样率多少、变比多少、什么时间录的”相当于目录和说明书数据文件里才是真正的采样值是一串串数字。做二次开发和深度分析时必须先读.cfg再去解析.dat否则连通道个数都不知道数据文件根本无法下手。这个思路跟拍照有点像。照片本身是CCD/CMOS传感器上的一堆亮度值你得知道像素分辨率、色彩深度、白平衡设置才能把这些数字还原成一张能看的图片。COMTRADE的.cfg就是这些元信息.dat就是原始像素数据。1.2 版本差异带来的选型问题COMTRADE标准有版本差异不能一视同仁。1991版是基础版主要面向传统互感器通道数量、采样率标识都比较简单1999版IEEE C37.111-1999增加了数字通道扩展、GPS时间同步、采样率变化等支持2013版是目前最新的引入了文件名编码、ASDU扩展、缩放因子多样性等重大变更主要适配数字化变电站和IEC 61850环境下的大量数据交互。我个人的建议是做解析工具时优先把1999版作为基准实现因为它向下兼容大部分1991版的常规用法同时向上兼容2013版的部分特性。比如采样率变化标识nrates这个字段在1999版之后才普遍应用但很多1991版的录波也会用模拟量工频过零法来标识采样区间如果完全不做版本判断解析结果就会偏差很大。1.3 解析方案选型自己写还是用现成库做项目时最常被问到的就是“解析COMTRADE到底用现成库还是自己写”。这取决于你的使用场景。如果只是偶尔看看波形、做简单的故障分析直接上开源库是效率最高、最稳的。Python生态里有comtrade这个库GitHub上星不少API清晰支持CFG配置读取、DAT数据解码、通道映射、缩放因子自动处理基本能覆盖90%的常规需求。但如果你是做性能要求高、并发量大的在线数据接入系统或者需要支持一些特殊厂家的私有扩展字段那自己写核心解析逻辑更靠谱。我的经验是用现成库保证基础解析稳定性然后自己封装一层业务映射层专门处理厂站名称、线路编号、保护动作信息这些跟具体业务相关的字段。这样既稳又灵活。2. 核心细节解析与实操要点2.1 认识四个文件的分工与优先级真正的老手拿到录波数据不是先看数据内容而是先看文件家族是否完整。COMTRADE标准中.cfg和.dat是必需成对存在的文件缺了任何一方数据都无法正常打开.hdr和.inf则是可选文件分别用于存放波形注释信息比如故障时间、保护动作情况、短路类型和录波设备状态信息比如设备型号、软件版本、通道定义补充说明。.inf文件的作用经常被忽略但实际上它往往是故障信息最集中的地方。以我自己用过的某国产录波装置为例保护动作的详细事件顺序记录SOE和故障测距结果都写在.inf文件里而.cfg和.dat里只存原始波形。如果分析人员只看波形不看.inf很容易错过关键动作信息。所以解析COMTRADE文件的第一要务是先判断这个录波包是有完整四件套还是只有核心两件套再决定从哪里提取业务信息。解析顺序应该是先读.cfg拿到通道和采样结构再读.dat取波形数据最后解析.inf/hdr补充故障语义。2.2 逐行拆解CFG文件的结构配置文件里的每一行都对应一种信息字段间以逗号分隔。最关键的行包括第一行是文件版本号和记录信息比如“1999, 5, 12, 10:30:00.123456, 10:30:02.456789, 20”表示版本是1999年标准采样开始时间是某时刻触发时间是某时刻一共20个采样点。第二行到第N行是模拟量或数字量通道定义每行包含通道序号、通道名称、相别、被测量单位、变比系数、偏移量、缩放因数、数据最小值/最大值等。接下来一行标识采样率个数比如2然后单独几行说明每个采样率区间的采样点数和对应采样频率。最后几行是数据文件格式标识0表示ASCII格式1表示二进制格式2表示带时标的二进制格式还有一个整数表示数据文件里采样的总周波数。这里有个细节需要特别留意很多设备的CFG文件里通道名称会写成中文GBK编码而此时如果你用UTF-8默认解码就会出现乱码甚至导致解析中断。处理办法是读取时先尝试UTF-8遇到异常再回退到GBK/GB18030同时把通道名称的编码问题做一个可配置项。2.3 DAT数据文件的两种格式差异数据文件根据CFG文件中的格式标识分为ASCII和二进制两类。ASCII格式直观每个采样点一行数字之间用逗号分隔包含了采样序号、时间戳以及各通道的采样值方便人工查看但文件较大。二进制格式则紧凑高效通道数据以标准C类型存储比如16位或32位有符号整数浮点数则用IEEE 754标准表示。二进制格式又细分为两种一种是固定采样率的压缩二进制另一种是支持采样率变化的扩展二进制。后者每个采样点都会额外带一个时间戳通常是4字节整数加4字节微秒解析时务必按这个结构逐字节读不能想当然地认为每个记录长度一样。还要注意数据格式标志行的最后几个字符里有些厂家的录波器会写“1”表示二进制但实际数据是按“2”的带时标格式存放的不读完整行直接按固定偏移去切分很容易解析出乱数据。建议所有二进制解析都先按2字节对齐预扫描一遍校验采样序号是否连续、时间戳是否符合工频周波间隔用这个办法能快速发现解析偏差。2.4 采样值时间戳的处理COMTRADE里的时间戳采用相对时间记录方式以CFG中声明的开始采样时间作为零点然后每个采样点记录相对该零点的时间偏移。时间戳精度直接影响暂态分析结果的置信度尤其是跨装置比对时各装置GPS对时误差和晶振漂移都会叠加到采样时刻上。处理时要特别留意“触发前时间”和“触发后时间”的分界点。很多厂家的录波数据前一段是故障前正常状态触发后才是故障波形CFG文件里并未直接给出触发点所在的采样序号但可以通过计算触发时间与开始时间的差值再乘以采样率来推算。如果CFG中触发时间字段为0那说明录波器设置的是实时触发持续记录这时候就得借助.dat里通道数据的突变点来做波形对齐。解决时间戳抖动的一个实际技巧是解析时先采样序号再算时间差如果发现时间差忽大忽小优先怀疑装置晶振温漂或GPS失步此时按采样序号处理比按时间戳处理更可靠。3. 实操过程与核心环节实现3.1 实操数据准备与工具选择在处理真实录波时我的工作流大致分四步拿到文件包先做文件名和扩展名的交叉校验.cfg/.dat/.hdr/.inf四个文件必须同主名大小写不敏感然后读CFG再按格式标识解码DAT最后输出统一结构化的分析数据。工具链上我比较推荐“Python comtrade库 自研业务映射层”这个组合。Windows环境下用Anaconda或miniconda管理包依赖代码用PyCharm或VS Code都行。如果工作环境没有Python用MATLAB也可以解析但效率相对低一些而且处理高采样率长录波时内存占用大。解析过程的第一步是写一个CFG类把文件中的字段逐行拆出来转换成Python对象。我习惯把“通道名称”、“单位”、“变比”和“缩放因子”等都作为属性存起来后面绘制波形图直接调用。注意这里缩放因子的计算要按标准公式实际数值 (原始整数值 - 偏移量) * 缩放因子顺序不能反否则幅值就全错了。3.2 Python解析CFG与DAT的核心代码实现先看一个最小可运行的解析脚本这段代码可以还原出模拟量通道的真实电压电流值并且能自动区分ASCII和二进制格式基于1999版标准。import struct class COMTRADE_Parser: def __init__(self, cfg_path, dat_path): self.cfg_path cfg_path self.dat_path dat_path self.channels [] self.sample_rates [] self.data_format None self.parse_cfg() def parse_cfg(self): with open(self.cfg_path, r, errorsreplace) as f: lines f.readlines() # 第一行: 版本, 开始时间, 触发时间, 采样点数 first lines[0].strip().split(,) self.station_name first[0] self.start_time first[2] , first[3] self.trigger_time first[4] , first[5] self.num_points int(first[6]) # 通道行数 analog_count int(lines[1].strip().split(,)[1]) digital_count int(lines[1].strip().split(,)[2]) idx 2 for i in range(analog_count): ch lines[idx].strip().split(,) self.channels.append({ type: A, name: ch[1], unit: ch[3], a: float(ch[4]), # 变比系数 b: float(ch[5]), # 偏移量 scale: float(ch[6]), min: float(ch[7]), max: float(ch[8]) }) idx 1 for i in range(digital_count): ch lines[idx].strip().split(,) self.channels.append({ type: D, name: ch[1], phase: ch[2], status: ch[4] }) idx 1 # 采样率部分 nrates int(lines[idx].strip().split(,)[1]) idx 1 for i in range(nrates): sr lines[idx].strip().split(,) self.sample_rates.append((int(sr[0]), float(sr[1]))) idx 1 # 数据格式 df_line lines[idx].strip().split(,) self.data_format int(df_line[1]) def read_data(self): # 返回每个通道的采样值列表 analog_values [[] for _ in range(self.analog_count())] digital_values [[] for _ in range(self.digital_count())] total_channels len(self.channels) analog_num self.analog_count() digital_num self.digital_count() if self.data_format 0: # ASCII格式 with open(self.dat_path, r) as f: for line in f: fields line.strip().split(,) # 每行: 序号, 时间戳, 各通道值 for i in range(analog_num): raw_val float(fields[2 i]) ch self.channels[i] real_val (raw_val - ch[b]) * ch[scale] # 实际值换算 analog_values[i].append(real_val) for i in range(digital_num): digital_values[i].append(int(fields[2 analog_num i])) elif self.data_format 1 or self.data_format 2: # 二进制格式 with open(self.dat_path, rb) as f: for point in range(self.num_points): # 前两个是序号和时间戳时间戳长度取决于格式 seq struct.unpack(I, f.read(4))[0] if self.data_format 2: ts struct.unpack(I, f.read(4))[0] # 有的厂家此处是精确时间戳8字节 extra f.read(8) if self.check_stamp_length() else b else: ts struct.unpack(I, f.read(4))[0] for i in range(analog_num): raw_val struct.unpack(h, f.read(2))[0] # 16位有符号 ch self.channels[i] real_val (raw_val - ch[b]) * ch[scale] analog_values[i].append(real_val) for i in range(digital_num): dval struct.unpack(H, f.read(2))[0] digital_values[i].append(dval) return analog_values, digital_values def analog_count(self): return sum(1 for c in self.channels if c[type] A) def digital_count(self): return sum(1 for c in self.channels if c[type] D) def check_stamp_length(self): # 探测时间戳长度避免偏移错误 return self.data_format 2这段代码里有一个很重要的设计点(raw_val - ch[b]) * ch[scale]这个换算公式。很多录波器在CFG中的偏移量b和缩放因子scale是针对“二次侧”预设的而有的装置直接给的是“一次侧”值。我的建议是解析时不硬编码按一次或二次换算而是把原始整数值和通道参数都原样保留让上层分析模块自行决定采用哪一侧数据。这样能避免“同一个文件不同软件看到不同幅值”的尴尬。3.3 处理多采样率的录波数据老式录波器基本都是固定采样率比如每周波40点对应50Hz系统就是2000Hz。但现代录波器为了兼顾长录波和暂态精度往往会动态切换采样率。典型模式是故障前和故障后各一段低采样率比如1000Hz故障期间切到高采样率比如10000Hz。此时CFG中的“nrates”字段会大于1且每个采样率区间有对应的点数。处理多采样率数据的关键是搞清楚每个区间对应的绝对时间范围。如果直接把不同采样率的数据按序号拼接画出来的波形会出现严重的横轴错位甚至看不出周波跳变点。我的处理策略是解析时把每个采样率区间作为独立数据块记录起始采样序号和结束采样序号然后按采样率插值或降采样到统一时间轴。如果只是做故障测距或暂态特征分析一般保留高采样率段低采样率段做趋势参考即可。用Python的numpy.interp可以快速完成时间轴统一实测效率很高。低频数据和高频数据衔接处的第一个点我一般会特殊处理——它对突变检测非常敏感很多录波器的触发定位就是靠检索这个点。3.4 绘制波形与自动判据解析完成后画波形是常见的下一步。我会把多通道波形组织成一张纵向分栏图模拟量通道以电压和电流分组数字量通道以状态量形式显示在底栏。Python里的matplotlib足够满足这个需求采样率变化点用竖线标出来触发时刻用红色虚线标出。我还习惯在绘图前做一次简单的数据校验电压通道的有效值是否在正常范围电压互感器二次侧约57.7V或100V电流通道要看变比通道是否出现满量程削顶是否有明显直流偏置。这些校验能快速判定录波文件本身是否存在问题避免一个坏数据拖慢整条分析链路。4. 常见问题与排查技巧实录4.1 CFG文件编码问题导致乱码这是遇到最多的坑没有之一。国内很多厂家的录波装置为了显示友好通道名称用的是中文但保存时用的是GBK编码。直接用open(cfg, r)默认UTF-8读取轻则乱码名称重则因为某些字节被当成了控制字符直接抛异常。排查技巧先用二进制模式打开文件查看文件头前三个字节是不是EF BB BFUTF-8 BOM如果是基本可以确定是UTF-8编码如果没BOM则优先试GBK用errorsreplace辅助。我建议封装一个detect_encoding()函数先用charset库做启发式检测再做一个白名单确认能省掉很多后续麻烦。4.2 二进制数据格式标志不匹配CFG最后的数据格式字段写明是0ASCII但.dat文件实际是二进制或者反过来的情况我遇到过不止一次。这通常是因为装置固件升级后配置模板没有同步更新。解析前先读数据文件的前几个字节做试探性判断如果第一个采样点的前4字节转成整数后是一个很大的数比如几百万而通道数据对应的数值范围又不像真实电压电流基本可以判定格式标志与实际数据不一致。此时不要直接报错退出而是向调用方抛出一个可容忍的警告然后按实际探测出的格式继续解析并把这个异常记录到日志里。现场运维人员看到波形正常日志里又有一行提示会很感激这种设计。4.3 采样点少了一个或多了一个录波文件的采样点总数与CFG声明不一致大概率是数据传输或存储过程中发生了丢包或者附加了冗余数据。标准说法是CFG中的采样点数必须与DAT严格一致但实际中部分装置在触发后会多写一个周波的数据作为裕量。我的处理方式是先按CFG声明点数读取如果文件提前结束则按已读部分分析并告警如果文件还有剩余数据则读取完全后截断到声明点数并保留一个“数据点数是否超出声明”的标记字段。这样既不破坏下游的数据结构又保留完整性信息。4.4 通道变比参数异常导致幅值偏差这种情况最隐蔽。有的保护装置通道定义中“变比系数”一栏不是标准意义的PT/CT一次比二次而是包含了装置内部校准系数。照搬CFG里的a和b去换算计算出的电压可能是额定值的几倍或者电流小到看不见。排查思路是先在空载/稳态工况下录一段参考波形看解析结果是否与实际二次值吻合。如果偏差是整体缩放关系说明变换系数有固定倍数偏差如果是非线性偏差则大概率是缩放因子的计算方式不对。早年我调试一个110kV线路录波时发现解析出来的电压幅值总是偏大1.732倍最后查到是装置把线电压和相电压的系数混淆了。4.5 高采样率大数据量文件的性能优化一个10秒钟、采样率10kHz、36个模拟量通道的录波文件ASCII格式很容易超过200MB解析耗时动辄几十秒。对于这类场景我一般从三方面优化第一用二进制格式优先体积小、解析快第二解析时用numpy的fromfile批量读取而不是Python循环逐点读速度能快一个数量级第三只解析需要分析的通道其余通道延迟到绘图阶段再按需读取。实测下来同样的文件用纯Python逐行读ASCII需要约40秒改用numpy批量解析后只要1.2秒左右。如果CPU资源允许还可以用multiprocessing并行解析多个文件不过在绝大多数单文件分析场景下numpy方案已经够用。4.6 特殊格式与扩展兼容性一些厂家会在标准COMTRADE基础上做扩展常见的有自定义的私有标记字段、额外的文件摘要信息、嵌入在CFG头部的JSON配置块等。遇到这类文件首先要遵循“先按标准解析再按扩展字段补充”的原则。尤其是文件尾部追加的扩展文本一定不要影响标准字段解析。还有一个容易被忽略的地方.cfg文件规格化后的行尾可能是\r\n也可能只有\n。Linux环境下读取Windows生成的CFG如果不做行尾标准化字段拆分时会残留一个\r在最后一个字段末尾导致类型转换失败。处理方式是用line.rstrip(\r\n)替换split(,)前的操作或者统一用splitlines()。5. 小结与经验补充分享实际工作中COMTRADE解析这个环节虽然基础但它决定着你后续所有分析工作的数据质量。把CFG的每个字段吃透把DAT格式差异搞清楚再配合稳健的异常处理机制基本能覆盖大多数主流装置。最后再分享一个我个人偏好的处理习惯解析时不要直接丢原始值而是把原始采样整数、通道参数、换算后的实际值同时保存在结果结构里。这个做法在很多排障场景中帮了大忙——当发现计算结果异常时可以回溯原始整数和换算链路快速定位是装置参数问题还是解析逻辑问题。COMTRADE本身不是什么值钱的技术但它承载的数据是所有二次系统分析的源头。把源头理顺了后面画波形、算故障分量、跑保护动作分析都会顺很多。本文还有配套的精品资源点击获取