资讯动态

Python实现PDF图片转Excel表格:从OCR到表格识别全流程

发布时间:2026/10/3 4:25:38 来源:尧图企业网站定制
最近在社区里看到有人问“怎么用Python写一个把PDF图片转成Excel表格的小工具”这个问题大家问得挺多但网上的回答要么只讲了半截要么压根没说清楚思路。我自己之前也遇到过一模一样的需求客户发来一堆PDF里面的数据全是扫描件或者图片需要整理进Excel里做汇总分析。手动敲了几百行数据之后我真是一边敲一边想着必须给自己写个工具后来折腾了两天多总算是把整套流程跑通了。这篇文章就把我从零到一完成这个小工具的全过程拆开讲清楚包括技术选型、环境搭建、每一步的代码思路、踩过的坑以及优化方向。如果你也卡在“PDF里是图片怎么才能变成Excel”这件事上这篇文章应该能直接帮你把路趟平。1. 问题拆解PDF图片转Excel本质上是什么流程先说清楚这个任务和普通的“PDF转Excel”完全不是一回事。普通的PDF转ExcelPDF里本来就留有文本层解析器可以直接把文字捞出来再根据坐标拼成表格结构。但如果PDF里是扫描件或者图片整页都是像素点没有任何可提取的文本信息这时候你要做的其实是“图像里的表格识别”全流程可以拆成四步PDF的每一页要被渲染成一张高清图片。从图片里找到表格线确定每行每列的边界位置。对每个单元格区域做OCR文字识别把图像变成文本。根据单元格坐标把文本按行列关系填进新的Excel表格。这个思路一旦想清楚后面其实就是在选工具和调参。技术路线非常直观就是“图像处理OCRExcel写入”三段式图像处理负责定位表格线和单元格边界OCR负责把单元格里的文字变成文本最后按坐标映射写入Excel。我最终选定的技术组合是这样的环节工具用途PDF转图片pdf2image底层依赖Poppler把PDF每页渲染成PNG图片表格线检测OpenCV 图像形态学操作找横线和竖线定位表格区域OCR识别PaddleOCR对每个单元格做中文/英文识别Excel写入openpyxl / pandas按行列表格结构生成Excel文件这套组合全部是开源免费的对个人工具来说足够用了。其中PaddleOCR是百度开源的识别效果尤其是中文识别准确率上比传统的Tesseract要好不少后面会详细说。2. 环境搭建与库安装这里我踩了一半的坑都在环境上这个项目的环境安装其实才是第一大坎。很多人在写代码前就被各种依赖给劝退了所以我把环境部分单独拿出来讲里面有一些版本和平台上的细节必须提前避开。2.1 基础依赖安装我用的是Python 3.9这个版本兼容性不错。装一下几个核心库pip install pdf2image opencv-python paddlepaddle paddleocr openpyxl pandas注意这里有几个坑要提前讲PaddleOCR 2.6版本以上和较新的paddlepaddle版本之间偶尔会有接口变动。我实测2.7版本整体稳定一些建议如果没有特殊要求就直接装paddleocr2.7和paddlepaddle2.6。如果你用的是Python 3.12装paddlepaddle的时候很容易遇到底层编译错误建议Python 3.10含以下版本跑OCR别在这个环节浪费大量时间去解决环境兼容问题。另外pdf2image依赖系统的Poppler。在Linux环境下直接apt-get install poppler-utils就行但Windows用户会麻烦一点需要自己去下载Poppler的Windows二进制包解压后把bin目录路径添加到系统环境变量PATH里。这个细节特别容易漏掉漏了之后pdf2image调用时会直接报OSError: cannot load library。2.2 验证环境是否装好了装完之后先在命令行里跑一段验证脚本确认每一步的可用性import pdf2image import cv2 print(pdf2image ok) print(opencv ok:, cv2.__version__)如果这一行能正常输出没有报错说明基础库没问题。再去跑PaddleOCR的初始化第一次运行会自动下载模型文件需要保持联网而且模型文件大概几百MB耐心点等着就行。3. 第一步把PDF每一页转成高清图片这是整个流程的基础。处理PDF页面渲染的时候有几个参数非常关键直接影响后续OCR的效果。from pdf2image import convert_from_path def pdf_to_images(pdf_path, dpi300, output_folderpages): os.makedirs(output_folder, exist_okTrue) images convert_from_path(pdf_path, dpidpi) paths [] for i, img in enumerate(images): # 转成RGB避免透明通道问题再存成PNG img img.convert(RGB) filepath os.path.join(output_folder, fpage_{i1}.png) img.save(filepath, PNG) paths.append(filepath) return pathsdpi参数决定了渲染清晰度第一个必须注意的值。我测试过150dpi下稍微复杂的表格就有大量的识别错误尤其是字号较小的单元格内容OCR会连蒙带猜300dpi的效果明显好一个量级而且对OpenCV找表格线也有帮助边缘更锐利。再往上提到400dpi提升已经不明显反而让文件体积和耗时成倍增加得不偿失。300dpi是我推荐的首选参数。灰度处理这步也很关键。存成图片之后读取时要记得先转灰度图因为后续的表格线检测和二值化都在灰度图基础上进行。一张干净的灰度图能显著减少OpenCV在找线条时的干扰。4. 第二步用OpenCV定位表格线、切分单元格拿到图片后最核心的部分就是精确找到表格的横线和竖线。这一环节决定Excel里行列结构是否准确是整个工具的技术核心也是默认最容易翻车的地方。4.1 核心原理形态学运算找直线我先解释一下为啥不直接用Hough变换来找表格线而是用形态学运算。形态学腐蚀和膨胀对“线条”这种特定形状非常敏感设置好合适的核大小后能精准地提取横向或纵向的连续线段对噪点的容忍度也比较高。Hough变换当然也能用但它会把图片里本来不是表格线的一些边缘也检测成线段筛选参数调整起来很费劲。实际手写代码时形态学方法更直接高效。核心代码如下def find_table_lines(gray_img): # 1. 二值化反过来让表格线是白色便于形态学检测 _, binary cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 2. 提取横向线用一个大宽度的水平核做腐蚀膨胀 horizontal_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (60, 1)) horizontal_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, horizontal_kernel) # 3. 提取纵向线用一个大高度的垂直核 vertical_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (1, 60)) vertical_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, vertical_kernel) return horizontal_lines, vertical_lines代码里两个需要重点理解的参数MORPH_OPEN是先腐蚀后膨胀可以抹掉比核小的孤立噪点。核的尺寸60, 1代表我很期待表格线是“横向连续至少60像素”的直线竖线同理至少连续60像素。对于300dpi处理A4页面来说这个长度足够覆盖大多数常见的表格线。二值化的方向要注意我用的是THRESH_BINARY_INV因为表格线在原始灰度图里通常是黑线取反之后变白线后续形态学检测时白色区域才是目标。提取完横线和竖线后第二步是把两者叠加成完整网格然后找轮廓得到每个单元格的矩形区域。def get_cell_boxes(horizontal_lines, vertical_lines): # 1. 合并横竖线构成完整表格框架 table_frame cv2.bitwise_or(horizontal_lines, vertical_lines) # 2. 闭运算填补交点缺口 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) table_frame cv2.dilate(table_frame, kernel, iterations1) # 3. 查找轮廓 contours, _ cv2.findContours(table_frame, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 过滤掉太小的噪点区域 if w 20 or h 20: continue boxes.append((x, y, w, h)) return boxes这里有个很关键的技巧横竖线交叉的时候交点处往往会因为二值化噪声出现小断裂如果不做闭运算第2步表格线看起来是断开的找出来的轮廓都会碎掉识别到的“单元格”会缺胳膊少腿。闭运算通过先膨胀后腐蚀把邻近的线段连起来交点补全。但这步也别太猛膨胀核设成(5, 5)就恰到好处核太大会把相邻单元格合并到一起边线粘连导致后续无法正常切分。提示如果你的表格是带斜线、有合并单元格的复杂表头这个方法就有点力不从心了。它最适合规则的二维数据表。遇到合并单元格需要再加一个“判断矩形相互嵌套包含”的逻辑这个放到后面优化部分说。4.2 对单元格进行排序和编号找到矩形框之后需要把它们按行列关系组织起来。用坐标排序逻辑先按y坐标分出行再按x坐标排好每一行内的顺序。def sort_boxes(boxes): # 先按y坐标粗略分出行以矩形高度的一半作为容忍阈值 boxes_sorted sorted(boxes, keylambda b: (b[1] // (b[3] // 2), b[0])) return boxes_sorted这个做法利用“每行的y坐标除以一个粗略阈值来聚簇”完成行分组虽然粗糙但大多数场景下够用。更严谨的做法是对y坐标做聚类分析比如按y中心点排序后前后比较差值但当表格行距均匀时上面的整除分组法已经足够稳定。5. 第三步对每个单元格做OCR识别与坐标还原单元格切好后就到了OCR环节。OCR的选择上我做过对比这里必须好好说一说。5.1 为什么选PaddleOCR而不是Tesseract很多人一上来会用Tesseract但用在实际项目里它的缺点很明显对中文识别支持一般对低清晰度图像鲁棒性远不如PaddleOCR如果单元格里有数字、小数点、单位混杂的情况Tesseract经常把“12.5元”识别成“125元”。PaddleOCR的识别链路里包含文本检测和文本方向分类对中文、混排、表格中常见的小字号内容都更友好准确率普遍高出不少。我实际测试了一组扫描版的报销表格Tesseract的字符准确率大概在75%-85%但PaddleOCR轻松能到95%以上而且PaddleOCR还能直接拿到识别置信度方便做二次判断。下面是使用代码from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def ocr_cell(img, box): x, y, w, h box # 裁出单元格区域做一点边缘拓展避免文字被截断 pad 2 x0 max(0, x - pad) y0 max(0, y - pad) x1 min(img.shape[1], x w pad) y1 min(img.shape[0], y h pad) cell img[y0:y1, x0:x1] # 单独对每个单元格做识别 result ocr.ocr(cell, clsTrue) if not result or not result[0]: return # 合并识别出的文本 text_parts [line[1][0] for line in result[0]] return .join(text_parts)这里有两个容易踩的坑一是单独切单元格时图片边缘会很窄。如果table line检测时残留了一点黑边或者字体笔画被切了一小截识别率会下降。所以我加了一个2像素的padding把单元格向四周外扩一小点实测对改善识别效果是有帮助的。二是result[0]的结构里每个元素是[坐标框, (文本, 置信度)]。这个嵌套结构在PaddleOCR 2.x里基本稳定但不同版本之间返回的数据结构会有细微差异。我已经看到很多人在新版里因为result[0][0]报错如果你遇到类似问题第一步先print一下result结构再改代码不要硬套旧接口。5.2 坐标映射到ExcelOCR识别完单元格要把数据按照刚才sort好的顺序一行行写入Excel。这个步骤逻辑上很直接from openpyxl import Workbook def save_to_excel(rows, output_path): wb Workbook() ws wb.active for row_idx, row in enumerate(rows, start1): for col_idx, text in enumerate(row, start1): ws.cell(rowrow_idx, columncol_idx, valuetext) wb.save(output_path)这一部分看上去简单但实际会冒出不少意料之外的麻烦。最常见的是识别出一行数据之后有些行的单元格数量比其他行少比如空单元格没有被检测出来。这会导致数据错位本来是第三列的数值直接跳到了第二列。这个问题很恼火你可以通过基于列的位置做对齐来解决即先统计所有非空单元格的x中心点聚类出“全局列坐标”写Excel的时候再根据每个单元格x中心落在哪个全局列坐标来判定它是第几列。这个做法比光依赖boxes的排序可靠得多尤其适合处理有缺失单元格或部分空行的表格。6. 完整代码串联与验证第一版工具的实测效果把所有环节串成一个可运行脚本形成完整的工具雏形import os from pdf2image import convert_from_path import cv2 import numpy as np from paddleocr import PaddleOCR from openpyxl import Workbook # 省略上面各函数定义... def pdf_table_to_excel(pdf_path, excel_path, dpi300): # 1. PDF转图片 pages convert_from_path(pdf_path, dpidpi, fmtpng) all_rows [] for page_idx, page in enumerate(pages): pil_img page.convert(RGB) img cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 提取表格线 h_lines, v_lines find_table_lines(gray) # 3. 得到单元格坐标 boxes get_cell_boxes(h_lines, v_lines) boxes sort_boxes(boxes) # 4. 遍历单元格做OCR rows [] for box in boxes: text ocr_cell(gray, box, ocr_engine) rows.append((box, text)) # 5. 按行聚类整理 # 这里根据实际排序结果把text按row分组 page_rows group_text_into_rows(rows) all_rows.extend(page_rows) # 6. 写Excel save_to_excel(all_rows, excel_path)我拿着这套代码用一份10页的扫描版产品报价表做了测试。生成的Excel里数值和产品名称基本都正确首版准确率大概在85%到90%之间。剩下的问题主要集中在三种情况表头一些艺术字体OCR识别出来是乱码数字之间有空格或小数点被吞部分单元格文字和背景对比度太低识别不了。这些问题的根源不完全是OCR而是图像预处理没有到位。所以后面我又加了一个简单的前置处理对每个单元格做自适应阈值二值化。效果有很大提升乱码问题少了很多。def preprocess_cell(cell_gray): # 自适应阈值处理增强对比度 cell_bin cv2.adaptiveThreshold( cell_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 10 ) return cell_bin但要注意一点自适应阈值处理会让浅色底纹的单元格里的文字更容易被识别但同时也可能把单元格底纹的浅色块变成一大片白色或者黑色导致文字粘连。所以这个预处理要做得保守只对对比度确实很低的单元格使用或者用OpenCV的cv2.createCLAHE来做对比度受限的自适应直方图均衡化效果比单纯二值化更温和。实际我在项目里最终采用了两个路径的混合方案默认先用原灰度图直接识别如果OCR置信度低于0.6再换CLAH增强后的图重新识别一次取两次识别中置信度更高的结果。别怕麻烦这个“双保险”套路在实际批量处理时能救很多张图。7. 测试数据选择与效果评估别用完美PDF自欺欺人一个工具测得好不好很大程度取决于测试数据选得怎么样。我建议这样准备测试集第一类清晰扫描件白底黑字表格线完整这是理想情况。第二类略微倾斜的扫描件页面有点歪真实场景中很常见。第三类有底色、有合并单元格、有跨行的复杂表头。第四类低清晰度、有折痕阴影的扫描件。我第一版工具在第二类“倾斜扫描件”上直接就崩了。表格线不是完全水平竖直的话OpenCV的横竖线检测会错乱找出来的轮廓根本对不上真实单元格。这个问题很典型。解决办法是加一步“倾斜矫正”。我写了个简单的函数先用霍夫变换检测出图片中接近水平的长直线算出这些线段的平均角度然后用旋转矩阵把整个图片转正。大概代码如下def deskew(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold200, minLineLength500, maxLineGap50) angles [] for line in lines: x1, y1, x2, y2 line[0] angle np.degrees(np.arctan2(y2 - y1, x2 - x1)) angles.append(angle) median_angle np.median(angles) # 对接近0度或90度的微小偏差做矫正 if abs(median_angle) 0.3: h, w img.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), median_angle, 1.0) img cv2.warpAffine(img, M, (w, h), borderValue(255, 255, 255)) return img这个函数放在PDF渲染之后、找表格线之前。倾斜角度超过3度以上效果就不太好了但大部分扫描件都不会歪得太离谱。这一步让整个工具的适用范围扩大了很多。8. 合并单元格、跨行表格的进阶处理思路如果你的PDF表格里存在明显的合并单元格上面的基础版处理不了这个坑必须单独说一下。我见过太多人在这里卡死。合并单元格会导致横向合并后这一行的列数变少纵向合并后相邻单元格里没有文本坐标框被更大的框覆盖。处理思路核心在于“先识别大框再判断是否包含小框”。具体逻辑是这样找轮廓时不只需要RETR_EXTERNAL还需要用RETR_TREE拿到内外层关系。如果一个单元格的矩形框A包含另一个矩形框B那A大概率是合并单元格。针对横向合并A这一行里列数会少一列但A占的宽度大约是两列宽。通过把所有单元格的列坐标比如左边界x的值去重聚类能恢复出全局的列分隔线。Excel里合并单元格对应到OpenXML实际上也是依据起始行列和跨度计算好行列后用ws.merge_cells(start_row..., start_column..., end_row..., end_column...)即可。这个方法能覆盖相当多真实业务场景里的复杂表格。当然如果表格样式非常不规则比如斜线表头、多层级嵌套标题建议直接考虑商业库或用PaddleOCR的表格识别模型。PaddleOCR有PP-Structure模块专门做版面分析和表格还原可以一键输出表格的HTML结构再转成Excel只是模型部署更大、运行更慢。作为小工具的进阶方向给有兴趣的读者一个入口基础版用OpenCV已经能解决大半。9. 编码、路径与性能三个易踩的小坑写这个工具时有几个看起来不起眼但特别磨人的问题统一提一下9.1 中文路径程序跑起来没问题但一遇到中文文件名或中文目录路径PDF都打不开。这是因为OpenCV和pdf2image在Windows上对中文路径的处理不太友好。最简单的办法是把临时图片路径、PDF路径全部统一处理成不带中文的相对路径或者用Path对象管理路径from pathlib import Path pdf_path Path(数据表/产品报价表.pdf) img_path Path(temp_pages) / fpage_{page_num}.png但是还是那句话先把程序跑通再谈优雅。最省心的是把源文件放到一个纯英文路径下用。9.2 OCR速度PaddleOCR默认是用CPU推理的一个A4页面大概10个单元格识别耗时大约在3到8秒之间。如果一页表格有几十个单元格就会慢很多。提高速度的方式减小图片尺寸但不要低于200dpi用paddlepaddle-gpu替代CPU版需要NVIDIA GPU把OCR实例全局复用不要每次调用重新初始化。这个优化非常关键我见过有人放在for循环里创建PaddleOCR实例一页创建一次速度直接慢了20倍。9.3 空单元格与缺失行数据单元格里没有文字的话OCR返回空字符串但框仍然在那里。Excel里对应的格子就会是空的。这是正常现象不代表流程出了问题。10. 最终总结与经验建议这个工具从头到尾跑通之后我最大的感触是PDF图片转Excel这件事难不在“写Excel”也不在“OCR”真正的核心难点在步骤2“定位表格线和单元格”。只要定位做得准后面OCR和写入Excel基本一路顺风定位一塌糊涂后面再怎么调也白搭。所以如果想自己动手做一个类似的工具我的建议是先把OpenCV找表格线这个环节吃透反复在几张不同样式的表格上试验其他都是水到渠成的事。另外几个项目实践层面的心得别指望一版代码通吃所有PDF。扫描质量、表格样式、底色干扰这三座大山每一座都够调一阵子。最稳妥的办法是做一个“灰度图直接识别”和“CLAHE增强再接识别”的双保险分支每次单页识别后把置信度低的单元格抽出来打印一下方便调试。大批量处理时加上图片保存和日志输出每处理一页就记录一下页码、识别到的单元格数、平均置信度。出现“某一列整体错位”的问题时这些日志能帮你快速定位是某页的问题还是全局逻辑的问题。OCR识别没有100%正确率。对于财务、合同这类数据建议输出Excel之后保留一个“置信度低于0.6的单元格清单”单独一列方便人工快速复核而不是把低置信度的文本直接混进去。文章写到这基本把我的思路和踩坑经验都倒完了。如果你正在准备类似的工具我最后再分享一个实用技巧测试的时候别只用一份PDF反复调参数最好准备三份完全不同来源的表格文件一个规则无底色、一个带浅色底纹、一个扫描偏斜。三份都跑通你的工具才算真的能用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