BPA SWI文件Python解析与Excel转换实战 简介本资源是一套基于Python实现BPABusiness Process AnalyzerSWI日志文件批量转换为Excel的完整工具集面向电力系统分析、工业流程建模及自动化数据处理领域的工程师与Python中级开发者。它解决了SWI这类非标文本格式难以直接分析的问题通过定制化解析逻辑结合pandas与openpyxl实现结构化提取、字段对齐与样式可控的Excel导出显著提升业务流程数据的可视化与决策支持效率。压缩包共2000个文件主体为1063个Python源码含核心转换逻辑、命令行封装及配置模块与1062个对应pyc编译文件辅以少量C/Fortran底层扩展如fortranobject.c、cpu_avx512系列、可执行程序exe、配置文件ini、cfg及1个真实SWI样本和1个xlsx输出示例整体大小21.65MB工程结构体现典型Python科学计算项目特征。已有499人学习下载用户可直接复用main.py主流程脚本参考多层模块组织方式并借鉴其针对SWI非结构化文本的逐行解析策略与异常容错设计。1. 为什么你手里的 BPA SWI 文件总在 Excel 里“失真”——用 Python 做真正可控的格式转换电力系统仿真工程师常被 BPABonneville Power Administration软件生成的.swi文件卡住它本质是纯文本但结构松散、字段对齐靠空格、注释混在数据行、母线名带空格或特殊字符、控制块之间用空行分隔——Excel 直接双击打开会错列用“数据→从文本导入”又得反复调分隔符和列类型更别说批量处理几十个工况文件。这不是 Excel 不行而是 SWI 格式根本不适配表格工具的默认解析逻辑。Python 不是来替代 Excel 的而是做 Excel 做不了的事精准识别 SWI 的语义块如BUS,LOAD,GEN、按 BPA 规范提取字段比如I,ID,AREA,ZONE,VM,VA、保留原始注释上下文、并把每类设备映射到独立工作表。本文面向已能写基础 for 循环的电力系统从业者不讲 Python 语法只讲怎么让 SWI 文件一跑就进 Excel、列对、数准、可复用。2. 解析 SWI 文件的核心逻辑跳过“文本”陷阱抓住“语义块”SWI 文件不是 CSV不能用pandas.read_csv硬读。它的结构是典型的“块驱动”block-driven每个设备类型以关键词开头如BUS后跟若干行数据块间用空行或0 / END OF BUS DATA, BEGIN LOAD DATA这类分隔符。直接按行切分会丢失块边界用正则全局匹配又易被注释行C ...开头干扰。必须先做三件事识别有效块起始、过滤注释与空行、按块归类数据行。常见误操作是试图用split()或strip()处理整行——SWI 中字段宽度不固定ID字段可能占 2 列也可能占 4 列空格数量不可信。正确做法是依赖 BPA 官方文档定义的字段位置如I在第 1–4 列ID在第 5–8 列用字符串切片而非空格分割。2.1 用字符串切片精准定位字段拒绝空格分割BPA SWI 格式严格遵循列宽定义见《BPA Program Manual Vol. III》Table 3-1。例如BUS数据块中列 1–4I母线编号整数列 5–8ID母线标识字符常含空格列 9–12AREA区域号列 13–16ZONE分区号列 17–22VM电压幅值浮点列 23–28VA电压相角浮点这意味着必须用line[0:4].strip()而非line.split()[0]提取I。后者在ID含空格时如1 或2 会把ID错当I的后续字段。def parse_bus_line(line: str) - dict: 按 BPA SWI 列宽规范解析 BUS 行返回字典 if len(line) 28: # 行太短跳过 return {} return { I: int(line[0:4].strip()) if line[0:4].strip().isdigit() else 0, ID: line[4:8].strip(), AREA: int(line[8:12].strip()) if line[8:12].strip().isdigit() else 0, ZONE: int(line[12:16].strip()) if line[12:16].strip().isdigit() else 0, VM: float(line[16:22].strip()) if line[16:22].strip() else 0.0, VA: float(line[22:28].strip()) if line[22:28].strip() else 0.0, }注意line[0:4]是 Python 切片取索引 0 到 3 共 4 个字符strip()去首尾空格避免 123 变成空字符串。isdigit()防止注释行如C BUS DATA导致int(C)报错。2.2 按关键词识别块边界构建块状态机SWI 文件中块切换靠关键词触发如BUS,LOAD,GEN,TRANSFORMER,0 / END OF ...。不能简单用if BUS in line因为注释行C BUS DATA也会命中。必须匹配行首或特定位置的关键词。我们用一个状态变量current_block记录当前解析的块类型并在遇到新块关键词时切换def identify_block_start(line: str) - str: 识别行是否为某类数据块的开始返回块名或空字符串 line line.strip() if not line or line.startswith(C): # 注释或空行 return # 匹配行首关键词且后跟空格或换行排除 BUSINESS 类误匹配 if line.startswith(BUS ) or line BUS: return BUS if line.startswith(LOAD ) or line LOAD: return LOAD if line.startswith(GEN ) or line GEN: return GEN if line.startswith(TRANSFORMER ) or line TRANSFORMER: return TRANSFORMER # 匹配结束标记如 0 / END OF BUS DATA if END OF BUS DATA in line or END OF LOAD DATA in line: return END_BUS return # 主解析循环示例 blocks {BUS: [], LOAD: [], GEN: [], TRANSFORMER: []} current_block None with open(case.swi, r, encodingutf-8) as f: for line in f: block_hint identify_block_start(line) if block_hint in [BUS, LOAD, GEN, TRANSFORMER]: current_block block_hint continue # 跳过关键词行本身下一行才是数据 if block_hint END_BUS: current_block None continue if current_block and not line.strip().startswith(C) and line.strip(): # 当前行属于 current_block且非注释、非空 if current_block BUS: parsed parse_bus_line(line) if parsed: # 非空字典才加入 blocks[BUS].append(parsed)提示identify_block_start必须检查line.strip()否则读入的\n会导致startswith(BUS )失败。current_block None在遇到END标记后重置防止跨块污染。2.3 处理 SWI 特有陷阱混合数据与注释、字段缺失、编码异常实际 SWI 文件常含以下问题混合行C THIS IS COMMENT后紧跟1234 1 1 1 1.0000 0.0000注释与数据在同一物理行字段缺失某些版本 SWI 中VA字段为空但列位仍保留编码问题老版 BPA 输出可能用ISO-8859-1而非 UTF-8。应对策略用正则re.split(r\s{2,}, line)按两个以上空格切分比单空格更鲁棒对缺失字段设默认值如VA0.0并在日志中记录警告尝试多种编码打开文件捕获UnicodeDecodeError后回退。import re def robust_split_line(line: str) - list: 用多空格分割避免单空格导致 ID 分裂 # 先移除行首注释C 开头直到行尾 clean_line re.sub(r^C.*$, , line).strip() if not clean_line: return [] # 按 2 个及以上空格分割保留字段内单空格如 ID1 return [part.strip() for part in re.split(r\s{2,}, clean_line) if part.strip()] # 示例处理 1234 1 1 1 1.0000 0.0000 → [1234, 1, 1, 1, 1.0000, 0.0000] # 而 1234 1A 1 1 1.0000 → [1234, 1A, 1, 1, 1.0000]3. 写入 Excel用 openpyxl 控制工作表结构与格式pandas.DataFrame.to_excel()适合规则表格但 SWI 转 Excel 需要① 每类设备一个工作表② 表头用 BPA 字段名I,ID,VM③ 保留原始顺序④ 对数值列设置数字格式如VM保留 4 位小数。openpyxl是唯一能精细控制单元格样式的库。关键点不用DataFrame中转直接将解析后的字典列表写入Worksheet避免pandas自动类型推断导致ID字符被转成数字。3.1 用 openpyxl 创建多工作表按块名命名from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter def create_excel_from_blocks(blocks: dict, output_path: str): wb Workbook() # 删除默认创建的 Sheet wb.remove(wb.active) # 定义各块的表头顺序严格按 BPA 手册 headers { BUS: [I, ID, AREA, ZONE, VM, VA, BASEKV, ZRO], LOAD: [I, ID, AREA, ZONE, PL, QL, IP, IQ, YP, YQ], GEN: [I, ID, PG, QG, QT, QB, VS, IREG, MBASE, ZR, ZX, RT, XT, GT, BT], TRANSFORMER: [I, J, K, CKT, R, X, SBASE, TM, ANG, RATEA, RATEB, RATEC, COD1, RATA1, XATA1] } for block_name, data_list in blocks.items(): if not data_list: continue ws wb.create_sheet(titleblock_name[:31]) # Excel 工作表名最长 31 字符 # 写入表头 header_row headers.get(block_name, list(data_list[0].keys())) for col_idx, h in enumerate(header_row, 1): cell ws.cell(row1, columncol_idx, valueh) cell.font Font(boldTrue) cell.fill PatternFill(start_colorD3D3D3, end_colorD3D3D3, fill_typesolid) cell.alignment Alignment(horizontalcenter) # 写入数据行 for row_idx, data_dict in enumerate(data_list, 2): for col_idx, field in enumerate(header_row, 1): value data_dict.get(field, ) # 对数值字段设置格式 if field in [VM, VA, PL, QL, PG, QG, R, X]: ws.cell(rowrow_idx, columncol_idx, valuefloat(value) if isinstance(value, (int, float)) or str(value).replace(.,).isdigit() else 0.0) ws.cell(rowrow_idx, columncol_idx).number_format 0.0000 else: ws.cell(rowrow_idx, columncol_idx, valuestr(value)) # 自动调整列宽 for col in ws.columns: max_length 0 column col[0].column_letter for cell in col: try: if len(str(cell.value)) max_length: max_length len(str(cell.value)) except: pass adjusted_width min(max_length 2, 50) # 限制最大宽度 ws.column_dimensions[column].width adjusted_width wb.save(output_path)注意ws.cell(row1, columncol_idx)显式指定行列比ws.append()更可控number_format 0.0000确保VM列显示为1.0000而非1min(..., 50)防止列宽爆炸影响可读性。3.2 处理 ID 字段保留前导/尾随空格与特殊字符BPA 中ID字段常为1 带空格或G1用于区分同一母线的多个发电机。若用str.strip()写入 Excel1 会变成1导致后续潮流计算找不到设备。必须原样保留# 在写入数据行时对 ID 字段特殊处理 if field ID: raw_id data_dict.get(field, ) # 保持原始字符串不 strip ws.cell(rowrow_idx, columncol_idx, valueraw_id) else: # 其他字段正常处理 ...3.3 添加元数据工作表记录转换信息与原始文件头用户需要知道 Excel 是谁、何时、用什么参数生成的。在 Excel 中添加METADATA工作表写入转换时间、Python 版本、脚本路径原始 SWI 文件的前 10 行含注释确认版本各块数据行数统计。ws_meta wb.create_sheet(titleMETADATA) meta_data [ [转换时间, datetime.now().strftime(%Y-%m-%d %H:%M:%S)], [Python 版本, sys.version], [脚本路径, os.path.abspath(__file__)], [原始文件, os.path.abspath(input_path)], [数据块统计, ], ] for block_name, data_list in blocks.items(): meta_data.append([f{block_name} 行数, len(data_list)]) for idx, row in enumerate(meta_data, 1): for col_idx, val in enumerate(row, 1): ws_meta.cell(rowidx, columncol_idx, valueval)4. 批量处理与命令行封装让同事一键运行单个文件转换只是起点。实际工作中需处理winter.swi,summer.swi,contingency_01.swi等数十个文件。手动改脚本路径不现实必须做成命令行工具支持通配符和输出目录指定。4.1 用 argparse 构建命令行接口import argparse import glob import os def main(): parser argparse.ArgumentParser(descriptionConvert BPA SWI files to Excel) parser.add_argument(input_pattern, helpInput SWI file pattern, e.g., *.swi or case_*.swi) parser.add_argument(-o, --output-dir, defaultexcel_output, helpOutput directory for Excel files (default: excel_output)) parser.add_argument(--no-metadata, actionstore_true, helpSkip writing METADATA sheet) args parser.parse_args() # 解析输入模式支持通配符 input_files glob.glob(args.input_pattern) if not input_files: print(fError: No files match pattern {args.input_pattern}) return # 创建输出目录 os.makedirs(args.output_dir, exist_okTrue) for swi_file in input_files: try: print(fProcessing {swi_file}...) blocks parse_swi_file(swi_file) # 此函数整合 2.x 节逻辑 output_xlsx os.path.join(args.output_dir, os.path.basename(swi_file).replace(.swi, .xlsx)) create_excel_from_blocks(blocks, output_xlsx, include_metadatanot args.no_metadata) print(f✓ Saved to {output_xlsx}) except Exception as e: print(f✗ Failed on {swi_file}: {e}) if __name__ __main__: main()提示glob.glob支持*.swi和data/**/case_*.swi需加recursiveTrueos.makedirs(..., exist_okTrue)避免目录已存在报错错误捕获确保一个文件失败不影响其余。4.2 参数配置表不同 BPA 版本的字段偏移微调BPA V35 与 V37 的TRANSFORMER块字段顺序略有差异。硬编码切片不灵活应提供配置文件swi_config.yamlBUS: columns: I: [0, 4] ID: [4, 8] AREA: [8, 12] ZONE: [12, 16] VM: [16, 22] VA: [22, 28] LOAD: columns: I: [0, 4] ID: [4, 8] PL: [16, 22] # V35 中 PL 在 16-22 列V37 可能在 18-24解析时动态加载import yaml def load_config(config_path: str swi_config.yaml) - dict: if os.path.exists(config_path): with open(config_path, r, encodingutf-8) as f: return yaml.safe_load(f) else: # fallback to default config return { BUS: {columns: {I: [0,4], ID: [4,8], ...}}, ... } # 在 parse_bus_line 中使用 config load_config() bus_cols config[BUS][columns] I: int(line[bus_cols[I][0]:bus_cols[I][1]].strip()) if ...5. 验证转换结果三步交叉校验法确保数据零丢失生成 Excel 后不能只看“有没有表”必须验证字段值、行数、顺序是否与原始 SWI 一致。我用以下三步法5 分钟内完成校验5.1 行数与块结构一致性检查用命令行快速统计原始 SWI 中各块行数排除注释和空行# Linux/macOS grep -n BUS\|^$ case.swi | head -20 # 查看 BUS 块起始行号 awk /^BUS$/{bNR} /^0 \/ END OF BUS DATA/{print BUS rows:, NR-b-1} case.swi # 或用 Python 一行统计 python -c import re; c0; [c:c1 for l in open(case.swi) if re.match(r^BUS$|^C|^$,l.strip())None]; print(BUS data lines:,c)对比 Excel 中BUS工作表的行数不含表头。差值为 0 才算通过。5.2 关键字段抽样比对用 pandas 快速验证数值精度对VM、PL等数值字段抽样 5 行比对import pandas as pd # 读取 Excel 中 BUS 表 df_excel pd.read_excel(case.xlsx, sheet_nameBUS, usecols[I,VM]) # 用相同逻辑解析原始 SWI 的 BUS 块不走 openpyxl只取数值 swi_values [] for line in bus_lines: # bus_lines 是从 SWI 提取的原始数据行列表 vm_val float(line[16:22].strip()) if line[16:22].strip() else 0.0 swi_values.append({I: int(line[0:4].strip()), VM: vm_val}) df_swi pd.DataFrame(swi_values) # 合并比对 merged df_excel.merge(df_swi, onI, suffixes(_excel, _swi)) merged[diff] abs(merged[VM_excel] - merged[VM_swi]) print(merged[merged[diff] 1e-5]) # 显示误差 0.00001 的行5.3 ID 字段完整性检查确认空格与大小写未被破坏在 Excel 中用公式EXACT(A2,TRIM(A2))检查ID列是否有前导/尾随空格返回FALSE表示有空格。或用 Python# 读取 Excel ID 列 df_id pd.read_excel(case.xlsx, sheet_nameBUS, usecols[ID], dtypestr) # 检查是否有空格 has_space df_id[ID].str.contains(r^\s|\s$, naFalse).any() print(ID contains leading/trailing space:, has_space) # 检查原始 SWI 中对应行的 ID 是否一致 original_ids [line[4:8] for line in bus_lines] # 原始 4 字符切片 excel_ids df_id[ID].tolist() print(First 3 IDs match:, original_ids[:3] excel_ids[:3])提示EXACT函数区分大小写与空格比更严格str.contains(r^\s|\s$)正则匹配行首或行尾空格比对前 3 行足够发现切片偏移错误。本文还有配套的精品资源点击获取