--- name: excel-report-converter description: 当用户需要创建Python脚本将报表形式的Excel数据转换为结构化数据表时使用此技能。当用户提到:将Excel报表转换为数据表、解析Excel报表结构、创建Excel数据转换脚本、从打印模板格式提取数据、或者有类似"请购单维护"、"订单打印模板"等报表格式需要转换为标准表格时,即使没有明确说"创建脚本",也应该触发此技能。这个技能专门处理那些面向打印/展示的报表布局(数据分散在多行多列、有表头/明细/页脚区块)到标准关系型数据表的转换。 --- # Excel报表数据转换技能 这个技能帮助你创建Python脚本,将各种报表形式的Excel数据转换为标准的数据表格式。 ## 适用场景 当你遇到以下情况时,使用此技能: - **报表式布局**:数据不是标准的行列表格,而是分散在多个区域 - **有表头/明细/页脚结构**:一个文档包含主信息、明细列表、汇总信息 - **打印模板格式**:为打印设计的Excel文件,需要提取其中的数据 - **多区块重复**:同一个Excel文件包含多个相同格式的报表区块 - **字段映射复杂**:目标字段与源单元格位置有复杂的对应关系 - **大数据量提取(性能敏感)**:需要快速提取数千行以上、包含大量单元格的复杂Excel报表 --- ## 🚀 性能优化核心技术 (提速法则) 在处理中大型Excel报表时,传统的 `openpyxl` 逐个单元格读取方法会导致极严重的性能瓶颈。本技能强制采用以下高阶优化方案: 1. **空间换时间(内存二维数组)**:禁止在循环中频繁调用 `sheet.cell(row, col).value`。必须使用 `sheet.iter_rows(values_only=True)` 将整表数据一次性读入 Python 的 `List[List]` 中。后续所有的查找全部基于内存列表索引进行,速度可提升数十倍。 2. **预先构建映射字典**:将 `column_index_from_string` (字母转数字索引) 等固定操作移出循环,在脚本初始化时预先计算好映射字典。 3. **Pandas 向量化降维打击**:在保存 Excel 自动调整列宽时,放弃 `openpyxl` 的逐单元格遍历,直接利用 Pandas 的底层 C 语言级别操作 `df[col].astype(str).map(len).max()` 秒算列宽。 4. **避开 `read_only` 的“公式陷阱”**:绝不能为了加载速度盲目开启 `read_only=True`,这会导致由 Excel 隐式公式(如自动递增序号 `=A1+1`)生成的值返回 `None`。必须坚持使用默认加载模式配合 `data_only=True`,然后依靠“内存二维数组”来解决速度问题。 --- ## 工作流程 ### 第一步:分析源Excel文件结构并构建内存视图 使用openpyxl读取Excel文件,并立即将其转换为内存二维数组以提升后续处理速度: ```python 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列包含特定关键词|"请购单维护"、"订单"| |表头标签|冒号结尾的标签|"请购单号:"、"日期:"| |明细表头|包含列名的行|"行号"、"物料编码"、"数量"| |数据行|标签行下方连续的数据|非空的具体数据值| |页脚行|包含"制单"、"审批"等|"制单人:"、"审批人:"| **识别规则**: 1. 区块开始:通常在A列,包含报表类型名称 2. 表头区域:区块开始后3-6行,包含带冒号的字段标签 3. 明细表头:表头区域后,A列包含"行号"或类似列名 4. 明细数据:明细表头后,连续的非空行 5. 页脚区域:明细数据后,包含签名/审批信息 ### 第三步:设计数据结构 根据识别结果,设计输出表结构: **扁平化单表** - 每条明细记录携带完整的表头信息 - 适合数据分析和导出 ### 第四步:生成转换脚本 基于分析结果,生成包含以下函数的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 ``` ## 验证清单 生成脚本后,验证以下内容: - [ ] 成功识别所有报表区块 - [ ] 表头字段提取正确 - [ ] 明细行数据完整 - [ ] 页脚信息准确 - [ ] 空区块得到正确处理 - [ ] 输出文件格式正确 - [ ] 数据类型准确(日期、数字等) - [ ] 没有重复或遗漏的记录 ## 调试技巧 当脚本出现问题时: 1. **打印中间结果**:在每个函数中添加print语句,查看提取的数据 2. **检查单元格值**:确认openpyxl读取的值与预期一致 3. **验证索引**:确保行号和列号计算正确 4. **分步测试**:先测试单个区块,确认正确后再处理全部 5. **对比原文件**:在Excel中查看原始数据和提取结果的差异 ## 常见问题 **Q: 为什么提取时有些序列号或公式计算的值变成了 `None`?** A: 这是因为在 `openpyxl` 中错误开启了 `read_only=True` 模式,导致依靠 Excel 公式生成的值无法正确读取缓存。**解决方案**:去掉 `read_only=True`,只保留 `data_only=True`,并使用二维数组提取法来保障速度。 **Q: 输出的数据需要进一步处理怎么办?** A: 脚本生成后,可以在将其转换为 Pandas DataFrame 之后,利用 Pandas 强大的生态添加数据清洗、验证、格式转换(如日期格式化)等功能。 ## 最佳实践 1. **绝对优先使用内存二维数组**:直接摒弃 `sheet.cell().value` 的传统思维,这是报表转换脚本能商用的性能基石。 2. **预先计算,拒绝重复**:所有能够确定位置或索引关系的映射字典,全部放在全局作用域一次性计算完成。 3. **先分析,后编码**:花时间理解报表结构和分页/分块标识,比直接编码更高效。 4. **异常容错机制**:数据提取行要进行 `if any(item.values())` 判断,过滤纯空行;对于越界索引使用 `try-except` 包裹。