☰
扫描件PDF转Word/Excel:OCR预处理、表格识别与批量验收
2026/10/24 5:44:11 网站建设 项目流程

简介:ABBYY FineReader 15 免激活版是一套面向办公人员、学生与文档工作者的 PDF 转换与 OCR 识别工具,可直接把普通 PDF 或扫描件转成可编辑的 Word、Excel 文档,省去手工录入与重新排版的时间。压缩包共 1787 个文件、约 621.65MB,含 27 个 exe 主程序与安装组件、24 个 rtf 说明与许可文本、726 个 dll 运行库、121 个 png 界面图标,以及大量 cnnmodel、cnn、tscnn 识别模型与 otf、ttf 字体,另有 Adobe-GB1、Japan1、CNS1 等中日韩 CID 字体编码文件,多语种扫描识别所需组件基本齐备。程序对倾斜、低分辨率扫描页与手机拍摄图有一定容错,转换后可保留原有段落、列表与表格框架,方便二次编辑和数据整理,适合批量处理合同、论文、报表、票据等纸质资料。已有 1537 人学习下载,需要中英日韩混排文档识别与表格还原的读者可直接取用,省去逐个查找语言包与字体的麻烦。

1. 一份两百多页的扫描件,卡住了整条数据流

某公司发来一份两百多页的年度报告扫描件,要求三天内把里面的财务附表变成能参与计算的 Excel。第一反应是打开 PDF 选中复制,粘进表格:一片空白,不是乱码,是压根没有字符,整页就是一张贴进去的图。这种场景下,PDF 转 Word、PDF 转 Excel 的核心不是格式封装,而是 OCR——先把像素认成字,再推断这些字原本排成什么结构。ABBYY FineReader 15 在这条链路上是老牌选手,版面还原和表格结构识别是它的强项,尤其是带合并单元格的财务报表,比多数在线转换工具稳。它适合手头压着成批扫描合同、报表、论文、票据,又需要保留原始排版的人;一年只转一两页的,用在线工具就够了,不必折腾。

2. 先摸清 PDF 的底子:有没有文本层决定后面所有动作

拿到一个 PDF,别急着往转换工具里丢。同一批文件里往往混着两种东西:一种是从 Word、LaTeX 直接导出的“电子版”,文字是真实字符,只是排版被固定住了;另一种是扫描仪或手机拍出来的“图片版”,整页就是一张位图。前者转换是解析,后者转换是识别,两条路的技术栈、耗时、出错方式完全不同。用错方法,你会得到一份看起来像 PDF 截图拼起来的 Word,改一个字整页乱飞。

2.1 用三行代码判断这份 PDF 属于哪一类

我一般先跑一遍探针脚本,把每页的字符数、图片数、页面尺寸打出来,几十页的文件几秒钟就能看完。判断标准很朴素:平均每页字符数低于 50,基本可以判定为扫描件;字符数正常但每页都挂着一张大图,多半是“图片 + 隐藏文本层”的混合体,这种最坑,后面单独说。

import pdfplumber def probe_pdf(path, max_pages=20): """探测 PDF 是否有文本层,输出每页字符数与图片数""" rows = [] with pdfplumber.open(path) as pdf: for i, page in enumerate(pdf.pages[:max_pages]): text = page.extract_text() or "" # 去掉空白后统计,避免只有换行符被误判为有文本 chars = len("".join(text.split())) rows.append({ "page": i + 1, "chars": chars, "images": len(page.images), "width": round(page.width, 1), "height": round(page.height, 1), }) total = sum(r["chars"] for r in rows) avg = total / max(len(rows), 1) kind = "扫描件/图片型" if avg < 50 else "文本型" print(f"抽样 {len(rows)} 页,平均字符数 {avg:.1f},判定:{kind}") return rows, avg rows, avg = probe_pdf("report.pdf") for r in rows[:5]: print(r)

这段代码的关键在extract_text()返回空串的处理。很多扫描件 PDF 里其实塞了一个空的文本对象,page.chars不是空的,但内容全是空白,直接判len(text) > 0会误判成文本型。把空白字符剔掉再计数,判定才准。max_pages默认只抽样前 20 页,是为了在几百页的文件上快速出结论;如果文件前几页是封面、目录这类纯图片页,建议把抽样范围改成随机 20 页,或者干脆全量跑一遍——纯解析文本层,速度足够快。

