关于 excel-automation
面向需要创建、解析和操作 Excel 工作簿的文档与财务工作者,提供 Excel 自动化技能。它按文件复杂度选择处理方式,并涵盖 macOS 上的 Excel 窗口控制。
主要功能
- 01创建格式化 Excel 报告
- 02解析复杂 xlsx 和 xlsm
- 03修复损坏的定义名称
- 04通过 AppleScript 控制 Excel
- 05按文件特征选择解析工具
适用场景
- 01制作财务报告和仪表板
- 02提取大型或含 VBA 工作簿的数据
- 03处理定义名称损坏的 xlsm 文件
- 04在 macOS 中调整 Excel 窗口视图
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill excel-automation安装后在 Claude Code 中输入/excel-automation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/formatting-reference.mdprintable-forms.mdscripts/create_formatted_excel.pyparse_complex_excel.py详细介绍
Excel 自动化
创建专业的 Excel 文件、解析复杂财务模型,并在 macOS 上控制 Excel。
如果需要将纸质表单的图像或扫描件转换为可编辑单元格和可打印版式,请在创建工作簿前加载 可打印表单。遵循宿主指定的电子表格编写工具;只有在允许使用该工具时,才使用下方的 openpyxl 示例。
快速入门
在执行会将 Excel 切换到前台或更改其可见窗口的命令之前,请检查用户活动和其他 Agent 的占用情况。仅执行当前任务授权范围内的前台操作。如果界面正在使用、占用情况未知,或出现意外输入,请停止输入,继续进行独立的后台工作。
# Create a formatted Excel report
uv run --with openpyxl scripts/create_formatted_excel.py output.xlsx
# Parse a complex xlsm that openpyxl can't handle
uv run scripts/parse_complex_excel.py model.xlsm # List sheets
uv run scripts/parse_complex_excel.py model.xlsm "DCF" # Extract a sheet
uv run scripts/parse_complex_excel.py model.xlsm --fix # Fix corrupted names
# Control Excel via AppleScript (with timeout to prevent hangs)
timeout 5 osascript -e 'tell application "Microsoft Excel" to activate'
概述
| 能力 | 工具 | 适用场景 |
|---|---|---|
| 创建 格式化的 Excel 文件 | openpyxl | 报告、原型、仪表板 |
| 解析 复杂的 xlsm/xlsx 文件 | zipfile + xml.etree | 财务模型、VBA 工作簿、>1MB 的文件 |
| 控制 Excel 窗口 | AppleScript (osascript) | 以编程方式缩放、滚动、选择单元格 |
工具选择决策树
Is the file simple (data export, no VBA, <1MB)?
├─ YES → openpyxl or pandas
└─ NO
├─ Is it .xlsm or from investment bank / >1MB?
│ └─ YES → zipfile + xml.etree.ElementTree (stdlib)
└─ Is it truly .xls (BIFF format)?
└─ YES → xlrd
“复杂” Excel 的判断依据:文件 >1MB、扩展名为 .xlsm、来自投资银行/经纪商,或包含 VBA 宏。
重要:始终先运行 file <path>——扩展名并不可靠。.xls 文件实际上可能是基于 ZIP 的 xlsx 文件。
创建 Excel 文件(openpyxl)
专业配色惯例(投资银行标准)
按照该惯例为财务报告应用以下颜色。对于纸质表单,应保留其预期的打印外观;默认不要添加输入颜色。
| 颜色 | RGB 代码 | 含义 |
|---|---|---|
| 蓝色 | 0000FF | 用户输入/假设 |
| 黑色 | 000000 | 计算值 |
| 绿色 | 008000 | 跨工作表引用 |
| 深蓝底白字 | 深蓝底 4472C4 上的 FFFFFF | 分节标题 |
| 深蓝色文本 | 1F4E79 | 标题 |
核心格式设置模式
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
# Fonts
BLUE_FONT = Font(color="0000FF", size=10, name="Calibri")
BLACK_FONT_BOLD = Font(color="000000", size=10, name="Calibri", bold=True)
GREEN_FONT = Font(color="008000", size=10, name="Calibri")
HEADER_FONT = Font(color="FFFFFF", size=12, name="Calibri", bold=True)
# Fills
DARK_BLUE_FILL = PatternFill("solid", fgColor="4472C4")
LIGHT_BLUE_FILL = PatternFill("solid", fgColor="D9E1F2")
INPUT_GREEN_FILL = PatternFill("solid", fgColor="E2EFDA")
LIGHT_GRAY_FILL = PatternFill("solid", fgColor="F2F2F2")
# Borders
THIN_BORDER = Border(bottom=Side(style="thin", color="B2B2B2"))
BOTTOM_DOUBLE = Border(bottom=Side(style="double", color="000000"))
数字格式代码
| 格式 | 代码 | 示例 |
|---|---|---|
| 货币 | '$#,##0' | $1,234 |
| 带小数的货币 | '$#,##0.00' | $1,234.56 |
| 百分比 | '0.0%' | 12.3% |
| 百分比(2 位小数) | '0.00%' | 12.34% |
| 带千位分隔符的数字 | '#,##0' | 1,234 |
| 倍数 | '0.0x' | 1.5x |
条件格式(敏感性分析表)
敏感性分析使用红到绿渐变:
from openpyxl.formatting.rule import ColorScaleRule
rule = ColorScaleRule(
start_type="min", start_color="F8696B", # Red (low)
mid_type="percentile", mid_value=50, mid_color="FFEB84", # Yellow (mid)
end_type="max", end_color="63BE7B" # Green (high)
)
ws.conditional_formatting.add(f"B2:F6", rule)
执行
uv run --with openpyxl scripts/create_formatted_excel.py
完整模板脚本:参见 scripts/create_formatted_excel.py
解析复杂 Excel(zipfile + xml)
当 openpyxl 无法处理复杂的 xlsm 文件(DefinedNames 损坏、VBA 复杂)时,直接使用标准库。
XLSX 内部 ZIP 结构
file.xlsx (ZIP archive)
├── [Content_Types].xml
├── xl/
│ ├── workbook.xml ← Sheet names + order
│ ├── sharedStrings.xml ← All text values (lookup table)
│ ├── worksheets/
│ │ ├── sheet1.xml ← Cell data for sheet 1
│ │ ├── sheet2.xml ← Cell data for sheet 2
│ │ └── ...
│ └── _rels/
│ └── workbook.xml.rels ← Maps rId → sheetN.xml
└── _rels/.rels
工作表名称解析
workbook.xml 中的工作表名称通过 _rels/workbook.xml.rels 链接到实际文件:
import zipfile
import xml.etree.ElementTree as ET
MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
def get_sheet_path(zf, sheet_name):
"""Resolve sheet name to physical XML file path inside ZIP."""
# Step 1: workbook.xml → find rId for the sheet name
wb_xml = ET.fromstring(zf.read('xl/workbook.xml'))
sheets = wb_xml.findall(f'.//{{{MAIN_NS}}}sheet')
rid = None
for s in sheets:
if s.get('name') == sheet_name:
rid = s.get(f'{{{REL_NS}}}id')
break
if not rid:
raise ValueError(f"Sheet '{sheet_name}' not found")
# Step 2: workbook.xml.rels → map rId to file path
rels_xml = ET.fromstring(zf.read('xl/_rels/workbook.xml.rels'))
for rel in rels_xml.findall(f'{{{RELS_NS}}}Relationship'):
if rel.get('Id') == rid:
return 'xl/' + rel.get('Target')
raise ValueError(f"No file mapping for {rid}")
单元格数据提取
def extract_cells(zf, sheet_path):
"""Extract all cell values from a sheet XML."""
# Build shared strings lookup
shared = []
try:
ss_xml = ET.fromstring(zf.read('xl/sharedStrings.xml'))
for si in ss_xml.findall(f'{{{MAIN_NS}}}si'):
texts = si.itertext()
shared.append(''.join(texts))
except KeyError:
pass # No shared strings
# Parse sheet cells
sheet_xml = ET.fromstring(zf.read(sheet_path))
rows = sheet_xml.findall(f'.//{{{MAIN_NS}}}row')
data = {}
for row in rows:
for cell in row.findall(f'{{{MAIN_NS}}}c'):
ref = cell.get('r') # e.g., "A1"
cell_type = cell.get('t') # "s" = shared string, None = number
val_el = cell.find(f'{{{MAIN_NS}}}v')
if val_el is not None and val_el.text:
if cell_type == 's':
data[ref] = shared[int(val_el.text)]
else:
try:
data[ref] = float(val_el.text)
except ValueError:
data[ref] = val_el.text
return data
修复损坏的 DefinedNames
投资银行的 xlsm 文件中,经常有损坏的 <definedName> 条目,其中包含 "Formula removed":
def fix_defined_names(zf_in_path, zf_out_path):
"""Remove corrupted DefinedNames and repackage."""
import shutil, tempfile
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
with zipfile.ZipFile(zf_in_path, 'r') as zf:
zf.extractall(tmp)
wb_xml_path = tmp / 'xl' / 'workbook.xml'
tree = ET.parse(wb_xml_path)
root = tree.getroot()
ns = {'main': MAIN_NS}
defined_names = root.find('.//main:definedNames', ns)
if defined_names is not None:
for name in list(defined_names):
if name.text and "Formula removed" in name.text:
defined_names.remove(name)
tree.write(wb_xml_path, encoding='utf-8', xml_declaration=True)
with zipfile.ZipFile(zf_out_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for fp in tmp.rglob('*'):
if fp.is_file():
zf.write(fp, fp.relative_to(tmp))
完整模板脚本:参见 scripts/parse_complex_excel.py
在 macOS 上控制 Excel(AppleScript)
所有命令均已在 macOS 上通过 Microsoft Excel 验证。
已验证的命令
# Activate Excel (bring to front)
osascript -e 'tell application "Microsoft Excel" to activate'
# Open a file
osascript -e 'tell application "Microsoft Excel" to open POSIX file "/path/to/file.xlsx"'
# Set zoom level (percentage)
osascript -e 'tell application "Microsoft Excel"
set zoom of active window to 120
end tell'
# Scroll to specific row
osascript -e 'tell application "Microsoft Excel"
set scroll row of active window to 45
end tell'
# Scroll to specific column
osascript -e 'tell application "Microsoft Excel"
set scroll column of active window to 3
end tell'
# Select a cell range
osascript -e 'tell application "Microsoft Excel"
select range "A1" of active sheet
end tell'
# Select a specific sheet by name
osascript -e 'tell application "Microsoft Excel"
activate object sheet "DCF" of active workbook
end tell'
时序和超时
始终在 AppleScript 命令与后续操作(例如截图)之间添加 sleep 1,以便 UI 完成渲染。
重要:如果 Excel 未运行或没有响应,osascript 会无限期挂起。始终使用 timeout 包装:
# Safe pattern: 5-second timeout
timeout 5 osascript -e 'tell application "Microsoft Excel" to activate'
# Check exit code: 124 = timed out
if [ $? -eq 124 ]; then
echo "Excel not responding — is it running?"
fi
常见错误
| 错误 | 修正方法 |
|---|---|
| openpyxl 无法处理复杂 xlsm 文件 → 尝试猴子补丁 | 立即改用 zipfile + xml.etree |
使用 wc -c 统计汉字 | 使用 wc -m(统计字符而非字节;中文字符 = 3 字节/个) |
| 轻信文件扩展名 | 先运行 file <path>,确认实际格式 |
openpyxl 的 load_workbook 在大型 xlsm 文件上挂起 | 使用 zipfile 有针对性地提取内容,而不是加载整个工作簿 |
重要说明
- 使用
uv run --with openpyxl执行 Python 脚本(切勿使用系统 Python) - LibreOffice(
soffice --headless)可转换格式并重新计算公式 - 详细的格式设置参考:请参阅
references/formatting-reference.md