Excel 插图在手机端不显示/错位:完整排查手册

有一类 Excel 问题特别让人头疼:明明电脑上渲染正常,发到手机端打开,插图要么看不见,要么位置跑偏。表格里带照片本来是常见需求,偏偏跨端兼容成了坑。这篇手册记录了我在处理这类文件时踩过的所有真实坑,从工具选型到排查思路到解决方案,全部来自实际项目复盘。如果你也需要在 Excel 里稳定地插入图片,并且确保手机端能正常显示,看这一篇就够了。

文章目录

为什么做这个

接到一个带照片的 xlsx 处理任务:表格本身是一份多栏格式的申报类文档,有合并单元格、文字内容、嵌入照片,版式已经调好,要求只改文字内容不改任何版式。电脑上用 LibreOffice 打开看起来没问题,但最终用户需要在手机 App 上查看和填写。结果手机端照片格只显示文字,或者位置错位压住了边框线。

这个需求背后有个关键矛盾:程序生成的 Excel 和手工维护的 Excel 在底层 XML 结构上存在微妙差异。桌面软件容错率高,手机 App 解析严格。很多在电脑上看似正常的文件,手机端就会原形毕露。我需要找出一套既能批量处理内容、又能保证手机端正常显示的完整方案。

工具与环境

这个任务涉及 xlsx 读写、图片处理、PDF 渲染验证三个环节,我把用到的工具列成表格方便对照。

工具 版本 用途
Python 3.12 主力脚本环境,openpyxl、Pillow、zipfile 均在此运行
openpyxl 系统自带 xlsx 读写:填值、改样式、插图、调打印设置
Pillow 系统自带 图片裁剪、缩放、格式转换
LibreOffice 24.2 soffice 命令将 xlsx 转 PDF,渲染效果最接近真实打印
poppler 系统自带 pdftoppm 将 PDF 转 PNG,用于目检排版
zipfile Python 标准库 xlsx 本质是 zip 包,拆包做结构级手术

环境注意一点:pip 安装受 PEP 668 限制,装新包需要用系统包管理器,或者加 --break-system-packages 参数。所有操作默认在本机完成,文件不上传任何第三方平台。

怎么做的

我把完整流程拆成两条并行线:内容处理线和图片插入线。

内容处理标准流程 分为六个步骤。第一步读结构,用 openpyxl 载入文件后先打印 dimensions、merged_cells、有值单元格坐标,搞清楚标签格和数据格的布局。第二步填值,向目标单元格写入内容,合并单元格只写左上角那个格,其他位置不写。第三步样式微调,按需调整字号、字体、对齐方式。第四步保存后回读验证,把填过的格子读出来逐项核对,确保没有遗漏。第五步 LibreOffice 转 PDF,再用 pdftoppm 转 PNG,在本地目检排版是否正常。第六步交付 xlsx 和 PDF 两个版本给用户。

图片插入流程 更复杂一点。PIL 按目标比例裁剪并缩放图片后,用 openpyxl 的 add_image 插入,锚点指向目标单元格。插入后需要拆包检查 xl/drawings/drawing*.xml,确认每张图都有对应引用、没有孤儿 drawing。转 PDF 目检照片位置、大小、是否压住文字。最后在手机端确认能看到图。

两条线在第五步合流,用 PDF 渲染结果做跨端一致性验证。

踩过的坑

这一节记录整个过程中遇到的所有实际问题,每个坑都写清楚现象、排查过程或原因、解法。

坑1:rels Target 绝对路径改相对路径(最核心)

这是整个任务里最关键的一个坑,排查过程最长,解决思路也最值得复盘。

现象很清楚:xlsx 里插入了一寸照片,电脑端用 LibreOffice 渲染能看到,用户手机 App 上照片格只显示文字内容,图片完全不显示。

排查从四个方向展开,每一步都排除了一种可能。

第 1 步,我先怀疑是锚点或者尺寸的问题。把 xlsx 拆包,确认 <anchor> 节点写法正确、图片文件本身有效,排除锚点问题。