还有一种情况需要单独处理:扫描件经过别人 OCR 之后,页面上叠了一层不可见的文字,位置和底图对不齐。这类文件的特征是字符数正常,但你把文字复制出来,顺序是乱的,甚至一句话被拆成七八段。判断方法是抽一页,把提取到的文字和肉眼看到的首行做对比,偏差超过两个字就说明文本层不可信,应当按图片型重新识别,别偷懒直接用现成文本层。

2.2 OCR 流水线的四个环节,错一个后面全歪

图片型 PDF 走的是完整流水线,拆开看是四步:图像预处理、版面分析、文字识别、版面还原输出。前两步决定上限,后两步决定观感。很多人的问题出在第三步——识别率上不去就反复换引擎、调参数,实际上瓶颈常常在第一步:原图分辨率不够、页面倾斜三度、扫描时压出了阴影,任何一条都能把准确率从 95% 拉到 80%。

预处理的目标是让文字变成“干净的黑色笔画落在干净的白底上”。灰度化、去噪、二值化、倾斜校正、边框裁切,都是围绕这个目标。版面分析的目标是回答“这一页上有几个区块、哪些是正文、哪些是表格、哪些是页眉页脚、有没有分栏”。这一步出错的表现很有辨识度:双栏论文被读成一栏、表格被拆成散落的段落、页眉的章节名混进了正文第一行。

文字识别环节,ABBYY FineReader 15 这类成熟工具的优势在于内置了多语言模型和版面自适应,中文、英文、数字混排的页面不用手动切分语言区域,它会自己判断。但语言设置仍然要手动确认:一份中英混排的技术文档,只勾了英文,中文部分会整段变成问号或者被当成图形跳过;只勾中文,英文单词容易被拆成单字母。常见做法是中英文都勾上,再把“识别语言”里的主语言设成文档占比更高的那个。

2.3 版面分析决定了 Word 和 Excel 分道扬镳

同样一份 PDF,转 Word 和转 Excel 用的是两套完全不同的还原逻辑,这也是很多人踩坑的地方。Word 的目标是“可编辑的文档流”:段落、标题、列表、图片按顺序排下来,允许有浮动元素,但主体必须是文字流。Excel 的目标是“网格坐标”:每个字属于哪一行哪一列,合并单元格的边界在哪,跨页的表格怎么接上。

所以同一份财务报表,转 Word 出来是一堆用空格和制表符对齐的伪表格,看着像那么回事,一改数字全散;转 Excel 才有机会还原成真正的单元格结构,但前提是版面分析阶段正确识别出了表格线——哪怕原图上表格线很淡,或者干脆是无框线表格靠留白分隔。无框线表格是 Excel 转换的经典难关,工具只能靠文字的行列对齐去猜,猜错一格,整张表的数值就串行了。

提示:转换前先翻三页,确认这份 PDF 里表格是有框线还是无框线。有框线的走“表格识别”模式,无框线的先考虑用 Excel 的“分列”或手工补一遍网格,指望工具一次到位不现实。

3. 预处理:识别率卡在 80% 时,该动的是图像而不是软件

识别率上不去,很多人的第一反应是换工具、换引擎、加钱买更贵的版本。我的经验是,先回头看看喂进去的图。同一份文件,200 dpi 的扫描和 300 dpi 的扫描,识别率能差出十几个百分点;页面歪了两度,表格线识别就开始丢;扫描时书脊压出的阴影,会让整列文字被判定成图形。预处理做扎实,普通引擎也能跑出漂亮结果。

3.1 分辨率、灰度与二值化的取舍

300 dpi 是扫描文档的经验值,不是玄学。低于 200 dpi,小号字(六号、七号)的笔画开始粘连,中文字符的横竖撇捺糊成一团,识别引擎再强也救不回来。高于 400 dpi,收益急剧递减,文件体积和处理时间却成倍上涨,跑几百页的时候这个代价很明显。

