290 lines
10 KiB
Markdown
290 lines
10 KiB
Markdown
---
|
||
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` 包裹。 |