第 2 步,我发现原模板自带一张示例占位图。openpyxl 保留了孤儿 drawing,虽然图片本身删掉了,但 drawing 节点还留在文件里。清理掉孤儿 drawing 后重新插入,问题仍然存在,排除这一步。

第 3 步,手写 drawing XML 的时候把 <clientData/> 塞进了 <pic> 内部。对照原模板结构逐项校验,发现 <clientData/> 应该与 <pic> 平级,而不是作为 <pic> 的子节点。修正后仍然不行,但这确实是一个 bug,严格解析的查看器会跳过整张图。这步排除,但不是真凶。

第 4 步,真凶浮出水面。检查图片引用的 Target 路径发现,openpyxl 生成的是绝对路径 /xl/media/xxx.jpeg,而原模板(真实 Excel 产出的)用的是相对路径 ../media/xxx.jpeg。桌面软件对两种路径都能识别,手机 App 只认相对路径。

解法:把 xl/worksheets/_rels/sheet1.xml.rels 和 xl/drawings/_rels/drawing1.xml.rels 里的 Target 全部改成相对路径。改完之后重新打包,手机端照片正常显示。

下面这张图是实测对比,上面是 openpyxl 生成的绝对路径,下面是改成相对路径之后的样子:

这个坑的教训是:程序生成的 xlsx 在手机端打不开或显示异常时,拿到原模板逐字节对比结构差异,rels 路径写法、drawing 结构、命名空间写法都是嫌疑点。不要只在电脑端验证就交付给用户。

坑2:openpyxl 给图片 blip 硬编码 cstate="print"

openpyxl 在生成图片节点时,spreadsheet_drawing.py 的 _picture_frame 里写死了 pic.blipFill.blip.cstate = "print"。目前观察多数查看器忽略这个属性,暂无实害。但手写 XML 的时候直接省略该属性会更干净。

坑3:openpyxl 会保留原文件的孤儿 drawing

模板自带示例图时,对应的 drawing 文件(比如 drawing2.xml)在 openpyxl 多次读写后变成无人引用的孤儿 parts,一直躺在 zip 包里。编辑别人给的、带过图的 xlsx 时,先拆包看 xl/drawings/ 和 xl/media/ 目录,确认每张图都有对应的 <drawing> 引用元素,没有引用的图就是孤儿,要删掉。

坑4:手机 App 重存文件会改名 media 并丢图

这是最意外的坑。用户拿到处理好的文件后,在手机 App 上改了几个字再保存,传回来的文件在 openpyxl 里直接打不开,报 KeyError。拆包排查发现:WPS 把 image1.jpeg 重命名成了 1.jpeg,还删掉了另一张图但留下了悬空引用(drawing2.xml 指向 ../media/2.jpeg,但这个文件不存在)。

修法:拆包删掉悬空的 drawing xml、它的 rels 文件、[Content_Types].xml 里的声明,以及 sheet 文件里残留的 <drawing> 引用元素。处理完后再用 openpyxl 打开就没问题了。

这个坑的教训是:用户手机改完传回来的文件,先做一次开包体检------尝试用 openpyxl 打开,同时检查有无悬空引用------确认没问题再当成新的母版使用。

坑5:毕业时间格式

预填值不一定对,关键字段要用权威来源交叉核对,毕业时间这类字段尤其要以证明材料为准,不能直接沿用预填值。

坑6:长文本格子的标签/数据交错布局

动手填值前先打印有值单元格坐标,看清楚标签格和数据格的交错布局再动笔,避免把内容写到错误的格子里。

坑7:照片在手机端位置偏移

现象和坑1不同:照片能显示了,但在手机 App 上位置跑偏,压住了边框线或者跑到别的格子里,电脑端 LibreOffice 渲染却正常。

排查后发现原因:之前用的 <twoCellAnchor> 写法里,from 和 to 都在同一列(比如 H 列),只靠 colOff 差值撑宽度。部分手机 App 对这种写法解析不一致,导致偏移。

解法:改用最标准的 <oneCellAnchor>。from 左上对准目标格(如 H2,可以带微小的 rowOff 垂直居中),ext 写死图片尺寸。结构是 from → ext → pic → clientData,不要手写多余属性。

