---
name: office-xlsx
description: 表格文件是主要输入或输出时使用。包括：打开、读取、修改或修复已有的 .xlsx、.xlsm、.xltx、.csv、.tsv 文件（加列、算公式、调格式、画图表、清洗脏数据）；从零或从其他数据来源新建表格；在表格格式之间转换。用户提到某个表格文件名时尤其要用——哪怕说得很随意（比如"我传的那个 Excel"）——只要他要对它做点什么或从它产出东西。把格式混乱的表格数据（错行、表头错位、垃圾数据）整理成规范表格也用它。交付物必须是表格文件。主要交付物是 Word 文档、HTML 报告、单独的 Python 脚本、数据库流程或在线表格接口时，即使涉及表格数据也不要用。
---

# XLSX：生成、修改与分析

| 任务 | 做法 |
|---|---|
| 带公式/格式地**生成**或**修改** | `openpyxl`——坑点见下 |
| **大批量**数据读写 | `pandas`（`read_excel`、`to_excel`） |
| **快速看一眼**某张表 | `markitdown 文件.xlsx`——每张表一个 `## 表名`，也能读 `.xlsm`。没有单元格坐标，不要据此规划修改 |
| **读懂**一个模型（公式**和**值） | `load_workbook` 读两遍——见坑点 |

> `openpyxl`、`pandas`、`markitdown` 都已装好，直接 import。沙箱没有网络，不要 `pip install`。

> 脚本在 `/opt/knowwork/office/`。用户给的原文件在 `/workspace/input`（只读），中间文件放
> `/workspace/tmp`，要交给用户的放 `/workspace/output`，写好后调用 `present` 交付（例如 `present({"files":[{"path":"/workspace/output/表格.xlsx"}]})`）。

## 每份输出的要求

- **全表用正规字体**（Arial、Times New Roman；中文内容用 等线 或 微软雅黑），用户另有要求除外。
- **零公式错误。** `recalc.py` 报 `errors_found` 时绝不交付。你认为某个错误是原来就有的，就证明它：
  用 `data_only=True` 打开**原文件**看那个格子。你引入的错误和继承来的错误看起来一模一样。
- **用公式，不要写死结果。** 写 `sheet['B10'] = '=SUM(B2:B9)'`，而不是 Python 算好的合计。输入变了，
  表格要能自己重算。
- **严格按用户的要求做。** 表名、列标题、用户写出来的公式都一字不差。算的是别的东西的"重新设计"，
  再漂亮也是失败。
- **每个假设和写死的数字都要写明**，写在读的人看得到的地方——单元格批注，或表格末尾相邻的格子。
  有真实出处就注明（`来源：公司 2024 年年报，第 45 页，营业收入附注`）；数字是用户给的，就直说。
- **你新建、要别人填写的表格**，要有一段简短的图例说明哪些格子可以改，并给一行真实的示例数据示范格式。
  要求你**修改**的文件里绝不要加这样的示例行。
- **修改已有文件：完全遵循它的惯例。** 它的惯例优先于这里的所有规则。先找出它指定的输入格——
  特别的字体颜色、填充或底纹通常就是标记——只往那里写，已有的公式一个都不动。

## 重算（只要文件里有公式就必做）

openpyxl 把公式当字符串写进去，**没有缓存值**。重算之前，任何读缓存值的东西——`pandas`、
`load_workbook(data_only=True)`、大多数预览——读出来的公式格都是 `None`。

```bash
python3 /opt/knowwork/office/recalc.py /workspace/output/文件.xlsx [超时秒数]   # 默认 60
```

LibreOffice 会算出每个公式，文件**原地重写**，并输出 JSON：`status`（`success` | `errors_found`）、
`total_formulas`、`total_errors`，以及 `error_summary`——每种错误最多列 100 个格子（`locations_truncated`
说明还有多少没列——以 `total_errors` 为准，不要数列表长度）。修掉它列出的问题再跑一次。**JSON 里是
`error` 而不是 `status`，说明什么都没算**，也只有这种情况退出码非 0——`errors_found` 的退出码是 0，
所以绝不要把退出码为 0 当成表格没问题。

**重算通过只证明公式能*算出值*，不证明它们*算得对*。** 范围差一行、引用错了行，得到的是一份干干净净、
没有错误、数字却错了的文件。先写 2–3 个公式，确认它们取到了你预期的值，再铺满整片区域。

**链接了其他文件的工作簿**，用 openpyxl 重存再重算会丢掉那些链接。这类公式长这样：
`='[1]回报分析'!$B$2`——`[1]` 是工作簿外部引用列表里的序号，指向**磁盘上另一个文件**，不是某张表。
那个文件在这里几乎不会有，所以格子里的缓存值是唯一保存着它数据的地方。openpyxl 保存时会清掉这个值；
LibreOffice 于是只能真的去解析引用，失败后写入 `#NAME?` 并删掉所有链接。这种情况下 `recalc.py`
会拒绝运行——先把那些格子的值从原文件里抄出来，再覆盖保存（`--force` 可以强制运行，并接受链接丢失）。

