7.2 KiB
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 里失败
- openpyxl 原生
CellRichText+InlineFont(color=...)→ Excel 报错。- 数据层验证(解压 xlsx 读 sheet XML)红色精确写入了,WPS 打开也正常,openpyxl readback 也对——但 Excel 报错。
- 修
<rPr>子元素顺序:openpyxl 输出<rFont/><color/><sz/>,OOXML schema 要求<rFont/>→<sz/>→<color/>(sz 在 color 前)。手改 XML 把 53 个 rPr 全调正 → Excel 仍报错。 - 补
<charset val="134"/>(中文字符集)+<family val="2"/>:styles.xml 里正常字体都有 charset/family,富文本 rPr 缺了。补齐 + 正确顺序(rFont→charset→family→sz→color)→ Excel 仍报错。 - 去掉所有富文本,恢复纯文本 → 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+ 次"还是不行"),用户失去耐心。
行为铁律(比技术更重要)
- 改完无法自验 Excel 行为时,如实说"我这边验不了 Excel,你帮我打开看下",绝不断言"修好了"。 没有复现工具就别打包票。
- "WPS 打开正常"≠交付合格。Maggie 和她客户用 Microsoft Excel,以 Excel 为准。
- 别在一个底层格式有问题的文件上反复打补丁——越补越不可控。识别到"连纯文本版都报错"时就该换根本方案,而不是继续修富文本。
正确做法(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 层做外科手术。
外科手术流程(已验证正确)
- 改前先备份 WPS 好版本为
_bak_*.xlsx(openpyxl 一旦失手,这是唯一退路)。 zipfile解压好文件到临时目录。- 定位"改哪个格 → 改第几条 si":
sheet1.xml里<c r="H11" t="s"><v>45</v></c>的<v>45就是 sharedString 索引。--map一键列出全部映射。 - 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。
- 纯文本格(无红 run)→ 清空子节点、重写单个
- 规范重打包:
[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,照抄别重写。