Files

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 里失败

  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 把整表打回 inlineStrsharedStrings.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,照抄别重写。