最近在做审计平台的底稿功能嘛,有个需求是把Excel文件按工作表拆开我让AI帮忙写了个脚本,经过多轮迭代基本能用
它的作用很简单:
一键批量拆:把文件夹中的Excel文件按工作表,自动拆成单个文件
自动归类:这些拆分出来的新文件,会统一放到一个以原Excel文件名命名的文件夹里,整整齐齐的。
处理公式:原来表格里那些引用其他表的公式,脚本会自动把它们转换成静态数值,不用担心链接丢失或报错。
格式:保留原始文件的字体、边框、颜色、合并单元格等格式
下面是AI生成的使用说明和源代码
Excel工作表拆分工具 - 内存优化修复版本
此脚本可以将指定文件夹内的Excel工作簿按工作表进行拆分,每个工作表会被保存为独立的Excel文件。内存优化修复版本能够正确处理跨工作表引用(包括中文工作表名),将公式转换为静态值,确保拆分后的工作表完全独立,并针对大量文件处理进行了内存优化。
功能特点
- • 支持多种Excel格式:
.xlsx, .xlsm - • 正确处理跨工作表引用:将
='Sheet Name'!A1 或 =表头!A2 等引用转换为计算后的静态值 - • 保留原始Excel格式(字体、边框、合并单元格等)
- • 内存优化:针对大量Excel文件批量处理进行了内存优化,避免内存溢出
- • 分批处理:支持批量处理模式,控制同时处理的文件数量
依赖库
使用方法
1. 安装依赖
# 使用虚拟环境(推荐)
python3 -m venv excel_splitter_env
source excel_splitter_env/bin/activate # 在Windows上使用 excel_splitter_env\Scripts\activate
pip install openpyxl
或者直接安装(可能会影响系统Python环境):
pip install openpyxl --break-system-packages
2. 运行脚本
python3 excel_splitter_optimized_fixed.py <输入文件夹路径> [输出文件夹路径] [--batch-size N]
示例:
# 基本用法
python3 excel_splitter_optimized_fixed.py ./input_folder
# 指定输入和输出文件夹
python3 excel_splitter_optimized_fixed.py ./input_folder ./output_folder
# 指定批量处理大小(控制内存使用)
python3 excel_splitter_optimized_fixed.py ./input_folder ./output_folder --batch-size 5
输出结构
脚本会为每个Excel文件创建一个以其文件名命名的子文件夹,然后将该文件的所有工作表拆分并保存到该子文件夹中。
输出文件夹/
├── 原文件名1/
│ ├── 工作表1.xlsx
│ ├── 工作表2.xlsx
│ └── 工作表3.xlsx
├── 原文件名2/
│ ├── 工作表A.xlsx
│ └── 工作表B.xlsx
示例
假设您有一个名为 Reports.xlsx 的文件存放在input_folder文件夹中,其中包含 "Sales", "Inventory", "Employees" 三个工作表:
运行命令:
python3 excel_splitter_optimized_fixed.py ./input_folder ./output_folder
输出结果:
output_folder/
└── Reports/
├── Sales.xlsx
├── Inventory.xlsx
└── Employees.xlsx
Python代码
#!/usr/bin/env python3
"""
内存优化版Excel拆分工具 - 修复版
专门针对大量Excel文件的批量处理进行内存和性能优化,并修复中文工作表名识别问题
"""
import os
import gc
import re
import sys
from pathlib import Path
from openpyxl import load_workbook
from openpyxl.workbook import Workbook
from openpyxl.utils import get_column_letter
import argparse
import time
def is_cross_sheet_reference(formula):
"""
高级跨工作表引用检测算法 - 修复版
支持中文工作表名
"""
if not formula or not formula.startswith('='):
return False
formula_content = formula[1:]
# 修复:添加对中文字符的支持
patterns = [
# 基本跨工作表引用(支持中文工作表名)
r'[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![A-Z]+[0-9]+',
# 带引号的工作表名(包括中文)
r"'[^']*'![A-Z]+[0-9]+",
# 跨工作表范围引用(支持中文工作表名)
r'[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![A-Z]+[0-9]+:[A-Z]+[0-9]+',
# 带引号的工作表范围引用
r"'[^']*'![A-Z]+[0-9]+:[A-Z]+[0-9]+",
# 多工作表范围引用(支持中文工作表名)
r'[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+:[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![A-Z]+[0-9]+',
# 跨工作表函数调用(支持中文工作表名)
r'SUM\([^)]*[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![^)]*\)',
r'AVERAGE\([^)]*[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![^)]*\)',
r'COUNT\([^)]*[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![^)]*\)',
r'(VLOOKUP|HLOOKUP|INDEX|MATCH|SUMIF|COUNTIF|AVERAGEIF)\([^)]*[A-Z0-9_\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u3300-\u33ff\ufe30-\ufe4f\uf900-\ufaff\u2f800-\u2fa1f]+![^)]*\)',
]
for pattern in patterns:
try:
if re.search(pattern, formula_content, re.IGNORECASE):
return True
except re.error:
# 如果正则表达式有错误,跳过并尝试下一个
continue
return False
def sanitize_sheet_name(name):
"""
清理工作表名称,确保符合Excel要求
"""
sanitized = re.sub(r'[\\/:*?"<>|]', '_', name)
if len(sanitized) > 31:
sanitized = sanitized[:31]
if not sanitized.strip():
sanitized = "Sheet"
return sanitized
def process_single_workbook_optimized_fixed(file_path, output_subfolder, progress_callback=None):
"""
内存优化的单个工作簿处理函数 - 修复版
逐个工作表处理,避免将整个工作簿加载到内存中
"""
print(f"开始预处理: {file_path.name}")
# 使用data_only模式和公式模式分别加载,以获取计算值
try:
wb_formula = load_workbook(file_path, keep_vba=False)
wb_values = load_workbook(file_path, data_only=True, keep_vba=False)
except Exception as e:
print(f"加载工作簿失败 {file_path.name}: {str(e)}")
return 0
processed_count = 0
for sheet_name in wb_formula.sheetnames:
# 获取工作表
ws_formula = wb_formula[sheet_name]
ws_values = wb_values[sheet_name]
print(f" - 处理工作表: {sheet_name}")
# 创建新工作表名称
new_sheet_name = sanitize_sheet_name(sheet_name)
output_file_path = os.path.join(output_subfolder, f"{new_sheet_name}.xlsx")
# 创建新的工作簿用于保存当前工作表
new_workbook = Workbook()
new_workbook.remove(new_workbook.active) # 移除默认工作表
new_worksheet = new_workbook.create_sheet(title=new_sheet_name)
# 计算最大行列数
max_row = ws_formula.max_row
max_col = ws_formula.max_column
# 处理单元格 - 逐个处理以节省内存
converted_refs = 0
for row_idx in range(1, max_row + 1):
for col_idx in range(1, max_col + 1):
formula_cell = ws_formula.cell(row=row_idx, column=col_idx)
# 检查是否是跨工作表引用的公式
if formula_cell.data_type == 'f' and formula_cell.value:
formula_str = str(formula_cell.value)
if is_cross_sheet_reference(formula_str):
# 使用计算后的值替换公式
calculated_value = ws_values.cell(row=row_idx, column=col_idx).value
new_worksheet.cell(row=row_idx, column=col_idx, value=calculated_value)
converted_refs += 1
# 调试:打印转换的单元格
print(f" - 转换跨工作表引用: {sheet_name}!{get_column_letter(col_idx)}{row_idx} -> {calculated_value}")
else:
# 保留非跨工作表引用的公式
new_worksheet.cell(row=row_idx, column=col_idx, value=formula_cell.value)
else:
# 复制非公式单元格
new_worksheet.cell(row=row_idx, column=col_idx, value=formula_cell.value)
# 复制格式
if formula_cell.has_style:
try:
new_worksheet.cell(row=row_idx, column=col_idx).font = formula_cell.font.copy()
new_worksheet.cell(row=row_idx, column=col_idx).border = formula_cell.border.copy()
new_worksheet.cell(row=row_idx, column=col_idx).fill = formula_cell.fill.copy()
new_worksheet.cell(row=row_idx, column=col_idx).number_format = formula_cell.number_format
new_worksheet.cell(row=row_idx, column=col_idx).protection = formula_cell.protection.copy()
new_worksheet.cell(row=row_idx, column=col_idx).alignment = formula_cell.alignment.copy()
except:
# 如果复制格式失败,继续处理下一个单元格
pass
# 复制列宽
for col in ws_formula.column_dimensions:
new_worksheet.column_dimensions[col].width = ws_formula.column_dimensions[col].width
new_worksheet.column_dimensions[col].hidden = ws_formula.column_dimensions[col].hidden
new_worksheet.column_dimensions[col].outline_level = ws_formula.column_dimensions[col].outline_level
# 复制行高
for row_idx in ws_formula.row_dimensions:
new_worksheet.row_dimensions[row_idx].height = ws_formula.row_dimensions[row_idx].height
new_worksheet.row_dimensions[row_idx].hidden = ws_formula.row_dimensions[row_idx].hidden
new_worksheet.row_dimensions[row_idx].outline_level = ws_formula.row_dimensions[row_idx].outline_level
# 复制合并单元格
for merged_cell_range in ws_formula.merged_cells.ranges:
new_worksheet.merge_cells(str(merged_cell_range))
# 复制左上角单元格的格式到合并区域
top_left_cell = ws_formula[merged_cell_range.coord.split(':')[0]]
merged_range = str(merged_cell_range)
for row in new_worksheet[merged_range]:
for cell in row:
if cell.coordinate != top_left_cell.coordinate:
try:
cell.font = top_left_cell.font.copy()
cell.border = top_left_cell.border.copy()
cell.fill = top_left_cell.fill.copy()
cell.number_format = top_left_cell.number_format
cell.protection = top_left_cell.protection.copy()
cell.alignment = top_left_cell.alignment.copy()
except:
pass
# 保存当前工作表
try:
new_workbook.save(output_file_path)
print(f" - 已保存: {new_sheet_name}.xlsx ({converted_refs} 个跨工作表引用已转换)")
processed_count += 1
except Exception as e:
print(f" - 保存失败: {new_sheet_name}.xlsx - {str(e)}")
# 释放内存
new_workbook.close()
del new_workbook
del new_worksheet
gc.collect() # 强制垃圾回收
# 关闭原始工作簿
wb_formula.close()
wb_values.close()
del wb_formula
del wb_values
gc.collect() # 强制垃圾回收
if processed_count > 0:
print(f" 预处理完成:总共转换了跨工作表引用")
return processed_count
def split_excel_batch_optimized_fixed(input_folder, output_folder=None, batch_size=5):
"""
批量处理Excel文件,优化内存使用 - 修复版
"""
if output_folder is None:
parent_dir = Path(input_folder).parent
output_folder = os.path.join(parent_dir, "拆分输出_优化版_修复")
os.makedirs(output_folder, exist_ok=True)
excel_extensions = ['.xlsx', '.xlsm']
input_path = Path(input_folder)
# 首先获取所有Excel文件
excel_files = []
for file_path in input_path.rglob('*'):
if file_path.suffix.lower() in excel_extensions:
excel_files.append(file_path)
print(f"找到 {len(excel_files)} 个Excel文件")
total_sheets = 0
processed_files = 0
# 批量处理
for i, file_path in enumerate(excel_files):
print(f"\n处理文件 {i+1}/{len(excel_files)}: {file_path.name}")
try:
# 创建输出子文件夹
base_name = file_path.stem
output_subfolder = os.path.join(output_folder, base_name)
os.makedirs(output_subfolder, exist_ok=True)
# 处理单个工作簿
sheet_count = process_single_workbook_optimized_fixed(file_path, output_subfolder)
total_sheets += sheet_count
processed_files += 1
print(f" 完成处理 {file_path.name},拆分出 {sheet_count} 个工作表")
# 每处理完一个文件就进行垃圾回收
gc.collect()
# 显示进度
print(f"进度: {processed_files}/{len(excel_files)} 文件,总计 {total_sheets} 个工作表")
except Exception as e:
print(f"处理文件失败 {file_path.name}: {str(e)}")
continue
print(f"\n处理完成! 共处理了 {processed_files} 个文件,拆分出 {total_sheets} 个工作表")
print(f"输出文件夹: {output_folder}")
return total_sheets
def main():
parser = argparse.ArgumentParser(description='内存优化版Excel拆分工具 - 修复版')
parser.add_argument('input_folder', help='输入文件夹路径')
parser.add_argument('output_folder', nargs='?', help='输出文件夹路径(可选)')
parser.add_argument('--batch-size', type=int, default=5, help='批量处理大小(默认:5)')
args = parser.parse_args()
if not os.path.exists(args.input_folder):
print(f"错误: 输入文件夹 '{args.input_folder}' 不存在")
return
output_folder = args.output_folder
print("🚀 内存优化版Excel拆分工具 - 修复版启动")
print(f"📁 输入文件夹: {args.input_folder}")
print(f"📁 输出文件夹: {output_folder if output_folder else '自动生成'}")
print(f"📦 批量处理大小: {args.batch_size}")
print("-" * 50)
start_time = time.time()
try:
total_sheets = split_excel_batch_optimized_fixed(
args.input_folder,
output_folder,
args.batch_size
)
end_time = time.time()
print(f"⏱️ 总处理时间: {end_time - start_time:.2f} 秒")
except KeyboardInterrupt:
print("\n⚠️ 用户中断处理")
except Exception as e:
print(f"\n❌ 处理过程中出现错误: {str(e)}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()