场景建议 dpi色彩模式说明
印刷体正文、合同300灰度最通用的组合,速度与精度平衡
小号字、密集表格400灰度表格线细,需要更高采样
带红章、彩色标注300彩色识别用灰度副本,归档保留彩色原件
手写批注、票据400 以上彩色手写体识别本身不稳定,留高分辨率备查

二值化不是必做项。现代 OCR 引擎对灰度图有自适应阈值处理,手动二值化反而可能把浅色笔画直接抹掉。我一般只做灰度化,二值化留给引擎;只有当原图对比度极差、背景有大片污渍时,才手动做一次自适应阈值,并且保留原图做对照,效果不好就退回灰度版本。

import fitz # PyMuPDF from PIL import Image import io def render_page(pdf_path, page_no, dpi=300, gray=True): """把 PDF 某一页渲染成图片,dpi 可控,默认输出灰度""" doc = fitz.open(pdf_path) page = doc[page_no] # zoom 是相对 72dpi 的缩放倍数,300dpi 即 300/72 zoom = dpi / 72.0 mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat, colorspace=fitz.csGRAY if gray else fitz.csRGB) img = Image.open(io.BytesIO(pix.tobytes("png"))) doc.close() return img img = render_page("report.pdf", 3, dpi=300, gray=True) img.save("page3_300dpi.png") print(img.size, img.mode)

zoom这个参数容易写错。有人直接把dpi传进Matrix,结果渲染出一张十几万像素宽的图,内存直接爆掉。正确做法是dpi / 72.0,因为 PDF 的内部坐标单位是 1/72 英寸。colorspace选fitz.csGRAY输出的就是单通道灰度图,后续喂给识别引擎时不需要再转换。如果这一页只用来识别、不需要归档,灰度版本能省下三分之二的体积,批量处理几百页时差别很明显。

3.2 倾斜校正:两度也是病

扫描仪进纸歪一点、书本摊平压不实,页面就会带一到三度的倾斜。人眼看着无所谓,识别引擎的表格线检测是按水平垂直去拟合的,歪两度就可能把一张表切成两半。判断方法很简单:把页面缩略图放大,看表格的上边框是不是和屏幕边缘平行。

校正用 OpenCV 的minAreaRect或霍夫直线都可以。霍夫直线对表格线密集的页面更稳,minAreaRect对文字块更稳。我一般在表格页用霍夫,正文页用最小外接矩形,两个都跑一遍取角度绝对值更小的那个。

import cv2 import numpy as np def detect_skew(img_path, max_angle=15.0): """检测页面倾斜角度,返回角度值(正数表示逆时针倾斜)""" img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 反色 + 二值化,让文字变成白点,方便做形态学膨胀 thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # 水平方向膨胀,把文字连成行,避免单个字干扰直线检测 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5)) dilated = cv2.dilate(thresh, kernel, iterations=1) coords = np.column_stack(np.where(dilated > 0)) if len(coords) < 100: return 0.0 angle = cv2.minAreaRect(coords)[-1] # OpenCV 返回的角度范围是 [-90, 0),换算成有符号倾斜角 if angle < -45: angle = 90 + angle if abs(angle) > max_angle: return 0.0 # 超过阈值多半是整页图表,别乱转 return angle def deskew(img_path, out_path): angle = detect_skew(img_path) img = cv2.imread(img_path) h, w = img.shape[:2] center = (w // 2, h // 2) mat = cv2.getRotationMatrix2D(center, angle, 1.0) # 旋转后用白色填充边缘,避免出现黑角影响识别 rotated = cv2.warpAffine(img, mat, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255)) cv2.imwrite(out_path, rotated) print(f"倾斜角 {angle:.2f} 度,已校正 -> {out_path}") deskew("page3_300dpi.png", "page3_deskew.png")

max_angle这个阈值是后悔药。整页都是图表的页面(比如组织架构图、流程图),minAreaRect会给出一个几十度的荒谬角度,照着转一圈,页面直接横躺。设成 15 度,超过就认为检测不可信、放弃校正,比乱转安全得多。borderValue必须是白色,默认的黑色填充会在页面四角产生黑块,识别引擎会把黑块当成大号图形区域,白白增加处理时间。

3.3 页眉页脚与分栏:先切干净再识别

