Files

72 lines
7.2 KiB
Markdown

# openpyxl 富文本局部着色 → Excel "需要修复" 陷阱(2026-06-18 世茂血泪全记录)
## 结论先行(2026-06-18 最终反转:WPS 另存救活了红色)
**openpyxl 的 `CellRichText` 局部标红会让 Microsoft Excel 报"发现部分内容有问题,是否修复"**(点修复后丢富文本→红色全没)。本 session 用尽各种手改 XML 的修法都没在 Excel 里直接救活——**但最后一步成了:openpyxl 写好红 → 用 WPS 打开 → 另存为 xlsx,Excel 不再报错且红色保留**(Maggie 亲验 Excel 正常打开+红色在)。WPS 另存把 openpyxl 的不规范 XML(`inlineStr`+`CellRichText`)整体重写成规范格式(`sharedStrings.xml`),消除所有不合规处,这才是 Excel 认它的真因。
所以"单元格内某条标红、其余黑、Excel 兼容"**有跑通的路**:`openpyxl 写富文本红 → WPS 另存`。两步缺一不可。嫌 WPS 那步麻烦/纯自动化场景,退而用**纯文本前缀**(`【需客户核实】…`,整条黑字零格式,Excel 绝不报错)。
## 需求背景
Maggie 要把汇总表里"需要客户去核实/确认某个事实"的风险点**整条标红**(如"两份合同衔接需核实""建议签约时补明2028租金"),方便客户一眼看到待办。一个单元格里装着一整列风险点(🔴1-3、🟡4-9…),只有其中某一条要红 → 必须"单元格内局部不同颜色" → 只能用富文本。
## 试过的修法,全部在 Excel 里失败
1. **openpyxl 原生 `CellRichText` + `InlineFont(color=...)`** → Excel 报错。
- 数据层验证(解压 xlsx 读 sheet XML)红色精确写入了,WPS 打开也正常,openpyxl readback 也对——**但 Excel 报错**。
2. **修 `<rPr>` 子元素顺序**:openpyxl 输出 `<rFont/><color/><sz/>`,OOXML schema 要求 `<rFont/>→<sz/>→<color/>`(sz 在 color 前)。手改 XML 把 53 个 rPr 全调正 → **Excel 仍报错**
3. **补 `<charset val="134"/>`(中文字符集)+ `<family val="2"/>`**:styles.xml 里正常字体都有 charset/family,富文本 rPr 缺了。补齐 + 正确顺序(rFont→charset→family→sz→color)→ **Excel 仍报错**
4. **去掉所有富文本,恢复纯文本** → Excel **还是报**(轻微,能打开)。说明根子不只是富文本,openpyxl 生成的这张表底层某处本就不合 Excel 严格校验。
## 为什么这么难定位——验证工具全部"宽松",骗过自己
| 工具 | 行为 | 能否当 Excel 合格证据 |
|---|---|---|
| openpyxl readback | 读回红色正确、XML 合法(lxml 解析过) | ❌ 不能 |
| WPS | 打开完全正常、红色在 | ❌ 不能(WPS 容错宽松) |
| OnlyOffice x2t | 转换成功、渲染出红色 | ❌ 不能(太宽松) |
| LibreOffice headless | 本环境**连干净文件都报 `source file could not be loaded`** | ❌ 不能(环境本身坏,不代表 Excel) |
| **Microsoft Excel** | **报"发现部分内容有问题/需要修复"** | ✅ 这才是真相 |
**致命点**:本地没有任何能复现 Excel 严格 OOXML 校验的工具。所有手头工具都比 Excel 宽松,导致我反复"修好了→还是不行",把用户当测试员(连续 4+ 次"还是不行"),用户失去耐心。
## 行为铁律(比技术更重要)
1. **改完无法自验 Excel 行为时,如实说"我这边验不了 Excel,你帮我打开看下",绝不断言"修好了"。** 没有复现工具就别打包票。
2. **"WPS 打开正常"≠交付合格**。Maggie 和她客户用 Microsoft Excel,以 Excel 为准。
3. **别在一个底层格式有问题的文件上反复打补丁**——越补越不可控。识别到"连纯文本版都报错"时就该换根本方案,而不是继续修富文本。
## 正确做法(Excel 绝不报错)
- **首选·纯文本标记**:需核实条前加 `【需客户核实】` / `❗待核实:` 前缀,整条黑字。
- **整格统一格式**:`cell.font=Font(bold=True/color=...)``PatternFill` 背景色——安全,但整格所有条目一起染,仅"整格一条"时可用。
- **真要某条带色**:让用户用 **WPS 打开→另存为 xlsx**,WPS 重写为规范格式后 Excel 不再报错、富文本保留(需用户手动一步)。
## 附
- 若硬要走富文本(不推荐),`scripts/fix-richtext-rpr-order.py` 可把 rPr 顺序修成 Excel 合规——但**本 session 实证:修了顺序 Excel 仍报错**,所以这个脚本不保证解决问题,仅作记录。
- 富文本验证小坑:判断红色 run 别用宽松 `'FF0000' in run`——黑色 `FF000000` 含子串 `FF0000` 会被误判成红。必须精确匹配 `rgb="FFFF0000"`(红)排除 `rgb="FF000000"`(黑)。
## ⚠️ 二次编辑陷阱:openpyxl 重存会把 WPS 救回的红色一键毁掉(2026-06-18 世茂实证)
WPS 另存后的好文件存储 = `sharedStrings.xml` + 富文本红 `<r>` run。**对它再做任何 `openpyxl.load_workbook → 改 → save` 都会把成果作废**:openpyxl 把整表打回 `inlineStr``sharedStrings.xml` 消失、**红色 run 3→0**,Excel 又报"需要修复"。实测:改个 H11 单元格而已,红色全没了——幸亏改前留了 WPS 好基线 `_bak_`,才回得来。
**铁律:改已标红(WPS规范化)的 xlsx,改一个字都不能用 openpyxl 存。** 正解是在 `sharedStrings.xml` 的 XML 层做外科手术。
### 外科手术流程(已验证正确)
1. **改前先备份** WPS 好版本为 `_bak_*.xlsx`(openpyxl 一旦失手,这是唯一退路)。
2. `zipfile` 解压好文件到临时目录。
3. **定位"改哪个格 → 改第几条 si"**`sheet1.xml``<c r="H11" t="s"><v>45</v></c>``<v>45` 就是 sharedString 索引。`--map` 一键列出全部映射。
4. lxml 打开 `xl/sharedStrings.xml`,**只动目标 `<si>``<t>` 文字**:
- 纯文本格(无红 run)→ 清空子节点、重写单个 `<t xml:space="preserve">新文本</t>`
- 含红 run 的格 → **只改黑色 `<r>` 的 `<t>`**;红 `<r>`(带 `<rPr>…<color rgb="FFFF0000"/>`)一个字不碰。
- 删一条红色风险项 + 顺移编号:`si.remove(目标<r>)` 后把后续 `<r>``"10. "→"9. "` 等前缀顺移,保 1–N 连续。删红 run 时红色计数随之 −1。
5. **规范重打包**`[Content_Types].xml` 必须 zip 第一项、`_rels/` 次之,否则 LibreOffice 等严格解析器报 `source file could not be loaded`(Excel/WPS 宽容,但别赌)。`ZIP_DEFLATED`
### 改后五查(本地验不了 Excel 时能做的最强保证,过了再发 Maggie)
`xl/sharedStrings.xml` 仍在(不在 = openpyxl 又把富文本毁了);② 红色 run 数 = 预期(删 1 条红就 3→2,没删则不变,精确数 `rgb="FFFF0000"`);③ `zipfile.testzip()` 通过 + 所有 `.xml/.rels` lxml 可 `fromstring`;④ 目标格文字已更新、编号连续、旧表述("留白/未约定"等)全表 `grep` 0 残留;⑤ 与 WPS 好基线部件清单同构(差异仅空目录条目可接受)。
一键:`python3 scripts/edit-redmarked-xlsx.py --verify 改后.xlsx --baseline WPS好基线.xlsx --expect-red 2`。完整实现(解压→按 si 改 `<t>`→删 run 顺移→规范重打包→五查 + 可 import 的工具函数)见 `scripts/edit-redmarked-xlsx.py`,照抄别重写。