Repository navigation
实现图片附件的文字识别 - #74
实现图片附件的文字识别#74
Conversation
|
@FengshuoChen 看到你把 PR 转成了待审。分支上还是 10-31 的两个提交,#47 里 11-06 说的两件事还没做:
是还在做比较,还是推送时漏了提交?改好后在这里说一声,我们就复审。比较的做法有疑问也可以在 #47 里问。 |
- 用真实附件比较 PP-OCRv6 三档模型并选 small - 删除真实通知示例,测试改为现场生成 - 在构建阶段准备模型,运行和测试保持离线
JunieXD
left a comment
There was a problem hiding this comment.
感谢!这次的比较做得很扎实:判对的标准写得清清楚楚,每档模型认错了哪些日期、网址、邮箱都逐条列出来了;medium 最准,却因为单独识别就占 3.3GB、给浏览器留不出余量而排除,这个取舍和我们要的一致。crawler/examples/ 也删干净了,测试改成现场生成图片。我们拉到本地装了依赖、跑了 ./check.sh,18 个测试全部通过;还读了 RapidOCR 的源码,用 PR 描述里的 9 张图片重跑了几种设置,核对了下面第 3、4 条。
剩下的问题主要是:分支落后 main,合进去以后图片还没接上读附件;模型的准备有几处多余;识别的设置改一下,内存能降一半以上;文档要按合并后的现状改。
必须改
-
先合进最新的 main,解决冲突:分支基于 10-31 的 main,之后 main 又合进了 #98(读正文和附件)、#100(入口和镜像)等 25 个提交,
crawler/pyproject.toml、crawler/uv.lock、docs/design.md冲突。有冲突时 GitHub 不跑这个 PR 的 CI,所以 27cf2e9 没有检查结果。pyproject.toml保留 main 的全部依赖、[project.scripts]和类型桩,再加上这个 PR 的;uv.lock用uv lock重新生成,不直接取哪一边;design.md以 main 为底,只把文字识别的几处改上去(见第 7 条);- 合进 main 和自己的改动分成两个提交,复审时能看出合并以后改了什么。
-
把识别接进读附件(
crawler/src/crawler/parse/files.py:26-43):#40 已经在 #98 里合并了,现在图片附件落到file_text最后的return "",文件开头的注释写着等 #47;image_text没有调用方,合并后图片附件的文字还是空的。#47 的范围就是「#40 读附件时对图片调用它」,这一步在这个 PR 里做:file_text像认 PDF、ZIP 那样按文件头认出图片(PNG、JPEG 等),调image_text;识别出错时由file_text已有的except转成ValueError,和别的坏文件一样记一条日志;- 改掉
files.py开头注释里等 #47 的那句; crawl/notice.py已经用asyncio.to_thread调file_text,加上image_text里的锁,不会卡住事件循环,同一时刻也只识别一张,那边不用改。
-
模型用安装包自带的,删掉下载:我们装完依赖看过,
rapidocr的安装包里自带 PP-OCRv6 small 的检测、识别模型和方向分类模型;rapidocr download_models --config ocr-models.yaml只输出两次File exists and is valid,什么也没下。所以:- 删掉
.github/workflows/ci.yml:64新加的那一步和crawler/ocr-models.yaml,Dockerfile也不用加下载; image.py:25-33拼模型路径、检查文件在不在的那段删掉,:38-42的model_type和三个model_path也不用写:small 本来就是默认的一档,安装包里的模型在,它就不会联网下载。参数只写第 4 条里和默认不同的几项;- 方向分类现在两处不一致:
ocr-models.yaml写的是use_cls: false,可image.py不读这个文件,RapidOCR 默认use_cls是 true,实际在用。比较时用的是哪种就照哪种写,关掉的话参数里加上"Global.use_cls": False; - 改完用
crawler/Dockerfile构建一次镜像,在容器里识别一张生成的图片,确认镜像里不缺系统库、不联网下载,结果写进 PR 描述。
- 删掉
-
在缩小的图上找行、从原图上切行识别,一次认一行(
image.py:34-44):我们拆开测了一次识别的内存。模型和库约 100MB;检测(找出哪里有字)的内存跟送进去的像素数成正比,max_side_len=4000时单做检测就占 2.0 到 2.6GB;识别默认一次送 6 行、按其中最宽的一行补齐,网页截图的整行很宽,这一步又多占约 1GB。4000 认得准,是因为 RapidOCR 先把整张图缩到max_side_len,再在缩过的图上切行识别,字大了才认得清;其实找行用缩小的图就够,切行可以从原图上切。改成:_engine = RapidOCR( params={ # Find lines on a copy capped at 2000 px but crop them from the original, so small text stays legible. "Global.use_preprocess_img": False, "Det.limit_type": "max", "Det.limit_side_len": 2000, # One line per batch: padding a batch of six to its widest line doubled the peak. "Rec.rec_batch_num": 1, } )
我们在 Mac(M4)上用 PR 描述里的 9 张图片比较过,每张图单独一个进程;关键信息取你在描述里写明原文的 18 项日期、网址和邮箱:
设置 认对 9 张里最高峰值 9 张总耗时 默认( max_side_len=2000)6/18 2094MB 26 秒 max_side_len=400012/18 2298MB 33 秒 上面的设置 12/18 965MB 12.5 秒 一次认一行,认出的文字和一次 6 行只有空格、全角半角这类差别,18 项一项没变。检测限到 3000 多认对 1 项,峰值涨到 1.7GB,18 项里差 1 项算误差,不值得;长图横切成几段分别识别也试过,没有更好。Mac 上的绝对值和 Linux 不同(默认设置你在容器里测的是 807 到 1151MiB,我们这边是 1367 到 2094MB),请在你那个 4 核、3600MiB 的容器里,用同样的 12 个样本把这一档重跑一遍,内存和关键信息全对的张数写进 PR 描述和 §5.2。
-
日志照现有的写法,去掉
psutil(image.py:23、:48-50):- 耗时照
crawl/fetch.py、screening/screen.py记成elapsed,单位秒,用time.monotonic(); - 内存用标准库的
resource.getrusage(resource.RUSAGE_SELF).ru_maxrss记进程的峰值(Linux 上单位是 KiB),去掉psutil依赖。抓取节点怕的是峰值超过 3.6GB,识别完那一刻的 RSS 说明不了这个;标准库做得到的,也不为它加新包(AGENTS.md「依赖从简」)。
- 耗时照
-
测试经
file_text测,挪进files_test.py:- 中文那张留着,报名时间、带时刻的日期都在里面;英文那张和它测的是同一件事,删掉。空白图返回空字符串的那条留着;
files_test.py:83只有 PNG 文件头的那个例子,接上识别以后就不是「没有文字」了,跟着改;- 字体和许可证挪进
parse/fixtures/,和别的测试数据放在一起。
-
补测
text_score,文档只写现状和决定:-
11-06 说的
text_score还没试。PR 里有几处「整行没有可用结果」,可能就是被 0.5 的门槛丢掉的。在第 4 条这一档上把text_score降到比如 0.3 重跑:丢掉的行回来了、又没多出错字,就用它;不然留 0.5。降门槛不多占内存。 -
总体设计 §5.2 的「文字识别」只留样本和判对的标准、表格、选了哪档和为什么、哪些图认不准;9 个来源的链接和逐条的错误放在 PR 描述里就行,「还要补测」这类待办不写进设计。可以写成:
**文字识别**:`data/sources.csv` 的 9 个来源各取一张通知图片(截图、海报、长图、密集的日程表),加一份渲染成图片的 4 页 PDF,在限成 4 核、3600 MiB、断网的 Linux 容器里用 RapidOCR 3.10.0 依次识别。只看日期、时刻、网址和邮箱:同一行里和原文一字不差才算对,一张里错一项就不算全对;有这些信息的共 12 张(页)。 (表格照现在的,加上第 4 条这一档复测的一行) 选 small,在缩到 2000 的图上找行、从原图上切行识别,一次认一行:默认把整张图缩到 2000 再识别,长截图的字太小认不准;把整张图放到 4000 一样准,但检测要多占 1GB 以上;medium 最准,但单独识别就占 3.3GB,给浏览器留不出余量。`text_score` 用 <补测的结论>。三档都会漏掉整行的网址、拆开表格里的日程,图片里认出的关键信息不能当作可靠的原文。 抓取节点同时开 2 个浏览器页面(约 450MB)、一次识别一张图(约 <复测的峰值>),合计约 <合计>,3.6GB 内存够用(第 16 节)。
-
§5.1 那一行 main 已经改写过:只把「文字识别候选 RapidOCR」改成「图片的文字识别用 RapidOCR(PP-OCRv6 small,设置见 §5.2)」,「文字识别在……的任务里定」改成「扫描的 PDF 用哪个库渲染成图片,在……的任务里定」;
-
§16 抓取节点那一行的「内存峰值约 1.4GB」按复测的结果改,不写「仍需复测」;
-
§18 「文字识别用的库」那一行改成「扫描的 PDF 用哪个库渲染成图片」还没定。
-
说明
- 扫描的 PDF:#98 选的 pypdf 只读文字层,不能把页面渲染成图片,要另选一个渲染的库(不用 AGPL 许可的 PyMuPDF)。这部分留在 #47 里另开 PR,选库前先在 #47 说一声;这个 PR 不关闭 #47。PR 描述里「PDF 的下载、渲染和接入仍由 #40 处理」「#40 尚未实现附件读取」这几句跟着改。
- 样本数量:Issue 写的是 15 到 20 张,这次是 9 张。medium 是因为内存排除的,多几张也改变不了选择,这次不用补。
- 识别错了的值:比如把
2026-08-28认成2025-08-28,这个错的日期在快照里照样「原样找得到」。证据只在图片里的字段送进审核页,这是网站那边的事,我们来做,这个 PR 不用管。 - 让 DeepSeek 直接读图:抽取用的 DeepSeek V4.1 Flash 已经能读图片,能不能用它代替本地识别,另开了 #102 做实验,有兴趣可以认领;这个 PR 照上面的改,继续用 RapidOCR。
- 改完在这里说一声,我们就复审。推了新提交后 CI 要我们批准一次才会跑;删掉 workflow 那一步以后,这个 PR 就不改
.github/了。
|
已按这轮意见改完并推送,请复审:
本次最终 diff 不再修改 |
锁文件以 main 的为底重新生成,只加上识别要用的包,已有的依赖不跟着升级。Playwright 留在 1.64.1,它自带的 Node 不要 libatomic,镜像也就不用再装。
JunieXD
left a comment
There was a problem hiding this comment.
感谢!上一轮的七条都改到了:合进 main 和自己的修改分成了两个提交;图片经 file_text 识别,坏图片和别的坏文件一样转成 ValueError;用安装包自带的模型,不再改 workflow;设置照第 4 条改了,容器里复测的峰值从 1151 MiB 降到 726 MiB,全对的从 4 张升到 7 张;text_score 也补测了,§5.2 写得清清楚楚。
剩下一处我们直接改了(89f1b10):uv.lock 原来是整个重新解析的,除了识别要用的 16 个新包,还把 14 个已有的依赖升了版本,比如 Playwright 1.64.1 → 1.65.1;Dockerfile 要装的 libatomic1 就是 1.65.1 自带的 Node 要的,1.64.1 不要。我们以 main 的锁文件为底重新跑了 uv lock,已有的依赖保持 main 的版本,Dockerfile 退回 main 的写法。以后依赖升级单独开 PR 就好(AGENTS.md「提交和 PR」)。改完以后本地 ./check.sh 通过,镜像不装 libatomic1 也能构建,在断网的容器里识别现场生成的图片,三行都认对了。
这就合并。扫描的 PDF 还留在 #47,选渲染的库之前在那边说一声。
关联
关联 #47 的图片识别阶段。#98 已接通正文和附件读取;扫描 PDF 的渲染库另在 #47 后续 PR 里选择,本 PR 不关闭 #47。
改了什么
max_side_len=2000/4000,统一使用默认text_score=0.5。text_score=0.5/0.3后保留默认 0.5。file_text按文件头识别 PNG、JPEG、GIF、BMP、TIFF 和 WebP,并调用图片识别;模型只加载一次,用锁保证同一时刻只识别一张。file_text现场生成中文图片和空白图片。libatomic1,使crawler/Dockerfile能完整构建。crawler/examples/,不把真实通知图片和识别结果提交进仓库。“完全正确”的标准
本次只把日期、时刻、网址和邮箱作为关键信息,先逐项人工抄录图片原文作为答案。一项关键信息满足下面全部条件才算正确:
一张图片或一页 PDF 只有在所有人工标注的关键信息都满足以上条件时,才计入“关键信息全对”。漏掉或错识别任何一项,整张(页)都不算全对。普通正文中的少量错字不影响这个指标;如果 OCR 输出图片中不存在的关键字段,也必须交给人审核,不能作为原文证据。
比较结果
测试环境为 Linux 容器、RapidOCR 3.10.0 和 ONNX Runtime 1.31.1,使用
--cpus=4 --memory=3600m --network none;模型提前放入镜像,每档只创建一个实例并顺序识别。4000/0.5的逐样本结果如下;“—”表示该页没有关键信息,不进入分母。最终设置(small、找行 2000、原图切行、每批 1 行、
text_score=0.5)的逐样本结果如下。2000/0.5的逐样本结果如下。max_side_len=2000每张(页)的耗时和容器内存峰值max_side_len=4000每张(页)的耗时和容器内存峰值三档模型在
2000/0.5下的具体错误以下只列日期、时刻、网址和邮箱的错误;普通正文错字不列。
PP-OCRv6 tiny
https: //yjspt.. xidian. edu. cn/gsapp/sys/wdyjsbm/ent.rance. do;导师网址被插入空格并把dsjies拆错;cs@xidian.edu.cn被识别为cs@xidian. cdu. cn。2026-08-28被识别为2025-08-28;报名网址被识别为带空格且把TMS写成TM5的https://zhaosheng.ucas.ac.cn/sign_ up/TM5/views/index.aspx。http://gsas.seu.edu.cn被识别为http://gsas.scu.cdu.cn;2025年7月2日17:00—8月1日24:00中两个“日”被识别为“口”;rjyjszs@126.com被识别为rjyjszs(@126.com。2026年9月14日14:00前被识别为2028年9J14口14:00前;日程中的日期、时刻被漏掉或拆行;两个邮箱均未识别出来;末尾2026年9月7日被识别为2026年3月7日。https://ssyjsbm.xmu.edu.cn/被识别为https://ssy.jsbm.xmu. edu.cn/;邮箱和学院网址被插入空格。https://ssyjsbm.xmu.edu.cn/在.c后换行,http://yz.chsi.com.cn/tm在chsi.后换行。scseyjs@uestc.edu.cn、shensuyjs@uestc.edu.cn均被错识别。.aspx被识别为.asp×。7月6日10:00-8月10日17:00被识别为7月6日 10:00-8月 10日 17:00。7月13日至8月25日被识别为7月 13 日至8月 25日。PP-OCRv6 small
yjsjy被识别为xjsjy;cs@xidian.edu.cn被识别为cs@xidian.cdu.cn。2026-08-28被识别为2025-08-28;报名网址所在整行没有可用结果。http://gsas.seu.edu.cn被识别为http://gsas.scu.cdu.cn。2026年9月14日14:00前被识别为202±年9,14日14:00前;日程中的日期、时刻被漏掉或拆行;rjgczs@uestc.edu.cn未识别出来,rjss@uestc.edu.cn被识别为rjss@uestc.eru.cr。https://ssyjsbm.xmu.edu.cn/被识别为含内部空格且缺少末尾/的https://ssyjsbm. xmu.edu.cn。.c和chsi.后换行。edu被识别为odu;结果网址被插入空格并把scse写成sese;多项日程被拆行或把半角冒号识别为全角冒号。jsj_yjsjw@bupt.edu.cn被识别为jsj-yjsjw@bupt.edu.cn。PDF 中的关键信息均能原样识别。
PP-OCRv6 medium
cs@xidian.edu.cn被识别为cs@xidian.cdu.cn。http://gsas.seu.edu.cn被识别为http://gsas.scu.cdu.cn。2026年9月14日14:00前被识别为2028年9月14□14:00前;日程中的日期、时刻被漏掉或拆行。https://ssyjsbm.xmu.edu.cn/被识别为带空格的https://ssyjsbm. xmu.edu.cn/。.c和chsi.后换行。2026年9月15日下午17:00的报名截止时间整行;一处报名网址把yzbm写成vzbm并漏掉点号;研招网和结果网址被插入空格;多项日程被拆行,9月20日被漏掉。7月6日10:00-8月10日17:00被识别为7 月6日10:00-8 月 10日17:00。三档模型在 2000 像素下都会漏掉或错识别关键字段;small 和 medium 同为 4/12 张(页)全对,但 medium 更慢、内存和模型文件更大。密集表格、跨行网址和日程仍须交给人审核,不能根据上下文补全。
选型和边界
在
4000/0.5下,medium 的识别效果最好;它比 small 多完整识别图片 2 和图片 3,图片 9 也只有 medium 全对。但 medium 在图片 2 上的容器内存峰值已经达到 3305.9 MiB,距 3600 MiB 上限只剩约 294 MiB,而且这次没有同时运行两个浏览器页面,不能满足抓取节点还要运行浏览器的约束。因此排除 medium,选择 PP-OCRv6 small:在缩到 2000 的图上找行、从原图切行,每批识别 1 行。它有 7/12 张(页)关键信息全对,13 个输入共 240.87 秒,进程峰值 726.0 MiB;与同时打开 2 个浏览器页面的约 450 MiB 合计约 1.2 GiB。
把
text_score从默认的 0.5 降到 0.3 后,13 个输入的关键信息一项没变,只在图片 2、图片 8 各多出一行乱码,也没有降低峰值,因此保留 0.5。最终设置仍会拆开长网址和密集表格中的日程,相关字段必须交给人审核,不能根据上下文补全。最终设置中未全对的 5 张图片,具体错误如下:
9月14日14:00前识别成单独的9月14,另有多项日期、时刻漏掉或拆行。https://ssyjsbm.xmu.edu.cn/漏掉末尾/。https://ssyjsbm.xmu.edu.cn/和http://yz.chsi.com.cn/tm都在域名中间断行。logon识别成1ogon,并漏掉9月15日下午17:00中的17:00;多项日程的空格和半角冒号也与原文不同。jsj_yjsjw@bupt.edu.cn被识别为jsj-yjsjw@bupt.edu.cn。输入图片
共 9 张输入图片,点击缩略图可查看原图。
样本分别来自:中国科学院自动化研究所、西安电子科技大学计算机科学与技术学院、中国科学院计算技术研究所、东南大学计算机科学与工程学院、软件学院、电子科技大学信息与软件工程学院、厦门大学人工智能研究院、厦门大学信息学院、电子科技大学计算机科学与工程学院和北京邮电大学计算机学院。仓库中不提交这些图片、PDF 或 OCR 全文。
怎么验证的
--cpus=4 --memory=3600m --network none,模型在离线环境中读取。crawler/Dockerfile成功构建镜像,并在--network none下识别现场生成的图片;日志确认三份 small 模型都来自 RapidOCR 安装包。text_score=0.5/0.3;0.5 为 240.87 秒、726.0 MiB,0.3 为 257.09 秒、715.0 MiB。crawler/check.sh:接口类型生成检查、Ruff 格式和 lint、严格 Pyright 均通过,pytest 268 项测试全部通过。影响
psutil,不修改网站接口或 MCP 工具。file_text调用 OCR;坏图片和其他坏附件一样转成ValueError,由现有抓取流程记录失败。crawler/Dockerfile安装libatomic1,供 Playwright 自带的 Node 在 Python 3.14 slim 基础镜像中启动;不增加部署单元或端口。