191MB 招生简章抓下来是「空」的:两次静默失败,和一套先验头部再开引擎的笨办法
引语:卡住我的不是下载慢,是「下载完成」之后所有引擎集体沉默——两次失败,没有一次报错。排查过程和回测过的体检代码全贴在下面,改改就能用。
1. 起点:给学校洞察管道补最后一块数据
我在跑一条全国高校洞察管道,定时任务逐校抓官网,把在校生数据落库。广西医科大学的主站简介页一次命中:全日制本科生 19295 人、研究生 10017 人、来华国际生 435,按口径合计 29747,顺利落库。这条管道队列 2991 所,每个数字都要有出处——来源、URL、口径记进库,凭印象的数一律不落。缺的是分专业招生人数——主站没有,唯一线索是招生网 挂出的 2026 年招生简章,一个 PDF 附件。
下载一看体量:191MB、40 页。管线的老动作是直接抽文字层,我照做,然后开始连环踩坑。
2. 第一个坑:curl 说完成了,文件只有一半
第一次下载给了 300 秒 max-time,超时被砍,磁盘落下一个 102,534,781 字节的文件。坑就坑在:退出码正常、文件头 %PDF-1.4 完整,怎么看都像一份好文档。pymupdf 打开它,不抛异常,只默默返回 pages=0。
引擎沉默,你分不清是没下完、下完了没字、还是根本不是 PDF。转折在文件头第 167 号对象里:/Linearized 1/L 191937132。线性化 PDF 的头几行自报了真实总长 L=191,937,132 字节和页数 N=40,而磁盘上只有 102,534,781——文件自己招了供,引擎一个字没说。
**教训:下载类任务的完整性校验,先读头部声明的 L,再对 size。**两个数字一比,截断、断点、续传起点全都有了。
修复是 Range 续传:curl -r 102534781- 从断点接着拉,HTTP 206 回来 89,402,351 字节。拼接后 102,534,781 + 89,402,351 = 191,937,132,三段算术严丝合缝——文件从「半份」变回完整 191MB,这次头部 L 和磁盘 size 终于对上。
3. 方法论章:先验头部,再开引擎
把判断固化成四步探针(在这次的真实文件上回测过,直接抄):
import os, re
import pymupdf
def pdf_probe(path):
head = open(path, 'rb').read(400).decode('latin-1', 'ignore')
size = os.path.getsize(path)
m = re.search(r'/Linearized 1/L (\d+)', head)
if m and size < int(m.group(1)):
return 'TRUNCATED', 'curl -r %d- 续传' % size
doc = pymupdf.open(path)
if len(doc) == 0:
return 'DAMAGED', '结构损坏,重下或换引擎'
sample = sum(len(doc[i].get_text().strip())
for i in range(min(5, len(doc))))
if sample == 0:
return 'IMAGE_PDF', '渲染位图 + 多模态 OCR'
return 'PASS', '直接 get_text'
它把「引擎的沉默」拆成三种含义:TRUNCATED(没下完,头部自报总长,顺带给出续传起点)、DAMAGED(结构损坏)、IMAGE_PDF(页数全开但一字抽不出)。三种死法三种修法,谁也不冤枉谁。
再配一条给智能体管线的验收提示词(可以直接抄):
你是下载类任务的验收员。任何二进制附件落盘后,依次回答四个问题再宣告成功:
1. 文件头魔数对不对(PDF 应以 %PDF- 开头);
2. 头部声明的线性化总长 L 是否大于磁盘 size——size 小于 L 即截断,输出续传起点;
3. 引擎打开后的页数是否为 0——为 0 不许重试引擎,先回到第 2 步查完整性;
4. 前 5 页文字采样是否全空——全空判图片型 PDF,转渲染加 OCR 通道。
四个问题没答完之前,禁止输出「下载成功」。沉默不等于成功。
4. 第二个坑:40 页全开,文字层是空的
续传补齐,这次 pages=40 正常。我按老经验跑 get_text(),从第 0 页数到第 39 页——每页长度都是 0。不是引擎坏了:这份简章是设计软件整版导出的图片页,每页就是一张大图,根本没有文字层。扩展名对、页数对、结构对,文档类型照样说谎。
修复两步:get_pixmap 渲染 140dpi 位图,喂给多模态 OCR。先拿第 5 页验通道,OCR 稳稳读出师资数据(博士学位教师 1800 人、硕士生导师 1878 位),通道确认可用,再往下走。这里有个容易想岔的地方:图片型 PDF 不算「坏了」,它是合法文档,只是文字层为空——同域的判别武器是文件魔数(09-16 篇写过的 file 路由),判完类型再选引擎,而不是拿一个引擎硬试。
5. 兜底方案:拼版定位,不全量 OCR
40 页全喂 OCR 是浪费。先降采样:30dpi 缩略图按 8 列拼两张 contact sheet,一次多模态调用让模型在缩略图里找「招生计划」章节——定位到 p38/p39 两页。再单独渲染这两页的 140dpi 高清图精读,数字直接出来:本科综合改革四省合计 95 人(天津 15、浙江 35、山东 29、海南 16),广西普通高职计划 1500 人,国家免费医学定向生 62 人。
分工判断一句话:文本层 PDF 走 get_text,图片型 PDF 才值得上 OCR;OCR 之前先用拼版把候选页从 40 压到 2,两次调用干完 40 页的活。顺带把上一轮中断的活收了尾:这条支线本来在「定位招生计划页」一步断掉,拼版图还留在临时目录,捡起来就能续跑——排查产物别急着删,那常常是下一次的起点。
6. 总结
三条判断。一,下载类任务最贵的坑是「看起来成功」——退出码和 %PDF 头都不校验完整性,线性化头部自报的 L 才是硬标准。二,引擎沉默有三种含义:没下完、损坏、图片型,混在一起判就是玄学,四步探针把它们拆开,各修各的。三,图片型 PDF 的正确姿势是拼版定位加逐页精读,不是全量 OCR。
金句收尾:成功不是没有报错,是 size 对得上 L。
- 点赞
- 收藏
- 关注作者
评论(0)