下面是实测拆包看到的 oneCellAnchor 结构,from、ext、pic、clientData 依次排开,clientData 和 pic 平级:

教训:给手机端看的 xlsx,drawing 有多少种写法就用最标准的那一种,别玩花样。<oneCellAnchor> 是兼容性最好的方案。

坑8:行高按最多行数估会翻车

估算行高时不要按"最多行数"直接估算,要数最长格子的实际字符数,除以每行容纳字数,向上取整再加一行余量。手机端字体渲染可能更宽,余量宁多勿少。

坑9:A4 打印先看纸型设置

涉及打印的文档先检查 page_setup.paperSize,数值 9 等于 A4,加上 fitToWidth=1、fitToHeight=1 单页自适应。如果这些都对就不用调,直接打。

坑10:openpyxl 同一进程内连 save 两次会炸

现象:写一个脚本里先 wb.save(A),然后删一张表,再 wb.save(B),第二次 save 报错 ValueError: I/O operation on closed file。原因是图片流被第一次 save 关掉了。

解法:分成两个 Python 进程来做。第一进程只管打开原文件、改内容、保存。第二进程重新 load 文件、删除目标表、另存。文件 IO 和内存对象的生命周期要分开管理。

教训:openpyxl + 图片的场景,一次进程只做一次 save。

坑11:openpyxl 每次保存都会把 rels Target 改回绝对路径

这个坑和坑1紧密关联,是坑1的复发机制。

现象:照片之前改好了,手机端显示正常。用 openpyxl 改了几个单元格内容或者调整了行高,再保存,照片又不显示了。

排查后发现原因:openpyxl 保存时会重写 xl/drawings/_rels/drawing1.xml.rels,把 Target 改回绝对路径 /xl/media/image1.jpeg(还会把 media 目录里的文件名改成 image1.jpeg)。drawing 结构本身没坏,纯粹是路径问题,电脑端和 LibreOffice 照样显示,只有手机 App 严格解析才出问题。

解法:zip 手术把 Target 改回相对路径 ../media/image1.jpeg 即可,不用动其他内容。

教训涉及操作顺序:带图的 xlsx,所有 openpyxl 操作放在前面做,最后一步再用 zip 手术修复 rels。顺序反了就会反复翻车------每改一次内容就要再修一次路径。

注意事项

动手前先确认以下事项,按清单逐项检查:

  1. 先拆包看原模板结构 。不要直接在原文件上改,先把 xlsx 当 zip 包拆开,看 xl/worksheets/、xl/drawings/、xl/media/ 的内容,确认有多少张图、对应哪些 sheet、有没有孤儿 drawing。

  2. 手机是最终验收环境之一。不能只在电脑端 LibreOffice 渲染正常就算通过,手机端必须实测。每一次交付前都要在手机上打开文件确认。

  3. 有图的文档必须在手机上确认。文字内容改动可能引起行高变化,导致图片位置偏移。行高调整后要在手机上重新看图有没有被挤压或跑位。

  4. 交付前必须经过渲染目检。LibreOffice 转 PDF、pdftoppm 转 PNG,在本地屏幕上放大检查文字有没有被截断、图有没有压线。图片清晰度也要检查。

  5. 代码写对不等于显示对。openpyxl 生成的 XML 结构在代码层面没问题,但手机端解析可能有问题。rels 路径写法、anchor 类型选择都要对照原模板来,不能靠想当然。

  6. 用户返回的文件先体检再使用。用户可能在手机 App 上修改过,传回来的文件要先用 openpyxl 打开测试,检查有没有悬空引用,再当成新的母版。

  7. 涉及敏感信息的文档,过程文件放临时目录,完工即删。中间生成的 PDF、PNG、拆包出来的临时文件不要留在工作目录,做完就删。

长期维护与扩展

我给自己定了一套标准动作清单,每次处理这类带图的 xlsx 都按这个流程走。

