在 BiSheng 里做 XLSX
0. 开工纪律
这一轮只读文档,不要在同一轮里并行调用别的工具。 读完本文件(必要时再读 references)之后, 下一轮才开始动手。把「读 SKILL.md」和「产出交付物」放进同一轮并行调用,等于技能没读。
本技能要求已勾选代码执行器(bisheng_code_interpreter)。 没有它就无法生成 .xlsx ——
直接告诉用户「请在工具里勾选代码执行器后重试」,不要用 Markdown 表格顶替。
1. 环境事实(照做,不要试探)
| 项 | 事实 |
|---|---|
| 生成方式 | 只有 openpyxl(后端 pyproject.toml 的正式依赖),import openpyxl 直接可用 |
| 公式重算 | 靠 LibreOffice(soffice)。openpyxl 写出的公式没有值,不重算等于交了张空表 |
| 不存在的东西 | markitdown、Node/npm、defusedxml、pdftoppm、zip/unzip 命令 |
xlsxwriter |
在,但只能新建、不能读改已有文件,也不能和 openpyxl 混用 → 一律用 openpyxl |
| 其它可用库 | pandas/numpy、Pillow、PyMuPDF(fitz)、matplotlib、python-docx、python-pptx、lxml |
| 禁止 | pip install、npm install、任何联网假设(生产多为离线内网) |
| 工作目录 | 执行器 cwd = 工作区根,一律用相对路径 |
output/ |
唯一交付区。每次运行自动创建 |
scratch/ |
中间产物区,不会交付,需自己 os.makedirs;跨轮追问时会丢 |
uploads/ |
用户上传的原件在这里。跨轮追问时和 output/ 一起被带进新会话 |
skills/bisheng-xlsx/ |
本技能包,脚本和参考资料在这里,只读 |
| 绝对禁止 | 写 /output/xxx.xlsx 这种带前导斜杠的路径 —— 文件被静默丢弃,用户拿不到 |
| 单次执行上限 | 本地执行器 600 秒;E2B 沙箱 300 秒。构建、重算、体检分多次调用 |
| 日志规则 | 二选一:returncode != 0 只回 stderr(stdout 整段丢弃);returncode == 0 只回 stdout(stderr 整段丢弃)→ 一切诊断用 print(),且子进程的 stdout 和 stderr 都要各自打印一遍 |
| 可见性 | exitcode 0 时执行器把本轮新建/修改的文件同步进工作区,之后 ls/read_file 一般能看到。但判断是否产出以 exitcode 0 + 日志为准,不要因为一次 ls 没看到就重做 |
| 轮次 | 最后两轮工具收窄到 write_file/edit_file/导出,代码执行器会被拦掉 → 表要尽早产出,不要拖到收尾 |
| E2B 沙箱 | 沙箱模式下 skills/ 结构性不可见(copy-in 快照早于技能物化)→ 调包内脚本必 FileNotFoundError。一旦报这个错,就改走纯 openpyxl 内联写法:不调本包脚本,公式改成 Python 算好写数值,并在收尾里说明没做重算与体检 |
1.1 先探一次环境(第一次执行代码时顺手做,只花一轮)
import shutil
try:
import openpyxl
print("openpyxl OK", openpyxl.__version__)
except ImportError:
print("openpyxl MISSING")
print("soffice:", shutil.which("soffice") or shutil.which("libreoffice") or "无 —— 公式无法重算,见 §4 降级方案")
openpyxl MISSING:正常部署不会出现。不要pip install(共享的离线环境,装了会污染所有租户)。 如实告诉用户环境缺依赖,需要运维补装。soffice没有、或重算时报「没有 Calc 组件」:见 §4 的降级方案,不要假装重算过了。
2. 选路线
| 情况 | 做法 |
|---|---|
| 用户给了数据(或让你先查再整理),要一张新表 | §3 从零构建 |
| 用户上传了 .xlsx 要改内容 / 填数 | §5 改已有文件(先读懂它的约定,再动手) |
| 用户上传了 .csv/.xls 要清洗成规范表 | pandas 读进来清洗 → 按 §3 写出 |
| 用户要的是图表图片、Word、PPT | 不属于本技能 |
3. 从零构建
第 1 步 · 定结构。先想清楚分几个表、每个表的列。复杂测算把假设单独放一个表。
草稿写 scratch/,不要写进 output/ —— 交付物按类型排序,.md/.docx/.pdf/.html 排在
.xlsx 前面,所以 output/ 里只要有一个 .md,它就永远压过你的表成为用户看到的头条交付物。
不是「同一轮才会」,是只要在就会。
第 2 步 · 写构建脚本。用 write_file 把完整脚本写到 scratch/build_sheet.py,
不要把整段代码塞进代码执行器的参数里 —— 参数过长会被截断,导致反复重试却总差一截。
写法读 /skills/bisheng-xlsx/references/openpyxl-cookbook.md(表格、样式、数字格式、公式、
图表、批注都有可直接抄的片段),规范读 /skills/bisheng-xlsx/references/design-zh.md。
第 3 步 · 执行:
import subprocess, sys
r = subprocess.run([sys.executable, "scratch/build_sheet.py"], capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
为什么不直接写
python scratch/build_sheet.py:PATH 里的python未必是后端那个解释器, 用sys.executable才能保证跑在装了 openpyxl 的环境里。下面所有脚本调用都用这个写法。
第 4 步 · 重算(有公式就必做),见 §4。
第 5 步 · 体检并返修,见 §6。返修用 edit_file 定点改 scratch/build_sheet.py 再重跑,
不要每次重写整份脚本。
4. 重算公式(有公式就必做)
openpyxl 写出的公式只是字符串,没有结果。不重算的话,pandas、data_only=True、
以及多数预览器读到的全是空 —— 用户打开看到的是一张有公式没数字的表。
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/recalc_check.py", "output/xxx.xlsx"],
capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
LibreOffice 会算完所有公式、就地重写文件,然后脚本回读并列出所有错误单元格。
一次重算连冷启动约 10–30 秒;超时就加 --timeout 120。
看结论:
重算完成—— 公式都能算出结果。这不是交付条件,还要过 §6 的体检。重算未通过+ 错误单元格清单 —— 逐个改完再重算。[未重算]—— 环境问题(没有 soffice、或只装了 writer 没装 calc、或超时)。这时不要硬撑: 改成在 Python 里把数算好直接写数值,并在表里用一列文字说明计算口径, 同时如实告诉用户「本环境无法重算公式,已改为写入计算结果,修改输入不会自动重算」。
能算 ≠ 算对。 区间差一行、引用错行,照样是一张干净的错数字表。 铺开整张表之前,先写 2–3 个关键公式,重算一次,肉眼核对结果符不符合预期。
5. 改用户上传的表
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/inspect_workbook.py",
"uploads/原表.xlsx", "--content-only"], capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
它按单元格坐标打印内容和公式(这是 markitdown 在本环境的替代品,而且比它多给坐标,
所以可以据此规划改哪一格)。然后:
- 原表的约定压倒本技能的一切规范 —— 它用什么字体、什么数字格式、什么配色,就跟着它。
- 先找到它的输入格(通常有独特的字色或填充),只在那里写值,不要动任何已有公式。
- 用默认方式
load_workbook(path)打开(不要加data_only=True,那样保存会把公式全变成字面量)。 .xlsm要keep_vba=True,否则宏全丢。- 改完另存到
output/,不要覆盖uploads/里的原件。 - 不要给别人的表加示例行。
6. 交付前体检(必做)
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/inspect_workbook.py", "output/xxx.xlsx"],
capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
输出分两段:
- 内容:逐表逐格打印,公式旁边跟着重算后的值。用它核对数字对不对、有没有缺漏。
- 体检:ERROR 必须修完再交付;WARN 逐条复核;INFO 是建议。覆盖:禁用函数(含
_xlfn.前缀伪装)、_xlfn.缺前缀、跨表引用引号、百分比存成文本、百分比存整数、数字带单位存成文本、 公式无缓存值、重算后仍是错误值、缺表头、列宽不足导致###、合并区丢值、年份千分位、 残留占位符、硬编码系数、除法未保护。 同一条规则在同一列命中多次时只列 3 例 + 一句「同类还有 N 处」,按同样办法一次改完。
改完重新生成 → 重算 → 再体检,直到最后一行是 结论: 通过。
只有 ERROR 挡交付;结论: 通过 之后仍要把 WARN 逐条看一遍,确实无碍再交付。
7. 公式红线(体检会拦,但你一开始就别写)
- ❌
XLOOKUP/XMATCH/SORT/FILTER/UNIQUE/SEQUENCE/LET/LAMBDA—— openpyxl 写不出溢出元数据,LibreOffice 要么报#NAME?,要么只填左上角一个值 而且重算报 0 错误。查表用INDEX/MATCH;排序、去重、筛选在 Python 里做完再写值。 加_xlfn.前缀救不了这一组:=_xlfn.SORT(...)实测照样#NAME?,体检也照样拦。 - ⚠️
TEXTJOIN/CONCAT/IFS/SWITCH/MAXIFS/MINIFS必须带_xlfn.前缀, 裸写 →#NAME?。 - ⚠️ 表名含空格的跨表引用必须加单引号:
='假设 输入'!$B$5。用sheet_ref()自动处理。 - ⚠️ 百分比存小数(0.15 = 15%)。用
set_pct(),它会在你传 15 时直接报错。
8. 交付纪律
output/里只放最终的.xlsx。草稿、构建脚本、中间版本一律放scratch/。- 文件名用有意义的中文名,如
output/2024年度经营分析.xlsx。 - 收尾时如实说明做了什么、几个表、多少行、公式是否已重算。 不要声称生成了实际不存在的文件。
- 如果因为环境限制改成了写死数值,必须在收尾里明确说出来。
9. 绝不要做的事
- ❌ 跑
markitdown读表 —— 环境里没有,用 §5 的--content-only。 - ❌
pip install任何东西。 - ❌ 用绝对路径
/output/...。 - ❌ 有公式却不重算就交付。
- ❌ 把表降级成 Markdown 表格或 CSV 交付。用户要的是 .xlsx。
- ❌
load_workbook(..., data_only=True)之后save()—— 会永久毁掉所有公式。 - ❌ 因为一次
ls没看到刚生成的文件就重做一遍 ——exitcode 0+ 日志就是产出的凭据。 - ❌ 把百分比、金额写成
"15%"、"1,250,000"这种字符串 —— 不参与求和,体检会拦。