## 选能通过校验的公式

LibreOffice 支持的函数比 Excel 少，它算不了的函数会变成一个实打实的 `#NAME?` 写进你交付的文件。

- **优先用 Excel 2007 时代的函数**——`SUMIFS`、`INDEX`、`MATCH`、`IFERROR`、`SUMPRODUCT`——不需要前缀。
- **六个 2007 年以后的函数可以用，但必须加 `_xlfn.` 前缀**，因为 openpyxl 会把公式原样写进 XML，
  而 Excel 存储这些新函数名时带前缀（界面里看不到）：`_xlfn.TEXTJOIN`、`_xlfn.CONCAT`、`_xlfn.IFS`、
  `_xlfn.SWITCH`、`_xlfn.MAXIFS`、`_xlfn.MINIFS`。不加前缀就是 `#NAME?`。
- **绝不要用 `XLOOKUP`、`XMATCH`、`SORT`、`FILTER`、`UNIQUE`、`SEQUENCE`。** 这里的 LibreOffice 是
  较新的版本，能算出其中一些，但它们是溢出型数组函数，openpyxl 写出的文件没有溢出元数据，所以只有区域
  左上角那一格有值——而 `recalc.py` 对这个被截断的结果照样报 `total_errors: 0`。另外用户若用较老的
  Excel，这些函数本身就打不开。查找用 `INDEX`/`MATCH`，排序、筛选、去重在 Python 里做完再写进格子。
- LibreOffice 解析不了的公式会被**改成小写**写回去——看到 `#NAME?` 旁边的公式是小写，就是这个原因。

## openpyxl 坑点

- **读懂一个模型要读两遍。** `data_only=True` 拿到缓存值，公式没了；默认方式拿到公式字符串，没有值。
  一遍拿不到两样。
- **用 `data_only=True` 打开后保存是破坏性的。** 那个工作簿里已经没有公式了，保存就把每一个公式都
  永久替换成了固定值。
- **对 openpyxl 刚写出的文件用 `data_only=True`，读到的全是 `None`**——先跑 `recalc.py`。
  （结果为 `""` 的公式读回来也是 `None`。）
- **合并单元格：只写左上角那一格。** 区域里其他格子都是 `MergedCell`，`.value` 只读。
- **`.xlsm` 不传 `keep_vba=True` 给 `load_workbook` 就会丢掉宏。**
- **表名里有空格的，跨表引用时必须加引号**：`='假设 输入'!$B$5`。不加引号算出来是 `#VALUE!`。
- **openpyxl 读不进图表、图片、形状和数据透视表，存回去它们就没了。** 改已有文件前先看压缩包里有没有
  `xl/charts/`、`xl/drawings/`、`xl/pivotTables/`（`unzip -l 文件.xlsx`）；有的话不要整本重存覆盖，
  另存一份并在回答里说明哪些内容没带过去。

## 财务模型

用户没另说、已有文件也没有别的做法时，按下面来。

**颜色：** 写死的输入和情景变量用蓝字（`0,0,255`）· 公式用黑字 · 引用其他表用绿字（`0,128,0`）·
引用其他文件用红字（`255,0,0`）· 关键假设和需要用户填写的格子用黄色填充（`255,255,0`）。

**数字：** 货币 `$#,##0`（人民币 `¥#,##0`），单位写在表头里（`营业收入（百万元）`）· 零显示为 `-`，
百分比也一样（`$#,##0;($#,##0);-`）· 负数用括号 · 百分比 `0.0%`，**存成小数**（`0.15` 显示为 `15.0%`；
存 `15` 就显示成 `1500.0%`）· 估值倍数 `0.0x` · 年份存成文本（`"2024"`，绝不要显示成 `2,024`）。

**结构：** 每个假设放在自己的带标签的格子里，公式去引用它（`=B5*(1+$B$6)`，绝不写 `=B5*1.05`）·
每个预测期的公式保持一致——一行中间某个格子被单独改过，是最常见的隐蔽错误 · 可能为零的分母要做保护。

## 回答里说什么

说清楚表格里有什么、哪些格子是公式、重算结果（有没有错误）、做了哪些假设。不要写 `/workspace` 路径，
文件会作为附件随回答交给用户。

## 依赖

`openpyxl`、`pandas`、`markitdown`（Python，已装）· LibreOffice（通过 `/opt/knowwork/office/soffice.py`
与 `recalc.py` 调用）