页眉页脚是 Word 输出的头号污染源。原 PDF 每页顶部有章节名、底部有页码,识别之后这些内容会被当作正文段落插进去,一份两百页的文档里就多出两百行“第X章 xxx”和两百个孤立数字,后续清洗比识别本身还费时间。处理办法有两个:一是在转换设置里启用页眉页脚检测(ABBYY 这类工具自带这个开关,默认开启,但排版不规则时容易漏),二是在预处理阶段直接把页面上下各裁掉 5% 到 8%,用裁切后的图去识别,页码从源头消失。

分栏的处理更讲究。双栏论文、三栏的产品手册,如果按整页识别,引擎可能按从左到右的阅读顺序把两栏的文字交错拼在一起,读起来是“第一栏第一行 + 第二栏第一行 + 第一栏第二行”这种鬼话。判断方法:看页面中间有没有一条明显的竖向留白带。有的话,用垂直投影法找空白列,把页面切成左右两半,分别识别,再按栏序拼接。

import numpy as np import cv2 def split_columns(img_path, min_gap_ratio=0.02): """按垂直投影找分栏切点,返回左右两张图的路径列表""" img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) h, w = img.shape binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # 每列黑像素数量,文字列多、留白列接近 0 col_sum = binary.sum(axis=0) / 255.0 mid_start, mid_end = int(w * 0.35), int(w * 0.65) mid = col_sum[mid_start:mid_end] min_gap = w * min_gap_ratio # 找中间区域最宽的一段近零区间作为栏间距 zero_idx = np.where(mid < h * 0.01)[0] if len(zero_idx) < min_gap: print("未检测到明显分栏,按单栏处理") return [img_path] cut = mid_start + int(np.median(zero_idx)) left = img[:, :cut] right = img[:, cut:] cv2.imwrite("col_left.png", left) cv2.imwrite("col_right.png", right) return ["col_left.png", "col_right.png"]

min_gap_ratio控制判定灵敏度。默认 2% 的页宽作为最小栏间距;版面很挤的双栏文档可以调到 1.5%,但再低就容易把段落的左缩进误判成栏间距,切出一堆碎片。这个函数只处理两栏,三栏以上要把中间区域拉宽到 25%–75%,并且允许检测到多个切点。实际用下来,双栏占绝大多数,先把两栏跑通最划算。

4. Word 与 Excel 的还原路线:两个完全不同的目标函数

转换参数里最容易被忽略的是“输出目标”。同一份 PDF,选“转换为 Word 文档”和选“转换为 Excel 表格”,工具内部的版面重建策略不一样。Word 优先保证文字可编辑、样式连贯;Excel 优先保证坐标对齐、单元格结构正确。用错目标,你得到的是一份能看不能改的文件——看着和原文一模一样,一动手就崩。

4.1 Word:段落流优先,样式继承比位置精确更重要

Word 输出的两种模式,业内叫法不太统一,本质区别是“流式排版”和“绝对定位”。流式排版把识别结果组织成段落、标题、列表,字体字号按原文档推断,段落间距用样式控制;绝对定位则把每块文字放进一个文本框,按原坐标摆放,视觉上和原 PDF 几乎重合。

新手容易被绝对定位的“高保真”骗到,觉得这才叫还原。实际用下来,文本框模式在需要修改时是灾难:改一个词,文本框撑大,压住下面三行;删一段,整页留白;想调行距,得一个个文本框点。我的建议是,只要这份 Word 后续还要编辑,一律选流式排版。视觉上差几个像素的段落缩进、差半号的字体,远不如“能正常改字”重要。

具体设置上,重点看三个开关:保留图片(原始分辨率还是压缩后)、识别表格(转成 Word 原生表格还是保留为图片)、保留页眉页脚。表格那一项,如果原文表格结构简单、没有跨页合并,转成原生表格是值得的;如果表格里嵌了图、有斜线表头、多层合并,转原生表格大概率错乱,不如先转成文字,回头在 Excel 里重做。

4.2 Excel:合并单元格是死穴,跨页表格要手工接

