夜雨聆风学习资料网

ARTICLE · 1333092

文档解析批量跑太慢?每份白等0.9秒,100份实测差了93秒

文档解析批量跑太慢?每份白等0.9秒,100份实测差了93秒

先把账算出来:60 页的 Word 合同,常驻进程里转一次 0.35 秒;每份新起一个 Python 进程去转,1.26 秒。

一份差 0.9 秒。一天几十份,看着不多。

但我第一次跑批量任务时是每份文件起一个进程,一百多份文档跑了四分多钟,而且机器风扇一直在响。我以为是自己代码写得烂,查到最后发现,时间全花在启动上。

"

这篇不聊功能,只聊一件事:同一套解析工具,进程怎么起,决定你每天白等多久。

{01}

PART

账要拆成三层才看得清

PART 1

我们做的生意里,文档是持续来的:渠道发来的对账单、客户签的合同、设备回收的报价单。我把这些归类算了一下,每天大概几十份,周末和月初月末会集中爆发。

之前我没在意“一份文件要转多久”,因为单次看都在一秒以内,感觉不出来。直到有一次月初集中对账,我把一批文件丢进去,去泡了杯咖啡,回来才转完一半。

于是我把时间拆成三层,一层一层测。

层
耗时
说明
进程启动 + 导入库
约 0.9 秒
每份文件都有,纯浪费
实际转换
0.3 秒左右
真干活的部分
落盘与日志
忽略不计
本地磁盘

结论摆在明面上:每份文件的固定成本,是实际工作的三倍。

{02}

PART

我测的三个场景

PART 2

同一个 60 页合同,我测了三种起法。

第一种:每份起一个新进程。

...bash

$ time python3 -c "from markitdown import MarkItDown; MarkItDown().convert('contract.docx')"

real    0m1.26s

第二种:一个进程里连续转。

...bash

$ python3 -c "

from markitdown import MarkItDown

import time

md = MarkItDown()

for i in range(3):

    t = time.time(); md.convert('contract.docx'); print('%.2fs' % (time.time()-t))

"

0.35s

0.29s

0.35s

第三种:常驻服务。

我把它挂成一个本地端口,客户端 POST 文件路径上去。第二批文件开始,每份稳定在 0.3 秒到 0.35 秒之间。

一百份文档的差距:

每份起进程:约 126 秒

常驻服务:约 33 秒

差了九十多秒。一个月里月初月末各一次集中对账,一年下来是四十多分钟,还不算我中途要盯着它别卡死。

{03}

PART

顺带发现的第二个成本

PART 3

优化进程之后我以为到顶了,结果输出内容又给我添了麻烦。

60 页合同转成文本是 13909 个字符,看着还行。但里面的条款是同质的——“甲方应在收到账单后十五个工作日内完成付款”这一句,在六十页里重复了几十次。

这种内容塞进模型做检索,等于用一个重复的句子占满上下文。我后来加了一步句级去重,只保留每一条条款的第一次出现,后面重复的折叠成一句“以下若干条款内容重复”。

对账单也类似:3 万行的客户表转成文本,是 165 万字符。真正有价值的是“哪些客户的套餐要到期了”,不是全部三万行。我现在先筛再转,输出从 165 万字符降到几千。

这一步省的不是时间,是后面差点走不通的路。

{04}

PART

我为什么没选更快的方案

PART 4

有人会问,为什么不直接用更快的解析库,甚至自己写?

我试过。对账单这种结构化数据,pandas 读进来直接算,比转成文本快得多。但合同、说明类的 Word 文档,自己写解析要处理标题层级、列表、表格、嵌套结构,我写了两天就放弃了。

所以我的选择是:结构化数据自己处理,非结构化文档交给现成工具,把省下来的力气花在“怎么起进程”这种一次性优化上。

这个取舍我不是很满意。它是妥协——我没有找到一个又通用又快的东西,只能各管一段。如果你的文档类型比较单一,自己写解析会比我这套干净。

{05}

PART

常驻之后踩的新坑

PART 5

说完好处,把常驻带来的新麻烦也说清楚,不然你照着我这套做也会撞上。

第一个:内存不回收。

常驻进程跑一天之后内存涨到接近 1G。我一开始以为是泄漏,翻了半天代码没找到问题。后来把每次转换前后的常驻内存打出来,才发现是每次转换都会临时加载一份文档结构,转完不主动释放。我加了一句显式的对象清理,涨幅就收敛了。

第二个:单次失败拖死整批。

每份起进程的时候,一份失败不影响别人;改成常驻之后,一份文件把进程搞崩,后面排队的全卡住。我第一次遇到是一份损坏的 docx,进程直接退出,队列里剩下的十几份文件全部超时。

后来我给每份文件加了独立的 try 包裹,超时单独掐,失败的那份记日志跳过。这一层保护必须加,不加就不要上常驻。

第三个:端口和重启。

常驻服务意味着它得一直在。机器重启、端口被占、进程被系统杀掉,这些事都会发生。我没有做很复杂的守护,就是一个启动脚本加一次健康检查,服务不起来时告警,不自动重试。

自动重试我是故意不做的。之前试过一次,服务启动失败后自动重试,结果一个死循环跑了二十分钟,日志刷了几百兆。宁可让我收到告警手动看一眼,也不要它自己在那边一直转。

常驻之后的新问题
我的处理
代价
内存不回收
显式清理转换对象
一行代码
单份失败拖死整批
每份独立 try + 超时
约十行代码
服务挂了
健康检查 + 告警
不做自动重试
端口被占
启动前检查占用
启动脚本里两行

{06}

PART

算下来值不值

PART 6

按我这边的量:一天几十份,每份省 0.9 秒,一天省半分钟左右。

单看时间,不值。 为它改代码、加常驻服务、处理重启和端口占用,前期投入的时间比省下来的多,我大概花了两个晚上。

真正值的是另外两件事:

第一,批量任务变得可预期。原来一百份文件跑四分多钟,中间我总想去看一眼;现在三十多秒,丢上去就回来收结果。

第二,不再为了省时间而偷懒。之前因为慢,我会把好几天的文档攒一起跑一次,结果出错时不好定位是哪天的文件。现在快了,随时跑,出问题当场能查到。

这笔账我的结论是:如果一天只处理几份,别优化,用最简单的方式跑就行;如果批量跑、每次几百份,把这 0.9 秒砍掉是值得的。

最后说清楚这篇给不了什么:没测精度、没比各家解析质量,那些是另一个问题;也没给出通用方案,不同工具的启动开销差很多,得自己掐表。

问个具体的:你们批量任务里,每份文件是起新进程还是进程池复用?我是踩了坑才改成常驻的,不知道有没有更省事的做法。

你们批量任务里,每份文件是起新进程还是进程池复用?我是踩了坑才改成常驻的。

点赞
在看
转发

THANKS FOR READING

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料