# 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`，照抄别重写。
