夜雨聆风学习资料网

ARTICLE · 1270462

Oracle 导出的 Excel 一打开就卡死?212MB 的报表,8 秒变真 Excel

Oracle 导出的 Excel 一打开就卡死?212MB 的报表,8 秒变真 Excel

上个月有个下午,我在 Oracle 里点了一次导出,格式选的 Excel。

得到一个文件,扩展名 .xls,图标是 Excel 那片绿。

双击。

转圈。

转到第三分钟,标题栏后面多出三个字:未响应。

强退,重开。

这次开了,中文全是方块。

以前我会以为是自己电脑不行。

这台是 i5 十三代,十六线程,固态硬盘,跑什么都够。

问题不在电脑。

那东西从生成的第一秒起,就不是 Excel。

这篇讲三件事:它到底是什么,为什么改后缀没用,以及一份 212.5 MB 的报表怎么在 8 秒内变成真能筛能算的 xlsx。

一、你打开的不是 Excel,是个网页

用记事本打开那个 .xls,第一行是 <html>,接着是 <table>,一行行 <tr>,一格一格 <td>。

扩展名写着 .xls,内容是网页。

这不是哪个版本没修好的 bug,是这类系统早年的习惯:报表页面上显示的就是一张网页表格,你要导出,它就把这张表原样存给你。

一份 212.5 MB 的报表,我数过:八成体积是尖括号和属性,真正的数据不到两成。

Excel 拿到这种文件,认不出是伪造的 xls,于是退回网页引擎,把这两百兆标签一个个解析成单元格。

这份报表 140,184 行、61 列,一共 8,551,225 个格子。

八百五十五万个格子,一个一个渲染。

这解释了两件事。

一是换多贵的电脑都一样卡。

卡的不是算力,是解析方式。

二是中文会变方块。

文件是 UTF-8 存的但没写 BOM,Excel 猜编码,猜成了本地 ANSI。

BOM 是文件开头三个看不见的字节,专门用来告诉程序「我是 UTF-8」。

省了这三个字节,后面全靠猜。

猜错的后果不是乱码两个字这么轻。

一份六十列的报表,列名全变方块,你连哪列是金额都认不出来。

一句话:让 Excel 去渲染一个两百兆的网页,它当然罢工。

那这文件到底长什么样?

我从项目里的样例文件截了一段出来,去掉内容只剩骨架,是这样:

  • • 每一个单元格写成 <td align=left>内容</td>,光这对标签就 20 个字节
  • • 整行再包一层 <tr>,行尾还有换行
  • • 表格前面还有 <style>、<title>、几行 <BR> 参数,都跟数据无关

拿这 20 字节算一下:855 万个格子 × 20,标签本身占掉约 163 MB。

而整份文件 212.5 MB。

也就是说,真正的内容只剩不到五十兆,标签占七成半上下。

实测下来,约八成体积都是标签。

Excel 不是读不懂,是它太老实了:你给它标签,它就一个一个认真渲染。

顺便说个细节,这个细节后面会咬人:样例的样式表里定义了一个 .amount,规则是 mso-number-format:Standard,意思是「按数字显示、右对齐」。

可这份样例里没有任何一个格子引用它。

也就是说,生成这份报表的那套模板,本意是要把金额列当数字处理,只是没套上。

而真正被标了 align=left 的那种编码列,值是 11.020,一个物料分类编码。

Oracle 认它是数字,你的业务认它是编码,两边从文件生成那天起就打架了。

二、三条省事的路,全是死胡同

同事们早就自己发明了办法。

最常见的一条:改后缀,把 .xls 改成 .xlsx。

改完确实能打开,然后呢,还是慢,还是乱码。

因为改的是门牌号,不是户型。

Excel 照样按 HTML 解析,一分钱没省。

第二条:换电脑。

我见过有人为这个申请加内存,十六 G 加到三十二 G,卡的时间从三分钟变成两分半。

第三条:写脚本读。

让 Python 把数据读出来再写成 xlsx,听着对,做起来死。

pandas 的 read_html、BeautifulSoup 这类库,会把整棵 HTML 树建成对象放进内存。

八百五十五万个格子,每个变成一个对象。

普通办公机要么内存溢出,要么卡到你以为死机。

看出来了吗,这跟 Excel 打不开是同一个病:一次性把整个文件吃进内存。

三条路都通不了,剩下的方向只有一个:不要一次性读。

边读边切,切出来就写走,内存不跟着文件长大。

这个思路叫流式。

说人话就是:不把整本书搬进屋子再看,而是站在门口一页一页翻,翻完一页记下要点,把这一页扔掉。

书多大都无所谓,屋里永远只放一页。

落到这个工具上:程序读到一行,切出这行的格子,立刻写进 xlsx,然后把这一行放掉;写 xlsx 的库还有个常驻内存模式,连写过的行都不留。