Excel 转换的失败率高,核心原因是表格结构识别的容错空间极小。Word 里错一个字只是错字,Excel 里错一格,整列的数值含义就变了。最常见的三类问题:合并单元格被拆成独立格子、数字被识别成文本、跨页的表格断成两张。

合并单元格的识别依赖表格线检测。原图上合并区域内部没有分隔线,引擎能检测到“这里缺一条线”,进而推断出合并;如果扫描质量差、线条断断续续,推断就会失败,输出一张每格独立的表,看着完整,实际上每一行的项目名只出现在第一格,其余全是空白。补救办法是在转换设置里把“表格识别”的灵敏度调高,代价是容易把无框线表格的留白误判成单元格边界,产生一堆空格子。两头都不完美,实操里我倾向于先按高灵敏度转,再用脚本把空白单元格按左侧值填充,比手工补快得多。

数字变文本是 Excel 的老问题。识别出来的“1,234.56”带着千分位逗号,Excel 认成字符串;“00123”这种前导零,转成数字就丢了零;“2023.10”这种点分隔日期,可能被识别成小数。转换完成后必须做一次类型校验,把所有本该是数字的列统计一遍文本型单元格的数量。

from openpyxl import load_workbook def audit_sheet(path, sheet_name=None): """检查工作表的文本型数字、合并区域与空行分布""" wb = load_workbook(path) ws = wb[sheet_name] if sheet_name else wb.active text_numbers, empties = [], 0 for row in ws.iter_rows(): for cell in row: v = cell.value if v is None: empties += 1 elif isinstance(v, str) and v.replace(",", "").replace(".", "").isdigit(): text_numbers.append(cell.coordinate) print(f"工作表 {ws.title}:{ws.max_row} 行 x {ws.max_column} 列") print(f"合并区域 {len(ws.merged_cells.ranges)} 处") print(f"空单元格 {empties} 个") print(f"文本型数字 {len(text_numbers)} 个,前 10 个:{text_numbers[:10]}") return text_numbers audit_sheet("report_tables.xlsx")

v.replace(",", "").replace(".", "").isdigit()这个判断粗糙但够用,能捞出资深财务表里八九成的文本型数字。真正严谨的做法是逐列判断:一列里超过 80% 的单元格能被float()解析成功,这一列就该是数值列,剩下那 20% 的字符串单独列出来人工看。合并区域数量是个很有用的指标——原表有 12 处合并,转出来只剩 2 处,说明表格结构识别基本失败,这份文件别直接用,回去调参数重转。

跨页表格是另一个高频问题。一张五十行的明细表,被原 PDF 分成两页,转换后会变成两个独立的工作表或同一张表里断开的两个区域。处理方式有两种:一是转换前把两页拼成一张长图再识别,适合页数少的情况;二是转换后用脚本按表头匹配把两段接起来,适合批量。我一般选后者,因为拼图会引入新的对齐误差。

4.3 批量转换的工程化:命名、分批、日志

单文件转换靠手工点没问题,几十上百个文件就必须上脚本。批量处理的核心不是调用转换工具本身,而是三件事:输入文件的命名规范、分批投喂的节奏、失败重试的日志。

命名规范决定了输出文件能不能和源文件对上。我习惯用“项目编号_文档类型_页码区间”这样的结构,源文件本来就乱的话,先用脚本统一重命名,再进转换流程。转换工具本身对中文文件名、长路径、特殊符号的兼容性参差不齐,重命名时顺手把空格、括号、井号去掉,能省掉一批莫名其妙的失败。

分批投喂是为了防止中途崩了前功尽弃。一次丢两百个文件进去,跑了两小时在第一百八十个卡住,剩下的全部重来。按每批 20 到 30 个文件切分,每批完成就落一次日志,崩了从日志里读断点续跑。

#!/bin/bash # 批量转换的外层编排:分批 + 日志 + 断点续跑 SRC_DIR="./pdf_src" OUT_DIR="./docx_out" LOG="./convert.log" BATCH=20 mkdir -p "$OUT_DIR" # 跳过日志里已标记成功的文件 done_list=$(grep -oP '(?<=OK )\S+' "$LOG" 2>/dev/null | sort -u) count=0 for f in "$SRC_DIR"/*.pdf; do base=$(basename "$f" .pdf) if echo "$done_list" | grep -qx "$base"; then continue fi # 调用转换工具的命令行接口,参数按实际工具调整 if convert_tool --input "$f" --output "$OUT_DIR/$base.docx" --lang chi_sim+eng; then echo "OK $base" >> "$LOG" else echo "FAIL $base $(date '+%F %T')" >> "$LOG" fi count=$((count + 1)) if [ $((count % BATCH)) -eq 0 ]; then echo "已处理 $count 个,暂停 5 秒让内存回落" sleep 5 fi done

