# 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. **修 `` 子元素顺序**:openpyxl 输出 ``,OOXML schema 要求 ``(sz 在 color 前)。手改 XML 把 53 个 rPr 全调正 → **Excel 仍报错**。 3. **补 ``(中文字符集)+ ``**: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` + 富文本红 `` 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` 里 `45` 的 `45` 就是 sharedString 索引。`--map` 一键列出全部映射。 4. lxml 打开 `xl/sharedStrings.xml`,**只动目标 `` 的 `` 文字**: - 纯文本格(无红 run)→ 清空子节点、重写单个 `新文本`。 - 含红 run 的格 → **只改黑色 `` 的 ``**;红 ``(带 ``)一个字不碰。 - 删一条红色风险项 + 顺移编号:`si.remove(目标)` 后把后续 `` 的 `"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 改 ``→删 run 顺移→规范重打包→五查 + 可 import 的工具函数)见 `scripts/edit-redmarked-xlsx.py`,照抄别重写。