openpyxl 写 Excel 的三个隐蔽坑:等号开头的文本、列宽自适应、公式缓存
openpyxl 写 Excel 的三个隐蔽坑:等号开头的文本、列宽自适应、公式缓存
用 Python 批量生成 Excel 报表时,openpyxl 几乎是默认选择:它不需要装 Office,纯 Python 就能写出带样式、带公式、带多个 Sheet 的工作簿。但它有几个地方的行为和"人手在 Excel 里操作"并不一样,踩上去不会报错,只会悄悄产出错文件。下面三个坑是我在实际项目里反复遇到的,每个都附上可复现的最小代码和处置方式。
坑一:以等号开头的字符串会被当成公式
这是最容易翻车的一个。只要赋值的字符串以 = 开头,openpyxl 就把它标成公式类型(data_type 为 f),而不是文本:
from openpyxl import Workbook
wb = Workbook(); ws = wb.active
ws["A1"] = "=工程验收标准" # 本意是文本
print(ws["A1"].data_type) # 输出 f,不是 s
wb.save("demo.xlsx")
打开文件会看到 #NAME? 错误。更隐蔽的情况是文本恰好长得像表达式,比如风险登记表里写 = 未按规范执行,或者备注里写 => 见附件。
处置办法是显式把类型改回文本:
c = ws["A1"]
c.value = "=工程验收标准"
c.data_type = "s" # 必须在赋值之后设置
另一种写法是在前面加一个单引号,但那会把引号一起写进单元格显示内容,导出给业务方看很难看,不推荐。
顺带一提:如果同名列里既有 = 已闭环 这种文本、又有真正的数字公式,最好在读入阶段就用 data_type 判断一次,而不是等 Excel 打开报错才发现——批量生成的表往往几十个 Sheet,肉眼查不过来。
坑二:列宽不会自适应,得自己算
openpyxl 的 column_dimensions["A"].width 默认是 None,Excel 打开时会用标准列宽,长文本被截断成一串 #### 或者只显示前半句。openpyxl 没有"测量渲染宽度"的能力(它不加载字体度量),所以想自适应只能自己估:
def fit(ws, min_w=8, max_w=50):
for col in ws.columns:
width = min_w
letter = col[0].column_letter
for cell in col:
if cell.value is None:
continue
# 中日韩字符按 2 个宽度估算,其余按 1
s = str(cell.value)
w = sum(2 if ord(ch) > 127 else 1 for ch in s)
width = max(width, w + 2)
ws.column_dimensions[letter].width = min(width, max_w)
三个要点:ord(ch) > 127 这个粗略判断对中文和全角符号够用;+2 是给单元格内边距留余量;必须设上限,否则一段几百字的备注会把列拉到屏幕外。另外合并单元格 merge_cells 之后 ws.columns 拿到的还是原始单元格,宽度估算要把合并区域的总宽按跨列数摊开,否则会算出过窄的列。
坑三:公式写进去了,但没有缓存值
openpyxl 只负责把公式文本写进 XML,不负责计算。这意味着:
ws["B10"] = "=SUM(B2:B9)"
wb.save("demo.xlsx")
用 Excel/WPS 打开能正常显示结果,但如果你用 openpyxl.load_workbook(path, data_only=True) 或 pandas 读回来,拿到的是 None——因为文件里根本没有缓存值,缓存值是 Excel 打开时才写的。
这在"生成→回读校验"的自动化流程里会造成误判:校验脚本读到 None,以为汇总行是空的,其实只是没人算过。
可行的处置有两条:
- 生成阶段同时把计算结果写进相邻的隐藏列,回读时用结果列校验;
- 用 LibreOffice 无头模式重算一遍再另存:
soffice --headless --convert-to xlsx --outdir out in.xlsx,转换过程会写入缓存值。
第二种方式在 CI 里更干净,代价是多一层外部依赖;第一种方式没有依赖,但要保证两处算法一致,容易分叉。我一般按报表的下游用途选:给人看的走第二种,给程序读的干脆不写公式,直接在 Python 里算好写数值。
小结
这三个坑的共同点是"静默失败":不抛异常、不报错,只是产出的文件不对。所以比起事后排查,更划算的是在生成器里加一道自检——赋值 = 开头的字符串后立刻断言 data_type == "s",保存前跑一遍列宽估算,交付前明确公式列是"给人看"还是"给程序读"。这三行断言能省掉后面大量的返工。
- 点赞
- 收藏
- 关注作者
评论(0)