convert_tool那一行要换成你实际使用的工具的命令行入口,参数名各家不同,--lang这类选项在有的工具里叫-l,有的干脆没有命令行接口、只能走热文件夹(把文件丢进一个监控目录,工具自动处理并输出到另一个目录)。热文件夹模式更省事,但失败时没有返回值,只能靠比对输入输出目录的文件数来判断,日志里记的是“处理前文件数”和“处理后文件数”的差值。

sleep 5这个停顿不是凑数。OCR 是内存密集型任务,连续处理二十个大页面文件,内存占用会一路涨上去,中间停一下让系统回收,能显著降低后半程卡死的概率。这个参数在低配机器上可以调到 10 秒,在高配工作站上改成 0 也无妨。

注意:批量处理前先拿三个不同类型的文件试跑——一份纯文字页、一份带合并单元格的表格页、一份双栏排版页。三个都过了再全量开跑,比跑完两百个再回头查错省几个小时。

5. 避坑与排查:五条踩出来的记录

转换工具用得久了,会发现失败案例高度集中,翻来覆去就是那几种。下面五条是我这几年反复撞上的,按“现象—原因—解决”记下来,遇到了直接对号入座,比翻文档快。

5.1 转出来的 Word 全是文本框,改一个字整个版式崩

现象:打开转换结果,看着和原 PDF 几乎一模一样,但点击任何一段文字,外面都套着一个文本框边框。想改一个错别字,删掉后文本框宽度不变,文字挤成两行,压住下方内容。

原因:转换时选了“绝对定位”或“保留原始版面”这类模式。工具把每一块识别结果按原坐标塞进独立文本框,视觉精度最高,可编辑性最差。这个模式适合做归档、不适合做再编辑。

解决:重转一次,输出模式改成“流式排版”或“适应文本流”。如果文件已经转完、不方便重跑,Word 里有个“删除所有文本框”的思路:全选后剪切,再用“选择性粘贴—无格式文本”,能抢救出文字,但表格和图片会一并丢失,只适合纯文字文档。

5.2 Excel 里数字全变成文本,公式一算全零

现象:表格转出来了,眼睛看着数字都在,选中整列求和结果是 0。点开单元格,左上角有个绿色小三角,提示“以文本形式存储的数字”。

原因:原 PDF 里的数字带千分位逗号、货币符号或者全角字符,识别结果原样保留,Excel 按字符串处理。另一类原因是单元格里有不可见字符,比如从 PDF 带过来的软连字符、不换行空格。

解决:先用“分列”功能把整列过一遍,什么都不改直接点完成,Excel 会重新推断类型;批量处理的话用脚本把字符串里的逗号、货币符号、空格全部剥掉再写回数值。清理不可见字符更省事的办法是=CLEAN(TRIM(A1))套一层,再复制粘贴为值。

5.3 中文标点全变成英文,公式符号变乱码

现象:转换后的文档里,句号变成点、引号变成直的、破折号变成两个短横。更麻烦的是数学公式,积分号、求和号、上下标全部错位,有的干脆变成方框。

原因:语言设置里只勾了英文,或者主语言设成了英文。中文标点在英文模型下会被映射到最接近的 ASCII 字符。公式乱码是因为公式区域被当作普通文本识别,没有走专门的公式识别通道。

解决:转换前把语言列表改成中文加英文,主语言按文档主体设。公式密集的文档,把公式区域单独框选出来,走公式识别或者干脆保留为图片——公式识别目前的准确率在复杂排版上仍然不够看,保留图片反而更实用。

5.4 批量转换跑到一半卡死,进度全部丢失

现象:两百个文件丢进去,跑到一半界面无响应,强退之后重新打开,前面处理完的文件不知道哪些成功了、哪些失败了,只能全部重来。