两端都不攒东西,内存就能一直稳着。

实测跑完整份两百多兆,内存曲线几乎是平的。

这比速度更值钱:速度慢你还能等,内存爆了程序直接死,前面转的全白搭。

三、我给它下了三句话

这次动手的不是我,是 OpenCode。

分工还是那套:我是产品经理,它写代码。

需求单三句话:

  1. 1. 把这种 html 报表转成真正的 xlsx,内容跟原文件一个字符都不能差; 
  2. 2. 两百多兆的不能卡死,内存不能被文件大小带着涨; 
  3. 3. 做好之后双击就能用,不能让财务同事去装 Python、开命令行。

第一句锁死保真,第二句锁死流式,第三句锁死交付形态。

三句话没一句在讲怎么实现,但每句都在排除一种错误的实现。

我原本以为它会先写转换循环,它先做的是确认病根:拿一份真实报表读出来看结构,确认这就是 html 表格,确认编码是 UTF-8 无 BOM。

这一步决定了后面所有设计。

病根是「Excel 在解析 HTML」,解法必然是「别让 Excel 干这活」;如果判断成「文件太大」,后面就会去压缩、分片、加配置,全走偏。

四、第一道险情:那 1 万多个尾零

第一版跑通了,速度可以,数据也对得上。

然后我看着满屏数字动了念头:这些格子都是文本,在 Excel 里不能排序不能求和,要不顺手把像数字的都转成数值?

这是大多数人第一次做这类工具都会加的功能,看着很贴心。

OpenCode 没直接加,它先数了一遍。

这份报表里看起来像数字的格子有 1,946,769 个,其中三类不能碰。

一是编码列。

库存分类写着 11.020,它是编码不是数量。

转成数值,末尾那个零当场消失,变成 11.02。

而且回不来。

Excel 的数字格式只管显示,管不了存值。

这样的格子有 10,505 个。

二是前导零,00123 这种,转成数值是 123。

24 个。

三是 0。

表里有 1,141,527 个 0,套上会计格式全显示成一条短横线;而这张表本来就有 360 万个真正的空格子。

满屏短横线,你分不清哪个是零、哪个是空。

结论很干脆:数值化默认关闭,所有格子按文本写,跟源文件逐字符一致。

想要数值,界面上有个勾选框,旁边写着代价。

为什么不干脆删掉这个选项?

因为确实有人要做透视、要排序。

你不给,他就用别的工具硬转一遍,那时更没人提醒他尾零的事。

留着,但把话说清楚。

界面上那行提示是:勾选后数字列可排序求和,编码类列会被改写。

快不是唯一指标。

改错了还看不出来,才是最贵的错。

五、第二道险情:整张表只剩一列

第一版发出去试用,回来一条反馈:有几种报表,转出来只有第一列有内容。

我以为数据丢了,打开源文件一看,数据好端端都在。

问题在列数怎么定。

第一版拿表格第一行定列数。

这条假设在大多数报表上成立,第一行就是表头。

但有些报表不这么长:第一行是个大标题,一个格子横跨六十列;第二行是参数,期间、组织、币种;真正的表头在第三行。

程序看到第一行只有 1 个格子,于是认定这张表只有 1 列。

后面每行都被截到第 1 列。

六十列的数据,五十九列当场蒸发。

更难受的是,没有任何参数能救。

调列宽没用,列宽是显示,列数已经定死了。

改法有三个方向。

自动跳过标题行?

聪明但危险。

程序凭什么判断哪行是标题,跳错了连数据行一起跳。

全量预扫找最宽那行?

最稳,但要把它多读一遍,实测 8.4 秒变 13 秒,慢五成。

最后落地的是第三种:只看开头 50 行,取其中「最右非空格」的最大列数,而且只当它是「至少这么宽」,不当上限;后面遇到更宽的行,列数当场扩。

不预扫所以不慢,遇见宽行所以不丢。

这个取舍承认了一件事:采样是猜,兜底才是保险。

宁可多占一列,不能少一格。

这条反馈还有个额外价值:它来自一份我这辈子不会拿到的报表。

我自己测一百遍那份 212.5 MB 的大表也测不出来,因为那份表首行就是表头。

测试数据越干净,越容易漏掉脏数据的问题。

六、8 秒是这么来的

同一份 212.5 MB、140,184 行、61 列的报表,四轮实测:

做法
耗时
累计提速
最常见的 xlsxwriter 写
42.7 秒
基准
改成自己手写 xlsx 的 XML
11.02 秒
3.9 倍
换用 Rust 的写出引擎
9.33 秒
4.6 倍
再把解析优化掉
8.4 秒
5.1 倍

真正有意思的是最后一步,因为它没换库。

