尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MATLAB读取PDF全攻略:文本提取、表格识别与OCR处理

MATLAB读取PDF全攻略:文本提取、表格识别与OCR处理 上个月在处理一批评测报告时又碰到了老问题——客户发来几十个PDF文件全是技术手册和数据公报我得把里面的测试条件、指标数值、结论段落逐一捞出来整理成Excel。一开始我想得很简单MATLAB里不就有现成函数么结果第一份PDF就给了我一个下马威扫描版合同读出来全是空白另一份带复杂表格的技术报告文本是出来了但表格列全错位根本没法直接用。后来我把MATLAB下PDF文件操作读取这件事完整梳理了一遍发现它其实要拆成好几条路线来处理没有哪个万能函数能一招吃遍所有PDF。这篇就把我实测过的方法、踩过的坑、能直接拿去改的脚本都写出来供正在用MATLAB处理论文、报告、说明书、数据公报或者需要成批PDF自动提取信息的朋友参考。1. MATLAB读PDF的路线选择先搞清楚PDF的成色再动手MATLAB不像处理Excel那样有一个类似readtable的readpdf函数这是很多人第一次尝试后都会疑惑的点。原因说穿了也简单PDF本质上是一套页面描述语言文本、图片、矢量图形、表格线条全部以绘图指令的形式混排在一起并不像Excel那样自带单元格或行这样的结构化语义。所以处理PDF之前必须先判断你手里的文件属于哪种类型再决定走哪条技术路线。1.1 文本型、表格型、扫描件三种PDF的根本区别按照读取难度我习惯把PDF分成三种成色文本型PDF。使用鼠标选中文字时能够逐字高亮。这类PDF内部有真正的文本对象和字体编码是最容易提取的一类绝大多数Word、LaTeX导出的PDF都属于这一类。表格型PDF。文字同样可以选中但其中包含大量表格区域。PDF里的表格其实只是若干条线段和浮动文字块并没有第几行第几列这样的概念。普通文本提取工具读完之后表格单元格的顺序通常是混乱的列与列之间的关系完全丢失。扫描件PDF。整页就是一张图片文字本质上是像素点鼠标选中没有任何反应。这种PDF必须走OCR光学字符识别才能拿到可编辑文本任何基于文本层的提取工具对它都无能为力。搞清楚这三者的区别基本就决定了后续80%的工作量。我见过太多人在扫描件上反复尝试extractFileText最后只得到一个空字符串然后怀疑代码写错了——其实方向从一开始就偏了。1.2 方案对照表与我的选型建议以一个做技术文档处理的长期项目视角我把适合MATLAB场景的几种方案整理成了一个对照表PDF类型推荐方案工具来源输出效果适用场景文本型extractFileTextText Analytics Toolbox按页面顺序输出纯文本快速抽取正文、段落清理文本型处理中文更稳pdftotext命令行工具Poppler外部命令保留一定版面布局的文本中文手册、英文论文批量抽取表格型pdfplumberPython库通过MATLAB调用Python能还原表格行列结构数据公报、测试报告、财务报表扫描件OCRMATLAB内置ocr或pytesseract识别图片中的文字扫描合同、盖章文件、历史档案我的默认建议是先用最轻量的方式验证PDF是不是带文本层。如果extractFileText能读出一段话就按文本型处理如果读出来是有文字但表格数据全部错位就上pdfplumber如果读出来是空的再考虑是否扫描件。2. 文本型PDF读取extractFileText实战与中文乱码排查对于大多数普通PDFextractFileText是最直接的工具。这个函数来自Text Analytics Toolbox如果你的MATLAB版本许可里包含这个工具箱直接用就行。可以先在命令窗口输入ver查看已安装工具箱确认是否有Text Analytics Toolbox没有的话有学校或单位授权的完整版安装包一般都会带上。2.1 用extractFileText把正文抽出来基础示例最基础的用法非常简洁% 提取整个PDF的纯文本内容 txt extractFileText(report.pdf); disp(strlength(txt)); % 查看前500个字符确认是否读到了有效内容 headLines extractBetween(txt, 1, 500); disp(headLines)extractFileText返回的是MATLAB的string类型可以直接使用strlength、extractBetween、contains等字符串函数继续处理。这比传统用fopen逐行读文本文件还要省事。如果PDF页数很多可以指定只提取某一页或某几页% 只提取第2页到第5页 txtPart extractFileText(report.pdf, Pages, [2 5]); % 只看第3页 txtPage3 extractFileText(report.pdf, Pages, 3);这个按页提取的能力在实际项目中非常实用。比如某份技术公报前面十几页都是目录和修订记录真正有价值的数据集中在最后几页那就直接把Pages参数指过去省得后面还要在长文本里做切割。提示如果系统里同时有多个MATLAB版本注意确认当前使用的是哪一个版本的工具箱路径。实际遇到过一次低版本MATLAB因为工具箱不完整extractFileText这个函数根本不存在的报错。读到文本后下一步通常是做清洗和结构化。我一般是这样处理的% 把换行符统一成char(10)方便后续按行处理 txt replace(txt, char(13), ); lines splitlines(txt); lines strip(lines); lines(lines ) []; % 删除空行 % 如果想定位包含某个关键词的行 targetLines lines(contains(lines, 测试条件)); disp(targetLines);这样一套下来从PDF到可分析的行文本就完成了。2.2 中文乱码与内嵌字体90%的人会遇到的坑文本型PDF读取最让人崩溃的不是读不出来而是读出来全是乱码尤其是中文资料。现象是英文和数字正常中文变成一个个方框或问号有些甚至变成韩文和日文编码的样子。我自己追过一次这个问题的根源PDF为了保证跨平台显示一致通常会把用到的字体子集内嵌进文件里。问题是很多转换工具在嵌入中文字体时并没有提供完整的Unicode映射表PDF标准里对应的是ToUnicode CMap提取工具只能靠猜测把字形编码对应到字符猜错了就出乱码。这不是MATLAB一个工具的问题任何基于文本层的提取工具遇到这类PDF都会翻车。碰到这种情况我的排查顺序是这样的第一步先换pdftotext试试。这是Poppler工具集里的命令行程序对字体映射的处理经验非常丰富。Linux下直接sudo apt install poppler-utilsWindows下可以从Poppler的Windows构建版下载并加入PATH。% 调用外部命令读取PDF-layout参数尽量保留版面布局 [status, result] system(pdftotext -layout report.pdf -); if status 0 extractedText string(result); else error(pdftotext执行失败); end实际经验中pdftotext对中文PDF的处理往往比MATLAB内置函数更稳成功率和字体映射兜底能力都强不少。我用它解决过好几个extractFileText乱码的PDF。第二步如果pdftotext也乱码那大概率是PDF生成端的编码就非常规。这时候别跟文本层死磕直接转到OCR路线上后面第五节会详细说。第三步如果你还有源文件去源头上做修改。在Word、LaTeX、WPS里导出PDF时尽量选择标准PDF字体或完整嵌入字体不要用仅嵌入所用的子集配合优化压缩的方案能大幅降低下游提取乱码的概率。这个属于上游建议但对批量处理别人发来的PDF帮助有限。注意extractFileText无法处理加密的PDF如果文件设置了打开密码函数会直接报错。关于密码和权限的处理我在第五节会展开。3. 表格型PDF提取借力pdfplumber把单元格数据带回MATLAB如果确认PDF里有大量表格那extractFileText这类方案只能当备胎。拿一份测试报告举例我用extractFileText提取后表格里温度 25°C 湿度 60%RH这种单元格内容被拆得乱七八糟原本在一行里的三列数据变成了三行完全没法对应起来。3.1 为什么PDF表格这么难提取布局语义的缺失要理解这个问题你得知道PDF里的表格到底长什么样。PDF中没有表格对象它只有几样基本东西文字对象带位置坐标、线条或矩形路径、图片。你看到的表格边框线只是一堆画线指令单元格里的文字也只是根据坐标散落排布。任何表格提取工具干的事情归纳起来就一句话用线条交叉点和文字坐标去反推单元格的边界再把落进每个边界的文字拼成单元格内容。所以表格提取的成功率极其依赖原始PDF中线条是否完整、文字是否对齐整齐。有的PDF为了美观用了很细的边框线、有的表格没有线条只用底色区分、有的单元格跨页——这些都会让表格提取工具失灵。pdfplumber之所以在社区口碑不错是因为它对线条和文字的坐标聚类做得比较细还能通过参数手动调教提取策略。MATLAB里没有直接对标的功能但MATLAB可以通过Python接口调用pdfplumber反而走出了第三条路。3.2 MATLAB调用pdfplumber的完整示例先做好环境准备。假设你已经安装了Python版本3.8到3.11都可以具体看你的MATLAB版本兼容性在命令行里安装依赖pip install pdfplumber openpyxl pandas然后在MATLAB里确认Python环境已被正确识别% 查看当前MATLAB使用的Python pyenv % 如果路径不对手动指定Python解释器 % pyenv(Version, C:\Python311\python.exe);确认环境没问题后就可以正式调用了。下面是一段我调通后一直在用的代码作用是把PDF第一页的表格完整提取到MATLAB的cell数组里% 导入需要的Python模块 py.importlib.import_module(pdfplumber); % 打开PDF文件 pdfObj py.pdfplumber.open(test_report.pdf); try pages pdfObj.pages; page pages{1}; % MATLAB访问Python列表元素用大括号 % 提取该页所有表格 tables page.extract_table(); if isempty(tables) error(该页面没有检测到表格); end % pdfplumber返回的是Python的list of list先转成MATLAB cell rowList cell(tables); numRows numel(rowList); colList cell(rowList{1}); numCols numel(colList); result cell(numRows, numCols); for r 1:numRows vals cell(rowList{r}); for c 1:numCols val vals{c}; if isa(val, py.NoneType) % 单元格为空时pdfplumber返回None result{r, c} missing; else result{r, c} string(val); end end end % 展示结果 disp(result); % 如果第一行是表头可以这样生成表格对象 dataTable cell2table(result(2:end, :), VariableNames, result(1, :)); % 导出为Excel这一步就自然衔接了MATLAB读取excel数据的周边需求 writetable(dataTable, extracted_table.xlsx); finally % 关闭pdf对象避免文件被占用 pdfObj.close(); end这段代码有几个细节值得说明。第一py.pdfplumber.open()返回的对象在MATLAB中是个py.pdfplumber.PDF对象使用完务必调用close()。我一开始不熟悉连续处理几十个PDF后系统提示文件被占用排查了半天才发现是没关句柄。第二tables page.extract_table()提取的是该页检测到的第一个表格。如果一页里有多个表格pdfplumber还提供了extract_tables()复数方法返回的是所有表格的列表。实际项目中一页多个表格的情况非常常见比如上半部分是参数表、下半部分是误差分析表这时候要遍历每个表格allTables page.extract_tables(); for t 1:numel(allTables) singleTable cell(allTables{t}); % 继续处理每个表格... end第三关于跨页表格。pdfplumber默认只在单页内提取表格如果一个表格从第3页延续到第4页那两页会各自提取出半个表格。处理跨页表格时我通常的做法是按页提取后检查首列是否重复如表头重复出现然后手动拼接两个表。没有特别完美的纯自动方案因为跨页表格的分割逻辑在不同PDF中差异太大。第四合并单元格的情况。pdfplumber对于合并单元格在被合并的空白区域会返回None即上面的py.NoneType判断分支。你需要在处理时决定填充规则向前填充把上方非空值往下带还是向后填充。一般报表习惯是合并单元格只在起始位置有值其余为长空白所以用缺失值填充为上一个有效值的策略比较合适。我提供一个填充缺失值的辅助代码片段% 将当前列中missing值替换为上方最近的非missing值向下填充 function data fillMissingDown(data) for c 1:size(data, 2) lastValid ; for r 1:size(data, 1) if ismissing(data{r, c}) data{r, c} lastValid; else lastValid data{r, c}; end end end end实测下来这套组合在多数规章类PDF表格上表现不错尤其是那种线条完整、文字对齐的表格提取准确率能做到95%以上。4. 批量处理与自动导出把一个文件夹的PDF变成一张Excel清单日常工作中最常见的场景不是读单个PDF而是有人一次性丢来几十个PDF要求从里面提取某个字段、某个表格整合到一张汇总表里。这种批量需求才是最体现效率的地方。4.1 文件遍历与信息提取脚本骨架% 指定PDF所在文件夹 folderPath D:\test_reports\; files dir(fullfile(folderPath, *.pdf)); % 预分配结果存储 numFiles length(files); fileNames strings(numFiles, 1); summary strings(numFiles, 3); % 假设每份文件提取3个关键字段 for k 1:numFiles fullPath fullfile(folderPath, files(k).name); fileNames(k) files(k).name; try % 读取文本层 txt extractFileText(fullPath); % 用正则或关键词定位字段 % 例提取型号ABC-123中的型号 pat 型号[:]\s*([A-Za-z0-9\-]); match regexp(txt, pat, tokens, once); if ~isempty(match) summary(k, 1) string(match{1}); end % 提取第二个字段比如测试日期 datePat 日期[:]\s*(\d{4}-\d{2}-\d{2}); matchDate regexp(txt, datePat, tokens, once); if ~isempty(matchDate) summary(k, 2) string(matchDate{1}); end % 统计字符数作为质量指标 summary(k, 3) string(strlength(txt)); catch ME % 记录失败原因不要把整个循环中断 warning(处理失败: %s原因: %s, fullPath, ME.message); summary(k, :) [读取失败, , ]; end end % 汇总成表格 resultTable table(fileNames, summary(:,1), summary(:,2), summary(:,3), ... VariableNames, [文件名, 型号, 日期, 提取字数]); % 导出Excel writetable(resultTable, summary.xlsx);这段脚本的精髓在try-catch的使用。批量处理时最怕某个PDF格式异常导致整个循环崩溃前面处理完的全部白干。我始终用try-catch包住单文件处理逻辑失败文件在warning里输出并继续往下跑全部跑完后可以再针对失败文件单独排查。正则表达式这块是提取效率的分水岭。先用extractFileText拿到全文再用regexp定位小字段比去读整页表格要快得多而且对格式轻微变化容忍度更高。写正则时注意中文冒号和英文冒号都覆盖一下用[:]这种方式因为不同PDF生成工具里的标点经常不统一。4.2 并行加速与异常保护跑批时不中断如果PDF文件数量上百且每个文件都有几十页单循环可能会跑很久。这时候可以用parfor把循环并行化尤其适合CPU多核环境。% 启动并行池 parpool; % 预分配结果 results cell(numFiles, 1); parfor k 1:numFiles fullPath fullfile(folderPath, files(k).name); try txt extractFileText(fullPath); results{k} [files(k).name, string(strlength(txt))]; catch ME results{k} [files(k).name, FAILED: ME.message]; end end但是有几个并行处理的注意事项都是过来人才知道的坑。第一parfor循环体内不要直接写Excel或写入共享文件。每个worker进程各自持有一份结果直接并行写入会出现文件锁冲突。正确做法是把结果存进cell数组循环结束后在client端统一writetable导出。第二调用外部工具比如pdftotext时确认工具路径在每个worker上都能访问。如果是网络映射驱动器上的读库并行时会放大网络IO瓶颈不如本地拷一份再跑。第三如果某个PDF包含需要解密或访问外部资源的操作并行时可能会出现资源竞争。遇到这类文件我倾向于把可疑文件单独提取出来串行重试一遍。5. 扫描件与加密PDF的破局思路OCR与权限限制的处理前面几条路线都建立在PDF有文本层基础上但现实中总有一部分PDF是扫描件或加密文件。这两类文件用extractFileText读出来基本是空白需要换个思路。5.1 怎么判断一个PDF是扫描件先做一个快速判断。对每个PDF执行一次文本提取如果返回的字符串长度非常小比如小于10个字符而文件页数又不少那基本可以断定是扫描件。也可以直接在PDF阅读器里尝试用鼠标选中文字选不中的就是扫描件。还有一个技巧拿到PDF文件后先用dir看一下文件大小。纯扫描件的PDF一般体积较大因为每页都嵌入了一张高分辨率图片。文本型PDF如果没内嵌大量图片体积通常明显更小。虽然这个规律不绝对但作为快速筛选条件足够了。5.2 借助pytesseract做中文OCRMATLAB里的完整流程对扫描件最直接的办法是OCR。MATLAB自带的Computer Vision Toolbox里也有ocr函数处理印刷体英文效果不错但是对中文支持相对有限尤其是带特殊字体的扫描件。实测下来如果只是英文报告MATLAB内置ocr足够但中文扫描件我更推荐用pytesseract配合简体中文语言包再通过MATLAB的Python接口调用。技术路线是这样的先用pdftoppmPoppler工具集的一部分把PDF的每一页转成PNG图片再用pytesseract对图片做OCR识别。% 第一步把PDF转成图片 [status, ~] system(pdftoppm -png -r 300 scan_report.pdf page); if status ~ 0 error(pdftoppm转换失败); end % 转换成功后当前目录会生成page-1.png、page-2.png等文件 pngFiles dir(page-*.png); % 第二步通过Python调用pytesseract识别 py.importlib.import_module(pytesseract); py.importlib.import_module(PIL); fullText ; for k 1:length(pngFiles) imgPath fullfile(pwd, pngFiles(k).name); img py.PIL.Image.open(imgPath); % lang参数指定中文简体注意需要提前下载好语言包 text py.pytesseract.image_to_string(img, pyargs(lang, chi_simeng)); fullText fullText string(text) newline; end这里有个参数值得强调-r 300表示300 DPI渲染分辨率。OCR识别率和渲染分辨率强相关我实测过150 DPI下识别准确率明显下降300 DPI时基本够用碰上字号特别小的可以提到400甚至600但代价是转换时间和磁盘占用会成倍增加。pytesseract需要安装Tesseract OCR引擎本体。Windows下安装Tesseract时在安装界面勾选中文语言包或之后手工把chi_sim.traineddata放进tessdata目录。Linux下用sudo apt install tesseract-ocr tesseract-ocr-chi-sim。这个环节跟MATLAB本身没有关系但部署环境时最容易漏掉。OCR输出后的文本同样可以走前面讲过的字符串清洗、正则提取流程。说实话扫描件的OCR提取很难做到100%准确尤其盖章、手写、反光区域。我在实际项目中会把OCR结果和原文图片并排保存方便人工复核时对照。5.3 关于加密PDF只在合法授权范围内处理加密PDF也是日常很容易遇到的情况。需要注意PDF的加密分两种一种是打开密码用户密码没有密码连打开都做不到另一种是权限密码所有者密码文档能打开看但限制了复制、打印或编辑。extractFileText遇到这类文件通常会报错错误信息里带encrypted字样。权限密码的场景下如果你本身就是文档的接收方日常工作需要提取文字做整理可以在有合法权限的前提下使用Python的PikePDF或PyPDF2库去除权限限制后读取。下面是一段用PikePDF处理的示例py.importlib.import_module(pikepdf); % 打开加密PDF密码参数按实际情况填写 pdfFile py.pikepdf.open(protected.pdf, pyargs(password, 你已知的密码)); % 保存为无权限限制的临时文件 pdfFile.save(unlocked_temp.pdf); pdfFile.close();之后就能用extractFileText正常读取unlocked_temp.pdf了。但这里我要特别说清楚这些操作仅适用于自己拥有合法权限的文档比如你是文档接收者且文档的控制密码本就应该允许你做文字提取。对没有授权或非法获取的文档做任何解密绕过操作是不允许的这条红线不能碰。我在公司内部处理PDF时都会先和文档提供方确认文件是否允许文本提取否则宁可用人工录入也不去碰权限限制。至于需要打开密码且你不知道密码的PDF从技术上讲不是不能暴力尝试但那个领域我不碰也不建议任何人把时间花在这上面。关于这套方法的一点个人体会用MATLAB做PDF文件操作读取折腾下来最大的感受是不要指望一个函数解决所有PDF。PDF格式太开放生成工具千奇百怪谁也不敢保证100%提取成功。我现在的标准工作流是先extractFileText快速试探有表格再上pdfplumber文本乱码就换pdftotext兜底遇上扫描件才启动OCR流程。每处理一类文件都把失败原因记到日志里积累一段时间后你对自己经常接触的那批PDF会非常熟悉哪些能自动跑、哪些必须人工看心里立刻有数。如果你手上的PDF类型比较特殊比如是某个专业软件导出的报告格式欢迎沿着这套流程自己摸一摸多半能找到比我这更合适的组合方案。做这类工具脚本最忌讳的就是拿一套代码跑遍天下一定要学会给每种PDF分诊、对症下药。
返回列表