简介:本资源是一套基于Python实现BPA(Business Process Analyzer)SWI日志文件批量转换为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 做真正可控的格式转换
电力系统仿真工程师常被 BPA(Bonneville 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–4:
I(母线编号,整数) - 列 5–8:
ID(母线标识,字符,常含空格) - 列 9–12:
AREA(区域号) - 列 13–16:
ZONE(分区号) - 列 17–22:
VM(电压幅值,浮点) - 列 23–28:
VA(电压相角,浮点)
这意味着必须用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', encoding='utf-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)按两个以上空格切分,比单空格更鲁棒; - 对缺失字段设默认值(如
VA=0.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 个及以上空格分割,保留字段内单空格(如 ID='1 ') 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(title=block_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(row=1, column=col_idx, value=h) cell.font = Font(bold=True) cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") cell.alignment = Alignment(horizontal="center") # 写入数据行 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(row=row_idx, column=col_idx, value=float(value) if isinstance(value, (int, float)) or str(value).replace('.','').isdigit() else 0.0) ws.cell(row=row_idx, column=col_idx).number_format = '0.0000' else: ws.cell(row=row_idx, column=col_idx, value=str(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(row=1, column=col_idx)显式指定行列,比ws.append()更可控;number_format = '0.0000'确保VM列显示为1.0000而非1;min(..., 50)防止列宽爆炸影响可读性。
3.2 处理 ID 字段:保留前导/尾随空格与特殊字符
BPA 中ID字段常为'1 '(带空格)或'G1',用于区分同一母线的多个发电机。若用str.strip()写入 Excel,'1 '会变成'1',导致后续潮流计算找不到设备。必须原样保留:
# 在写入数据行时,对 ID 字段特殊处理 if field == 'ID': raw_id = data_dict.get(field, "") # 保持原始字符串,不 strip ws.cell(row=row_idx, column=col_idx, value=raw_id) else: # 其他字段正常处理 ...3.3 添加元数据工作表:记录转换信息与原始文件头
用户需要知道 Excel 是谁、何时、用什么参数生成的。在 Excel 中添加METADATA工作表,写入:
- 转换时间、Python 版本、脚本路径;
- 原始 SWI 文件的前 10 行(含注释,确认版本);
- 各块数据行数统计。
ws_meta = wb.create_sheet(title="METADATA") 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(row=idx, column=col_idx, value=val)4. 批量处理与命令行封装:让同事一键运行
单个文件转换只是起点。实际工作中需处理winter.swi,summer.swi,contingency_01.swi等数十个文件。手动改脚本路径不现实,必须做成命令行工具,支持通配符和输出目录指定。
4.1 用 argparse 构建命令行接口
import argparse import glob import os def main(): parser = argparse.ArgumentParser(description="Convert BPA SWI files to Excel") parser.add_argument("input_pattern", help="Input SWI file pattern, e.g., '*.swi' or 'case_*.swi'") parser.add_argument("-o", "--output-dir", default="excel_output", help="Output directory for Excel files (default: excel_output)") parser.add_argument("--no-metadata", action="store_true", help="Skip writing METADATA sheet") args = parser.parse_args() # 解析输入模式,支持通配符 input_files = glob.glob(args.input_pattern) if not input_files: print(f"Error: No files match pattern '{args.input_pattern}'") return # 创建输出目录 os.makedirs(args.output_dir, exist_ok=True) for swi_file in input_files: try: print(f"Processing {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_metadata=not 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(需加recursive=True);os.makedirs(..., exist_ok=True)避免目录已存在报错;错误捕获确保一个文件失败不影响其余。
4.2 参数配置表:不同 BPA 版本的字段偏移微调
BPA V35 与 V37 的TRANSFORMER块字段顺序略有差异。硬编码切片不灵活,应提供配置文件swi_config.yaml:
BUS: 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', encoding='utf-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$/{b=NR} /^0 \/ END OF BUS DATA/{print "BUS rows:", NR-b-1}' case.swi # 或用 Python 一行统计 python -c "import re; c=0; [c:=c+1 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_name="BUS", 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, on='I', 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_name="BUS", usecols=["ID"], dtype=str) # 检查是否有空格 has_space = df_id['ID'].str.contains(r'^\s|\s$', na=False).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 行足够发现切片偏移错误。
本文还有配套的精品资源,点击获取