解析代码改了两处:读单元格原本是一个个匹配取内容,改成一次扫描把整行的格子全取出来;找行尾原本从头往后比,改成从缓冲区尾部往前找第一个候选再验一次。

都是很土的办法。

纯解析从 5.62 秒降到 4.35 秒,端到端从 9.5 秒降到 8.4 秒。

改完我做的第一件事不是看时间,是把输出文件全量算一遍 md5。

跟优化前一模一样,一个字符都没改。

这一步不能省。

快是好事,快的同时悄悄改了内容,那是最坏的事。

上期我写过一句:那个 212MB 的项目同时用着两个引擎,就是这篇里的东西。

Rust 那边只吃字典、列名必须唯一、只能做一张表,所以程序默认走 Rust,遇到要分表就自动退回内置引擎,日志里写明为什么退;表头重复时把第二个「存储期限」改成「存储期限_2」,但日志会写出原名。

名字可以改,得让你知道改了哪个。

七、最后一公里:让人敢双击

技术问题平了,需求单第三句还悬着。

财务同事不会开命令行,不知道什么叫 pip install。

你发个 py 文件过去,等于没做。

这一步是打包:把整个包编译成一个 730 KB 的 pyd,外面套一层运行时目录,双击 exe 就出窗口。

这里有个反直觉的地方:那 730 KB 里装的是全部业务代码,可交付目录加起来七十多兆。

为什么?

因为 Python 解释器本身就在里面。

runtime 目录 20 MB,PyQt6 加 xlsxwriter 53 MB。

你不是发给同事一个脚本加一堆库,是发给他一个能自己跑起来的整套环境。

他那边什么都不用装,不用管 32 位 64 位,也不用在环境变量里折腾。

代价就是这七十多兆得跟着工具走。

我算过,比起让他花一下午配环境,这个代价很划算。

打包这块踩的坑比转换本身还多。

第一次打出来的包里,一运行就报「找不到模块 html_to_xlsx.core」,因为编译参数少了一个开关,子模块根本没被编进去。

还有一次,验收脚本先建了一个界面对象,再调主函数,程序直接崩溃退出,连报错都不给,屏幕上一片空白。

排查了半天才发现:Qt 不允许一个进程里有两个应用对象。

后来把主函数改成「有就复用、没有才新建」,才算稳。

编译成 pyd 还有个副作用:源码看不见了。

对内部工具不算坏事,同事不会不小心改到某个数字;反过来,以后要修 bug 也只能在你自己机器上改、重新编译。

界面只有三样东西:拖文件的地方、一个开始按钮、一块日志。

原来有个「输出格式」能选 xlsx 和 csv,删了,固定 xlsx。

多一个选项,就多一次「我该选哪个」的犹豫,而这个问题没有意义。

转换完成的弹窗,默认按钮从「关闭」改成「打开文件」:人转完文件,下一秒想看的就是那个文件。

验收那天把测试全跑了一遍,72 个全过,包括界面的离屏测试。

现在同事把文件拖进窗口,点开始,等几秒到几十秒,完事。

结果跟源文件同目录同名。

原来双击一次等三分钟还可能白等,现在双击一次,等一杯水的时间。

八、三个判断

第一个:先认清病根,再决定用不用力。

最开始我以为问题是文件大。

按这个判断走,我会去压缩、去分片、去申请更好的电脑,全是白费。

病根是 Excel 在解析 HTML,认准这一句,后面每一步都顺。

下次你被一个「就是很慢」的问题卡住,先花十分钟问:慢在哪一步。

第二个:数据保真优先于功能花哨。

默认全文本这个设计当时看着保守,现在我觉得它是这个项目里做对的最重要一件事。

程序分不清编码列和数值列,那就别猜。

猜对了没人夸你,猜错了没人发现,等发现了已经过了三个月。

第三个:让工具自己承认边界。

Rust 只做单表,就在日志里说;表头被改写,就写出原名;中途取消,就把半成品删掉。

这些都是「我不行」的坦白。

可靠不是永不出错,可靠是出错的时候,你立刻知道错在哪。

回到那个下午。

Oracle 里点导出,得到一个 .xls,双击,转圈,三分钟,未响应。

现在这个流程是:点导出,把文件拖进一个窗口,点开始,八秒,一个真 xlsx,能筛能算能排序,中文不乱码。

中间没有一步需要懂技术。

这就够了。


如果这篇对你有用,关注公众号「火菲」,篇篇能直接上手。下一篇实战见!

在公众号对话框回复【资料】,领取《AI 办公工具清单》。

📚 往期回顾

如果你觉得这篇有用,点个「赞」和「在看」。

也欢迎「转发」给那个每月导完报表、还在对着转圈发愁的同事。

留言区聊聊:你们系统导出的「Excel」,打开要等多久?

① 几秒钟 ② 一两分钟 ③ 直接卡死,早就不指望了

相关学习资料