标准动作清单:

  1. 读结构:openpyxl 载入后先打印 dimensions、merged_cells、有值单元格坐标,搞清布局再动手。
  2. 填值:合并单元格只写左上角,写完回读验证逐项核对。
  3. 微调样式:只在必要的地方调字号和对齐,不动版式。
  4. 回读验证:把填过的格子读出来对照原始数据确认无误。
  5. 转 PDF 目检:LibreOffice 转 PDF,pdftoppm 转 PNG,放大检查排版。
  6. 手机端确认:最终在手机上打开文件,看图是否显示、位置是否正常。
  7. 双份归档:定稿存两份,交付版给用户,敏感母版只存本地,不上传、不进仓库。
  8. 清过程文件:临时拆包、PDF、PNG 全部删除,不留痕。
  9. 更新手册:遇到新坑就回这篇手册补一条,经验累积起来。

核心原则:

  • 内容核对用权威来源,不拍脑袋。关键字段(数字、日期、名称)要以原始证明材料为准,不能依赖原模板的预填值。
  • 敏感信息只存本地,处理过程不经过任何云端服务。
  • 过程文件放临时目录,完工即删。工作目录只保留最终交付物。

复盘

做对的三件事:

第一,把 PDF 渲染目检作为标准环节,而不是可选项。没有这一环,很多文字截断和图压线问题要到用户反馈才发现。加上这一步骤后,交付质量明显提升。

第二,遇到坑1的时候没有只在电脑上反复试,而是直接拆包对比原模板的结构差异。这才找到了 rels 路径这个关键点。只看代码不看 XML 结构会走很多弯路。

第三,把坑11的操作顺序固化下来:所有 openpyxl 操作在前,zip rels 修正在后。这避免了反复修反复翻的问题,每次只修一次就到位。

可迁移的建议:

处理任何涉及跨端兼容的文档格式问题时,优先怀疑结构细节而不是代码逻辑。程序生成的文档和手工维护的文档在结构上有微妙差异,手机 App 解析比桌面软件严格。把文件拆开看 XML 结构、逐字节对比差异,比反复调代码更有效率。

有图的文件,处理完内容后一定要在目标端(手机)实测,不能只看电脑端渲染正常就交付。电脑端验证只是必要条件,不是充分条件。

总结

  1. 手机端不显示图片的第一嫌疑是 rels Target 路径:openpyxl 生成的绝对路径在桌面软件正常但手机 App 不认,改成相对路径即可解决。
  2. openpyxl 保存会重写 rels 路径,所有内容修改操作要放在前面,最后一步再做路径修正,顺序不能反。
  3. 孤儿 drawing 会导致 openpyxl 打开报错,编辑别人提供的带图文件前要先拆包检查有没有悬空引用。
  4. drawing anchor 写法用 oneCellAnchor 最标准,避免 twoCellAnchor 同列靠 colOff 撑宽度的写法,手机端兼容性更好。
  5. 带图文件要在手机端实测、PDF 渲染目检,双重验证通过才算完成交付。

以上,全文完。

这篇对你有帮助的话,欢迎留言交流你的做法,也欢迎提出不同看法。

最后认识一下,我是黎羽,一名 AI 实践爱好者,会不定期记录我的学习过程,期待你的关注和分享。

相关推荐
用户298698530141 天前
React 前端实现 Excel 与 CSV 互转:基于 WebAssembly 的浏览器端方案
javascript·react.js·excel
仓库日记2 天前
Excel进销存何时该换?用3项检查判断是否需要软件
经验分享·excel·进销存·开单软件
用户0332126663672 天前
如何使用 Python 将 JSON 转换为 Excel
python·excel
2601_967980113 天前
Python自动化办公:模块、库与样式调整教程
python·excel·自动化办公·openpyxl·样式调整
用户0332126663673 天前
使用 Python 轻松加密或解密 Excel 文档
python·excel
总裁余(余登武)4 天前
【python项目】:1操作EXCEl自动化开发
python·自动化·excel
ailitsoft7 天前
库存管理系统如何记录跨门店调拨后的收货差异?
excel·零售·软件需求
百事牛科技8 天前
Excel的三种密码:防看、防存、防乱改,各管各的
windows·excel
用户298698530148 天前
React 前端将 Excel 工作表导出为图片的技术实践
javascript·react.js·excel