原因:一次性投喂的文件太多,内存耗尽;或者没有任何进度记录,失败后无法定位断点。

解决:分批投喂,每批二十到三十个,每批结束写一行日志。日志格式固定成“状态 + 文件名 + 时间戳”,恢复时先读日志把成功的文件剔出去。这个习惯一旦养成,后面所有批量任务都能照搬,包括批量转图、批量改名。

5.5 双栏论文被读成一栏,句子前后不接

现象:转换结果读起来是跳跃的,第一段的前半句接着第二段的前半句,语义对不上。把原文和结果并排看,能发现它是按“左栏第一行、右栏第一行、左栏第二行”这样交错的顺序拼的。

原因:整页识别时,引擎按水平投影切分文本行,双栏的两行恰好处于同一水平高度,被当成同一行处理。版面分析没有识别出分栏结构。

解决:转换设置里启用分栏检测;如果工具不支持,就在预处理阶段把页面沿中间切成左右两半,分别识别再按栏序拼接。切分点用第 3 章那段垂直投影代码找,比手工估位置准。

6. 验收这一步别省:把转换结果做成可核对的黑匣子

转换做完不等于活儿干完。我见过太多人把文件转出来直接发给下游,对方打开一看表格串行、数字错位,再退回来返工,来回折腾的时间比转换本身还长。验收的核心思路是抽样加自动比对,用脚本把明显的结构性问题筛出来,人只看筛出来的部分。

具体做法分三层。第一层是结构校验,检查输出文件的段落数、表格数、合并单元格数是否落在合理区间,和源文件的探针数据做对比。第二层是内容抽样,按页数均匀抽五到十页,把识别文本和原图并排打开,重点看数字、单位、人名、专有名词,这四类错了影响最大。第三层是回归比对,如果同一批文档之前转过一版、修过错误,把新结果和修好的旧版本做字符级 diff,差异集中出现的位置就是这次转换的新问题。

import difflib from openpyxl import load_workbook def diff_text(old_path, new_path, top_n=5): """对比两版识别文本,定位差异集中的行""" old = open(old_path, encoding="utf-8").read().splitlines() new = open(new_path, encoding="utf-8").read().splitlines() sm = difflib.SequenceMatcher(None, old, new) print(f"相似度 {sm.ratio():.2%}") for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == "equal": continue print(f"[{tag}] 旧 {i1}-{i2} 行 -> 新 {j1}-{j2} 行") for line in old[i1:i2][:top_n]: print(" -", line[:60]) diff_text("v1.txt", "v2.txt")

ratio()低于 0.95 就值得警惕,说明这一版改动幅度偏大,要么是修了不少错,要么是识别质量整体下滑。get_opcodes()返回的差异块按位置排列,连续几个块集中在同一页,基本可以定位到那一页的扫描质量有问题,回去单独重扫那一页比调参数管用。

Excel 的验收多一步:把合并单元格数量、数值列占比、空行分布这三项指标和源文件的人工统计做对照。数值列占比低于 60% 的表格,多半是类型识别整体失败,别急着往下游发。

验收项合格线不达标时的动作
文本相似度(对比上一版)95% 以上定位差异页,单独重转
数值列占比60% 以上重跑类型清理脚本
合并单元格数量与原件相差不超过 20%提高表格识别灵敏度重转
抽样页人工核对数字、单位零错误该页重扫或手工修正

这套流程跑顺之后,处理一批两百页的扫描件,从探针、预处理、转换到验收,大概能压到半天以内,其中人工核对占大头。我现在的习惯是:任何一批文件开跑之前,先拿三页做全流程试跑,把探针数据、倾斜角、分栏切点、输出结构全部记在一张纸上,全量跑完之后拿这张纸逐项对。有一次省了这一步,两百页的表格转出来合并单元格全丢,重跑了三遍才发现是扫描时纸张放歪了,倾斜角检测一直在返回 0——max_angle阈值设得太保守,把真实的倾斜当成了图表页给忽略了。从那以后,每次批量转换前的那三页试跑,我都强制走一遍,包括那个看起来很多余的倾斜角打印。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询