10 KiB
name, description
| name | description |
|---|---|
| excel-report-converter | 当用户需要创建Python脚本将报表形式的Excel数据转换为结构化数据表时使用此技能。当用户提到:将Excel报表转换为数据表、解析Excel报表结构、创建Excel数据转换脚本、从打印模板格式提取数据、或者有类似"请购单维护"、"订单打印模板"等报表格式需要转换为标准表格时,即使没有明确说"创建脚本",也应该触发此技能。这个技能专门处理那些面向打印/展示的报表布局(数据分散在多行多列、有表头/明细/页脚区块)到标准关系型数据表的转换。 |
Excel报表数据转换技能
这个技能帮助你创建Python脚本,将各种报表形式的Excel数据转换为标准的数据表格式。
适用场景
当你遇到以下情况时,使用此技能:
- 报表式布局:数据不是标准的行列表格,而是分散在多个区域
- 有表头/明细/页脚结构:一个文档包含主信息、明细列表、汇总信息
- 打印模板格式:为打印设计的Excel文件,需要提取其中的数据
- 多区块重复:同一个Excel文件包含多个相同格式的报表区块
- 字段映射复杂:目标字段与源单元格位置有复杂的对应关系
- 大数据量提取(性能敏感):需要快速提取数千行以上、包含大量单元格的复杂Excel报表
🚀 性能优化核心技术 (提速法则)
在处理中大型Excel报表时,传统的 openpyxl 逐个单元格读取方法会导致极严重的性能瓶颈。本技能强制采用以下高阶优化方案:
- 空间换时间(内存二维数组):禁止在循环中频繁调用
sheet.cell(row, col).value。必须使用sheet.iter_rows(values_only=True)将整表数据一次性读入 Python 的List[List]中。后续所有的查找全部基于内存列表索引进行,速度可提升数十倍。 - 预先构建映射字典:将
column_index_from_string(字母转数字索引) 等固定操作移出循环,在脚本初始化时预先计算好映射字典。 - Pandas 向量化降维打击:在保存 Excel 自动调整列宽时,放弃
openpyxl的逐单元格遍历,直接利用 Pandas 的底层 C 语言级别操作df[col].astype(str).map(len).max()秒算列宽。 - 避开
read_only的“公式陷阱”:绝不能为了加载速度盲目开启read_only=True,这会导致由 Excel 隐式公式(如自动递增序号=A1+1)生成的值返回None。必须坚持使用默认加载模式配合data_only=True,然后依靠“内存二维数组”来解决速度问题。
工作流程
第一步:分析源Excel文件结构并构建内存视图
使用openpyxl读取Excel文件,并立即将其转换为内存二维数组以提升后续处理速度:
import openpyxl
from typing import List, Any
# 1. 加载工作簿 (保留 data_only=True, 弃用 read_only=True 保证公式值完整)
wb = openpyxl.load_workbook(file_path, data_only=True)
sheet = wb.active
# 2. 【核心提速机制】将整表数据一次性抽取为 Python 二维数组
# 填充一行 [None],并为每一行填充一列 [None],使后续列表索引(1-based)与Excel坐标严格对齐
excel_data = [[None]]
for row in sheet.iter_rows(values_only=True):
excel_data.append([None] + list(row))
wb.close() # 释放文件句柄
# 3. 辅助读取函数 (替代慢速的 sheet.cell().value)
def get_val(data: List[List[Any]], row: int, col: int) -> Any:
try:
if row < len(data) and col < len(data[row]):
return data[row][col]
except IndexError:
pass
return None
第二步:识别报表区块
大多数报表式Excel有以下特征,按优先级在 excel_data 中进行检测:
| 特征 | 检测方法 | 示例 |
|---|---|---|
| 区块标题 | A列包含特定关键词 | "请购单维护"、"订单" |
| 表头标签 | 冒号结尾的标签 | "请购单号:"、"日期:" |
| 明细表头 | 包含列名的行 | "行号"、"物料编码"、"数量" |
| 数据行 | 标签行下方连续的数据 | 非空的具体数据值 |
| 页脚行 | 包含"制单"、"审批"等 | "制单人:"、"审批人:" |
识别规则:
-
区块开始:通常在A列,包含报表类型名称
-
表头区域:区块开始后3-6行,包含带冒号的字段标签
-
明细表头:表头区域后,A列包含"行号"或类似列名
-
明细数据:明细表头后,连续的非空行
-
页脚区域:明细数据后,包含签名/审批信息
第三步:设计数据结构
根据识别结果,设计输出表结构:
扁平化单表
-
每条明细记录携带完整的表头信息
-
适合数据分析和导出
第四步:生成转换脚本
基于分析结果,生成包含以下函数的Python脚本:
Python
# 必需的核心函数
def find_sections(excel_data)
"""识别所有报表区块的起始行"""
def extract_header_data(excel_data, start_row)
"""提取表头信息"""
def extract_line_items(excel_data, header_row, section_end)
"""提取明细行数据"""
def extract_footer_data(excel_data, line_items_end)
"""提取页脚信息"""
def parse_section(excel_data, start_row, next_section)
"""解析单个报表区块"""
def parse_excel_file(file_path)
"""读取Excel文件,转换为二维数组并解析所有区块"""
def save_to_excel(data_df, output_path)
"""保存转换结果,利用Pandas向量化计算列宽"""
字段提取模式
模式1:固定偏移量(利用预计算)
当表头字段位置固定时使用:
Python
# 提取表头信息 (从内存数组快速读取)
def extract_header_data(excel_data, start_row):
header = {}
header['请购单号'] = get_val(excel_data, start_row + 3, 2) # Row+3, B列(2)
return header
模式2:标签查找
当字段位置不固定但标签唯一时使用:
Python
def find_field_by_label(excel_data, label, start_row, search_range=10):
"""通过标签查找字段位置"""
max_row = len(excel_data) - 1
for row in range(start_row, min(start_row + search_range, max_row + 1)):
# 假设标签在前10列中
for col in range(1, min(11, len(excel_data[row]))):
cell_value = get_val(excel_data, row, col)
if cell_value and label in str(cell_value):
# 返回值的位置(通常在标签的右侧)
return get_val(excel_data, row, col + 1)
return None
明细行提取策略
策略:预计算列映射字典 (极速匹配)
避免在双重循环中调用 column_index_from_string。
Python
from openpyxl.utils import column_index_from_string
LINE_ITEM_COLUMNS = {
'A': '行号', 'B': '排产号', 'C': '物料编码'
}
# 全局预计算列索引
LINE_ITEM_COLUMNS_IDX = {
column_index_from_string(col): field
for col, field in LINE_ITEM_COLUMNS.items()
}
def extract_line_items(excel_data, header_row, section_end):
line_items = []
for row in range(header_row + 1, section_end):
# ... 判断跳出逻辑 ...
item = {}
for col_num, field_name in LINE_ITEM_COLUMNS_IDX.items():
item[field_name] = get_val(excel_data, row, col_num)
if any(item.values()):
line_items.append(item)
return line_items
处理特殊情况
1. 空区块处理
当某个区块没有明细数据时,仍需创建一条记录:
Python
if not line_items:
# 创建一条空记录,保留表头和页脚信息
record = {**header_data, **footer_data}
for field in LINE_ITEM_COLUMNS.values():
record[field] = None
flat_records.append(record)
输出格式
Excel格式(结合 Pandas 向量化提速)
Python
from openpyxl.utils import get_column_letter
def save_to_excel(data_df, output_path):
"""保存为Excel文件并极速调整列宽"""
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
data_df.to_excel(writer, sheet_name='转换结果', index=False)
worksheet = writer.sheets['转换结果']
# 【提速】利用 Pandas 的向量化操作一次性算出最大列宽
for idx, col in enumerate(data_df.columns):
max_len = max(data_df[col].astype(str).map(len).max() if not data_df.empty else 0, len(str(col)))
adjusted_width = min(max_len + 2, 50)
col_letter = get_column_letter(idx + 1)
worksheet.column_dimensions[col_letter].width = adjusted_width
依赖库
脚本需要以下依赖,确保在运行前安装:
Bash
pip install openpyxl pandas
验证清单
生成脚本后,验证以下内容:
- 成功识别所有报表区块
- 表头字段提取正确
- 明细行数据完整
- 页脚信息准确
- 空区块得到正确处理
- 输出文件格式正确
- 数据类型准确(日期、数字等)
- 没有重复或遗漏的记录
调试技巧
当脚本出现问题时:
- 打印中间结果:在每个函数中添加print语句,查看提取的数据
- 检查单元格值:确认openpyxl读取的值与预期一致
- 验证索引:确保行号和列号计算正确
- 分步测试:先测试单个区块,确认正确后再处理全部
- 对比原文件:在Excel中查看原始数据和提取结果的差异
常见问题
Q: 为什么提取时有些序列号或公式计算的值变成了 None?
A: 这是因为在 openpyxl 中错误开启了 read_only=True 模式,导致依靠 Excel 公式生成的值无法正确读取缓存。解决方案:去掉 read_only=True,只保留 data_only=True,并使用二维数组提取法来保障速度。
Q: 输出的数据需要进一步处理怎么办?
A: 脚本生成后,可以在将其转换为 Pandas DataFrame 之后,利用 Pandas 强大的生态添加数据清洗、验证、格式转换(如日期格式化)等功能。
最佳实践
-
绝对优先使用内存二维数组:直接摒弃
sheet.cell().value的传统思维,这是报表转换脚本能商用的性能基石。 -
预先计算,拒绝重复:所有能够确定位置或索引关系的映射字典,全部放在全局作用域一次性计算完成。
-
先分析,后编码:花时间理解报表结构和分页/分块标识,比直接编码更高效。
-
异常容错机制:数据提取行要进行
if any(item.values())判断,过滤纯空行;对于越界索引使用try-except包裹。