86 lines
3.5 KiB
Python
86 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
fix-richtext-rpr-order.py — 把 openpyxl CellRichText 写出的 <rPr> 子元素顺序
|
|
改成 OOXML 合规顺序 (rFont -> sz -> color)。
|
|
|
|
⚠️⚠️ 重要警告(2026-06-18 世茂实证):
|
|
本脚本只修 rPr 顺序,但实测 **修了顺序 Microsoft Excel 仍报"需要修复"**。
|
|
openpyxl 生成的富文本表在 Excel 严格校验下还有其他不合规处(去掉富文本只留
|
|
纯文本都仍报错)。所以本脚本 **不保证解决 Excel 报错**,仅作"试过什么"的记录。
|
|
|
|
正解:不要用 CellRichText 做局部标红/加粗,改用纯文本前缀(【需客户核实】…)。
|
|
详见 ../references/openpyxl-excel-richtext-pitfall.md
|
|
|
|
用法:
|
|
python fix-richtext-rpr-order.py <文件.xlsx> # 原地修复
|
|
python fix-richtext-rpr-order.py <文件.xlsx> --check # 只检查,不改
|
|
python fix-richtext-rpr-order.py <文件.xlsx> --sheet sheet2.xml # 指定 sheet
|
|
"""
|
|
import sys, re, zipfile, os
|
|
|
|
|
|
def analyze(xlsx, sheet_name="xl/worksheets/sheet1.xml"):
|
|
z = zipfile.ZipFile(xlsx)
|
|
if sheet_name not in z.namelist():
|
|
sheets = [n for n in z.namelist() if re.match(r"xl/worksheets/sheet\d+\.xml$", n)]
|
|
print(f"指定 sheet 不存在;可用:{sheets}")
|
|
return None, None
|
|
xml = z.read(sheet_name).decode("utf-8")
|
|
rprs = re.findall(r"<rPr>(.*?)</rPr>", xml, flags=re.S)
|
|
bad = 0
|
|
for inner in rprs:
|
|
p_sz = inner.find("<sz")
|
|
p_color = inner.find("<color")
|
|
if p_sz != -1 and p_color != -1 and p_sz > p_color:
|
|
bad += 1 # color 在 sz 之前 = 错误顺序
|
|
return xml, (len(rprs), bad)
|
|
|
|
|
|
def fix(xlsx, sheet_name="xl/worksheets/sheet1.xml"):
|
|
xml, stats = analyze(xlsx, sheet_name)
|
|
if xml is None:
|
|
return
|
|
total, bad = stats
|
|
print(f"rPr 总数 {total},错误顺序 {bad}")
|
|
if bad == 0:
|
|
print("无需修复(顺序已合规)。注意:顺序合规 ≠ Excel 不报错,见脚本顶部警告。")
|
|
return
|
|
|
|
def _fix(m):
|
|
inner = m.group(1)
|
|
rf = re.search(r"<rFont[^/]*/>", inner)
|
|
cs = re.search(r"<charset[^/]*/>", inner)
|
|
fam = re.search(r"<family[^/]*/>", inner)
|
|
b = re.search(r"<b[^/]*/>", inner)
|
|
i = re.search(r"<i[^/]*/>", inner)
|
|
sz = re.search(r"<sz[^/]*/>", inner)
|
|
co = re.search(r"<color[^/]*/>", inner)
|
|
# OOXML 顺序: rFont, charset, family, b, i, ..., sz, color
|
|
parts = [x.group(0) for x in (rf, cs, fam, b, i, sz, co) if x]
|
|
return "<rPr>" + "".join(parts) + "</rPr>"
|
|
|
|
fixed = re.sub(r"<rPr>(.*?)</rPr>", _fix, xml, flags=re.S)
|
|
tmp = xlsx + ".tmp"
|
|
with zipfile.ZipFile(xlsx, "r") as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zo:
|
|
for it in zin.namelist():
|
|
zo.writestr(it, fixed.encode("utf-8") if it == sheet_name else zin.read(it))
|
|
os.replace(tmp, xlsx)
|
|
print(f"已修复并写回 {xlsx}")
|
|
print("⚠️ 仍需用 Microsoft Excel 实际打开验证——本脚本不保证消除 Excel 报错。")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
path = sys.argv[1]
|
|
sheet = "xl/worksheets/sheet1.xml"
|
|
if "--sheet" in sys.argv:
|
|
sheet = "xl/worksheets/" + sys.argv[sys.argv.index("--sheet") + 1]
|
|
if "--check" in sys.argv:
|
|
_, stats = analyze(path, sheet)
|
|
if stats:
|
|
print(f"rPr 总数 {stats[0]},错误顺序 {stats[1]}")
|
|
else:
|
|
fix(path, sheet)
|