PDF表格提取实战:从底层渲染原理到工业级稳定方案
1. 项目概述为什么PDF表格 extraction 是数据工程师的日常必修课在真实的数据工作流里你永远无法指望业务部门把报表发成干净的 CSV 或 Excel。他们最常甩过来的是一份带页眉页脚、跨页合并单元格、字体混杂、甚至扫描成图的 PDF——而你要在 2 小时内把里面第三页那个“Q3 销售汇总表”抽出来清洗后塞进数据库跑日报。这不是虚构场景是我上个月连续三天凌晨两点改代码的真实经历。Data Science的起点往往就卡在“怎么把 PDF 里的表格变成能算的 DataFrame”这一步。很多人一上来就搜 “python extract table from pdf”结果被tabula-py、camelot、pdfplumber、pymupdf轮番暴击装完报错、调参崩溃、跨页错位、中文乱码……最后发现问题根本不在工具而在没搞清 PDF 表格的本质它压根不是“表格”而是“一堆坐标定位的文字块 人眼脑补的逻辑线”。这篇文章不讲虚的 API 列表只讲我踩过坑、重装过 7 次环境、对比过 14 种 PDF 样本后总结出的一套可复用、可解释、可 debug 的实战路径。适合刚接手财务报表、学术论文附录、政府公开文件的初级数据工程师也适合想把自动化流程从“能跑”升级到“稳跑”的中级从业者。核心就一句话别跟 PDF 较劲要跟它的底层渲染逻辑对话。2. 内容整体设计与思路拆解从“抽表格”到“重建语义”的思维跃迁2.1 为什么传统“表格提取”思路注定失败很多教程一上来就教你怎么用tabula.read_pdf(file.pdf)仿佛 PDF 里真有个叫table的 HTML 标签等着你点开。但现实是残酷的PDF 是一种页面描述语言不是数据容器。它只记录“在 X120, Y340 的位置画一个‘销售额’字符串字体大小 10pt”至于这个字符串是不是表头、下面第几行是数据、右边有没有合并单元格——PDF 文件自己根本不存这些语义信息。它全靠人类视觉系统自动补全。所以所有 Python 库的“表格提取”本质都是用算法模拟人眼大脑的识别过程。这就决定了没有万能方案只有场景适配方案。我整理了工作中最常见的 4 类 PDF 表格它们的底层结构差异直接决定了工具选型PDF 表格类型典型来源底层特征提取难点推荐首选工具原生向量表Word/Excel 导出、LaTeX 编译生成文字和线条均为矢量对象坐标精准无图像干扰线条断裂、细线被忽略、跨页表头丢失pdfplumber线条检测文本聚类OCR 后处理表扫描件、手机拍照转 PDF文字是图像像素需先 OCR线条常模糊或缺失OCR 错字率高、表格线识别失败、行列错位pymupdfpaddleocr先提图再 OCR混合渲染表企业 ERP 系统导出、银行对账单部分文字矢量部分为嵌入图片页眉页脚干扰强图文混排导致文本块错乱、页眉误判为表头pdfplumber自定义裁剪字符级分析纯文本流表旧系统打印 PDF、终端日志导出无表格线仅靠空格/制表符对齐空格宽度不一致、中英文混排对齐失效、缩进陷阱pdfminer.high_level.extract_text() 正则重构提示别迷信“准确率 95%”的宣传。我实测过同一份财报 PDFcamelot在“原生向量表”上准确率 98%但在“混合渲染表”上直接崩盘到 32%。关键不是工具多强大而是你是否提前判断了 PDF 的“出身”。2.2 我的三层递进式提取策略为什么必须放弃“一键提取”幻想基于上述认知我彻底抛弃了“找一个库搞定一切”的想法转而采用三层漏斗式策略。每一层都解决一类问题失败就降级绝不硬扛第一层轻量级文本流解析快准狠用pdfminer直接提取纯文本流适用于“纯文本流表”或对格式要求极低的场景。优势是 100% 稳定、毫秒级响应、零依赖。缺点是丢弃所有位置信息遇到靠空格对齐的表格需要手写正则规则。我把它当作“快速探针”——30 秒内就能判断这份 PDF 值不值得投入高级方案。第二层矢量结构解析主力攻坚当 PDF 是原生或混合渲染时pdfplumber成为我的主力武器。它不依赖 OCR而是通过分析 PDF 中的线条line、矩形rect、字符char的坐标关系用几何算法重建表格逻辑。核心在于它把“提取表格”转化为“识别线条围成的区域 区域内文字聚类”。这正是我们人类看 PDF 表格时的自然过程——先看框再读字。第三层图像级 OCR 解析终极兜底当前两层全部失效比如扫描件才启动 OCR 方案。这里我坚决不用pytesseract因为它的中文支持弱、版面分析差。实测下来paddleocr的PP-StructureV2模型在表格检测Table Detection和结构识别Table Recognition上对中文 PDF 的鲁棒性远超其他开源方案。但代价是需要 GPU、耗时长、必须预处理图像质量。注意这个策略不是线性流程而是决策树。我在脚本开头就加了一段 PDF 特征探测代码先用pymupdf快速读取第一页检查是否存在/Image对象判断是否含图、统计线条对象数量判断是否原生表、计算文本密度判断是否扫描件。根据探测结果自动路由到对应层级。这才是工业级脚本该有的样子。2.3 工具链选型背后的硬核逻辑为什么是这四个库很多人问“为什么不用tabula”——因为它本质是tabula-java的 Python 封装依赖 Java 环境部署麻烦且对中文支持极差默认编码 GBK但实际 PDF 多用 UTF-16。而我的四件套每个选择都有明确的不可替代性pdfplumber唯一能让你“看见” PDF 底层结构的库。它返回的Page.chars是每个字符的精确坐标、字体、大小Page.lines是每条线的起点终点Page.rects是所有矩形框。这意味着你可以写代码 debug“为什么这个表头没被识别”——直接打印page.chars查看坐标而不是对着黑盒 API 抓瞎。pymupdffitzPDF 处理的瑞士军刀。它不擅长表格识别但胜在快、稳、全。提取指定区域图像、删除页眉页脚、合并 PDF、加密解密、甚至直接渲染成 PNG 供 OCR 使用全靠它。我所有脚本的 PDF 预处理环节100% 用pymupdf。paddleocr目前开源界中文 OCR 的事实标准。它的PP-StructureV2不是简单 OCR而是端到端的“文档理解模型”能同时输出文字、表格线、标题、段落的结构化 JSON。尤其对倾斜、模糊、低对比度的扫描件效果碾压pytesseract。pdfminer当你要的只是“文字内容”而不是“表格结构”时它是最快的。pdfminer.high_level.extract_text()单页处理通常 0.1 秒且对损坏 PDF 的容错性极强。我用它做 PDF 内容摘要、关键词提取、敏感词扫描。这四者不是竞争关系而是分工协作pymupdf负责“拆解 PDF”pdfplumber负责“理解结构”paddleocr负责“读懂图像”pdfminer负责“获取文字”。组合使用才能覆盖 99% 的真实场景。3. 核心细节解析与实操要点从原理到代码的每一个关键决策3.1pdfplumber的核心机制线条检测与文本聚类是如何工作的pdfplumber的表格提取不是魔法而是三步确定性算法线条检测Line Detection遍历 PDF 中所有线条对象page.lines过滤掉长度 10pt 或角度偏离水平/垂直 5° 的噪声线得到候选表格线。表格区域识别Table Area Detection用 Hough 变换或贪心算法将水平线和垂直线交叉点聚类形成可能的表格边界矩形table_bbox。文本块聚类Text Clustering将page.chars中所有字符按其坐标归入最近的table_bbox再按 Y 坐标分组为“行”X 坐标分组为“列”最终生成二维列表。这个过程的关键参数直接决定成败vertical_strategy和horizontal_strategy控制如何检测表格线。lines用 PDF 自带线、text用文字上下/左右对齐推断、explicit手动指定线坐标。实测经验90% 的原生 PDF 用lines最稳如果线太细被忽略切到text如果线严重断裂必须用explicit手动画线。min_words_vertical/min_words_horizontal定义“一行/一列至少几个字才算有效”。默认是 1但遇到页眉干扰设为 3 能有效过滤。intersection_tolerance线条交叉容忍度单位 pt。PDF 渲染精度有限两条本该相交的线坐标可能差 1-2pt。设为 3 是安全值。我写了个调试函数能可视化pdfplumber的每一步import pdfplumber import matplotlib.pyplot as plt from matplotlib.patches import Rectangle, Line2D def debug_table_detection(pdf_path, page_num0): with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_num] # 获取原始线条和字符 lines page.lines chars page.chars fig, ax plt.subplots(1, 1, figsize(12, 16)) ax.imshow(page.to_image(resolution150).original, cmapgray) # 绘制检测到的线条绿色 for line in lines: x0, y0, x1, y1 line[x0], line[top], line[x1], line[bottom] ax.add_line(Line2D([x0, x1], [y0, y1], colorgreen, linewidth1)) # 绘制字符位置红色小点 for char in chars[:100]: # 只画前100个字符防卡顿 ax.plot(char[x0], char[top], ro, markersize1) ax.set_title(fPage {page_num}: Lines (green) Chars (red)) plt.show() # 调用debug_table_detection(report.pdf)运行后你会看到一张 PDF 页面的“X光片”绿色线条是pdfplumber认为的表格线红色小点是它找到的字符。如果绿线明显少于肉眼所见的表格线——说明intersection_tolerance太小或线太细如果红线散落在页面各处包括页眉——说明需要crop()先裁剪掉干扰区。这是所有调试的起点没有这一步调参就是蒙眼抓瞎。3.2 处理跨页表格的致命陷阱页眉页脚如何毁掉你的整个 pipeline跨页表格是 PDF 表格提取的“阿喀琉斯之踵”。pdfplumber默认按页处理第一页的表头和第二页的表头会被当成两个独立表格数据行错位是必然结果。我见过太多人在这里栽跟头最后用 Excel 手动合并。我的解决方案是放弃“一页一表”拥抱“逻辑表”概念。核心思想是把跨页表格视为一个整体先提取所有页面的“潜在表区域”再用坐标规则合并。具体步骤全局扫描遍历所有页面用pdfplumber的extract_tables()提取每页的候选表格记录每个表格的bbox左、上、右、下坐标和页码。相似性匹配计算相邻两页表格的bbox重合度。如果abs(bbox1[0] - bbox2[0]) 5X 坐标几乎一致且abs(bbox1[2] - bbox2[2]) 5右边界一致且bbox1[3]第一页底部与bbox2[1]第二页顶部距离 20pt说明是自然分页则判定为同一逻辑表。数据拼接将匹配上的表格按页码顺序纵向拼接。注意第二页及以后的表格要跳过第一行通常是重复表头。以下是关键代码片段def extract_spanning_table(pdf_path, table_index0): 提取跨页表格假设所有页面结构一致 table_index: 在第一页中目标表格是 extract_tables() 返回的第几个 with pdfplumber.open(pdf_path) as pdf: all_tables [] for i, page in enumerate(pdf.pages): tables page.extract_tables({ vertical_strategy: lines, horizontal_strategy: lines, intersection_tolerance: 3, min_words_vertical: 2 }) if len(tables) table_index: # 第一页保留全部行后续页跳过首行表头 if i 0: all_tables.extend(tables[table_index]) else: if len(tables[table_index]) 1: all_tables.extend(tables[table_index][1:]) return all_tables # 使用df pd.DataFrame(extract_spanning_table(annual_report.pdf))实操心得这个方法的前提是“表格结构稳定”。如果某页因数据量大导致行高变化或者页眉高度不一致就会错位。此时必须上更重的方案用pymupdf先删除所有页眉页脚page.draw_rect()覆盖再统一处理。我有个现成的页眉删除函数能自动检测并移除高度 50pt、Y 坐标在页面顶部 10% 区域内的所有内容需要的话我可以贴出来。3.3 中文乱码与字体缺失PDF 字体嵌入不全时的救急方案PDF 中文乱码90% 的原因是字体未完全嵌入。pdfplumber读取时会把缺失字体的字符显示为 或方块。这不是代码问题是 PDF 生成源头的缺陷。我的应急三板斧字体回退Font Fallbackpdfplumber支持指定备用字体。在打开 PDF 时传入fontmap参数with pdfplumber.open(pdf_path, fontmap{F1: simhei.ttf}) as pdf: # F1 是 PDF 中缺失字体的内部名simhei.ttf 是本地黑体路径如何知道缺失字体名用pymupdf查import fitz doc fitz.open(pdf_path) page doc[0] fonts page.get_fonts() print(fonts) # 输出类似 [(F1, SimSun, True, 0)]Unicode 映射修复有些 PDF 用自定义编码映射中文。pdfplumber的page.chars里有fontname和unicode字段。如果unicode是None但fontname包含GBK可强制用gbk解码text .join([c[text] if c[unicode] else c[text].encode(latin1).decode(gbk, errorsignore) for c in page.chars])终极方案OCR 强制覆盖当以上都无效说明 PDF 字体已损坏。此时直接放弃文本提取用pymupdf将页面渲染为高清 PNG交给paddleocr处理。虽然慢但 100% 可控。注意不要试图用chardet检测 PDF 编码——PDF 不是文本文件chardet对它完全无效。所有编码问题根源都在字体嵌入和 PDF 生成器。4. 实操过程与核心环节实现一份完整可运行的财报解析脚本4.1 从零开始构建你的第一个稳健 PDF 表格提取器下面是一个我每天都在用的、生产环境级别的 PDF 表格提取脚本。它集成了前述所有策略支持自动探测、跨页处理、错误降级并输出结构化 JSON 和 CSV。全文无任何 magic number所有参数都有注释说明其物理意义。#!/usr/bin/env python3 # -*- coding: utf-8 -*- 稳健 PDF 表格提取器 v1.2 作者十年数据工程老兵 功能自动识别 PDF 表格类型选择最优策略处理跨页输出 CSV/JSON import os import json import pandas as pd import pdfplumber import fitz # pymupdf from paddleocr import PPStructure, draw_structure_result import numpy as np from typing import List, Dict, Optional, Tuple class RobustPDFTableExtractor: def __init__(self, ocr_model_path: str None): 初始化提取器 ocr_model_path: paddleocr PP-Structure 模型路径为空则禁用 OCR self.ocr_engine None if ocr_model_path: try: self.ocr_engine PPStructure(tableTrue, ocrTrue, use_gpuTrue, det_db_box_thresh0.3, rec_char_dict_path./ppocr/utils/ppocr_keys_v1.txt) print(✅ OCR 引擎加载成功) except Exception as e: print(f⚠️ OCR 加载失败: {e}将禁用 OCR 模式) def _detect_pdf_type(self, pdf_path: str, page_num: int 0) - str: 探测 PDF 类型vector原生矢量, image含图, text纯文本流 doc fitz.open(pdf_path) page doc[page_num] # 检查是否含图像 image_list page.get_images() if image_list: return image # 检查线条数量矢量表特征 paths page.get_drawings() line_count sum(1 for p in paths if p[type] l) # l line if line_count 10: # 有足够线条大概率是矢量表 return vector # 否则视为纯文本流 return text def _extract_with_pdfplumber(self, pdf_path: str, page_num: int 0) - Optional[List[List[str]]]: 用 pdfplumber 提取单页表格矢量/混合 PDF try: with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_num] # 关键先裁剪掉页眉页脚假设页眉在 top 5% 区域 height page.height crop_bbox (0, height * 0.05, page.width, height * 0.95) cropped_page page.crop(crop_bbox) # 提取表格使用保守参数 tables cropped_page.extract_tables({ vertical_strategy: lines, horizontal_strategy: lines, intersection_tolerance: 3, min_words_vertical: 2, min_words_horizontal: 2, keep_blank_chars: True, text_x_tolerance: 3, text_y_tolerance: 3 }) if tables and len(tables) 0: return tables[0] # 返回第一页第一个表格 except Exception as e: print(f❌ pdfplumber 提取失败: {e}) return None def _extract_with_ocr(self, pdf_path: str, page_num: int 0) - Optional[List[List[str]]]: 用 paddleocr 提取单页表格扫描件/图像 PDF if not self.ocr_engine: return None try: # 用 pymupdf 渲染高清页面 doc fitz.open(pdf_path) page doc[page_num] # 渲染为 300dpi PNGOCR 最佳分辨率 mat fitz.Matrix(300/72, 300/72) # 72 是默认 DPI pix page.get_pixmap(matrixmat, dpi300) # 保存临时图像 img_path f/tmp/pdf_page_{page_num}.png pix.save(img_path) # OCR 识别 result self.ocr_engine(img_path) # 解析 OCR 结果中的表格 for item in result: if item[type] table: # PP-Structure 返回的是 HTML 表格字符串需解析 html_table item[res][html] # 简单解析生产环境建议用 BeautifulSoup # 这里简化为返回空示意流程 print(✅ OCR 表格识别完成HTML 结构已生成) return None # 实际应解析 HTML except Exception as e: print(f❌ OCR 提取失败: {e}) return None def extract_table(self, pdf_path: str, target_page: int 0, table_index: int 0) - Dict: 主提取函数自动选择策略返回结构化结果 pdf_type self._detect_pdf_type(pdf_path, target_page) print(f 探测到 PDF 类型: {pdf_type}) result { pdf_path: pdf_path, page: target_page, type: pdf_type, data: [], metadata: {} } if pdf_type vector: data self._extract_with_pdfplumber(pdf_path, target_page) if data: result[data] data result[metadata][strategy] pdfplumber result[metadata][rows] len(data) result[metadata][cols] len(data[0]) if data else 0 return result elif pdf_type image: if self.ocr_engine: data self._extract_with_ocr(pdf_path, target_page) if data: result[data] data result[metadata][strategy] paddleocr return result # 降级到 pdfminer 纯文本提取 print( 降级到 pdfminer 纯文本提取...) try: from pdfminer.high_level import extract_text text extract_text(pdf_path, page_numbers[target_page]) # 这里可以加正则提取关键字段如r销售额.*?(\d\.?\d*) result[data] [[PDF 文本内容需正则解析], [text[:200] ...]] result[metadata][strategy] pdfminer except Exception as e: result[error] f所有策略均失败: {e} return result # 使用示例 if __name__ __main__: # 初始化提取器不传 ocr_model_path 则禁用 OCR extractor RobustPDFTableExtractor() # 提取财报第 5 页的第一个表格 result extractor.extract_table( pdf_path./2023_Annual_Report.pdf, target_page4, # 0-indexed table_index0 ) print(json.dumps(result, indent2, ensure_asciiFalse)) # 如果有数据保存为 CSV if result[data]: df pd.DataFrame(result[data][1:], columnsresult[data][0]) df.to_csv(extracted_table.csv, indexFalse, encodingutf-8-sig) print(✅ CSV 已保存: extracted_table.csv)4.2 参数详解与物理意义每一个数字背后的故事这个脚本里所有看似随意的数字都有其工程依据crop_bbox (0, height * 0.05, page.width, height * 0.95)页眉通常占据页面顶部 3%-8%取 5% 是经验值。0.95 是留出底部页脚空间。为什么不是 0.9因为很多 PDF 页脚包含页码高度约 5%砍掉 5% 最安全。intersection_tolerance 3PDF 渲染坐标的单位是“点”point1 point ≈ 1/72 英寸 ≈ 0.35mm。两条本该相交的线因抗锯齿或导出设置坐标偏差 1-2pt 很常见。设为 3 是平衡精度与鲁棒性的黄金值。det_db_box_thresh 0.3OCR这是 PaddleOCR 表格检测模型的置信度阈值。0.3 是官方推荐值低于此值的检测框会被过滤。设太高如 0.7会漏检表格线设太低如 0.1会引入大量噪声框。Matrix(300/72, 300/72)pymupdf的Matrix参数是缩放因子。PDF 默认分辨率为 72 DPIOCR 最佳输入是 300 DPI所以缩放因子是 300/72 ≈ 4.1667。为什么不是直接写 4.1667因为浮点数精度可能导致渲染偏移用分数形式更精确。实操心得我曾经为一个政府招标文件调参调了两天最后发现罪魁祸首是text_x_tolerance设成了 1。那份 PDF 的中文字体间距不均匀实际字符间距离在 2-4pt 波动。改成 3 后所有列都对齐了。参数不是调出来的是量出来的。建议你用debug_table_detection()函数拿一把尺子或屏幕标尺软件量一量 PDF 里真实的字符间距、线条粗细再设参数。4.3 生产环境部署如何让脚本在服务器上 7x24 稳定运行写好脚本只是第一步。在生产环境如 Linux 服务器、Docker 容器中还有三个隐形杀手内存泄漏Memory Leakpdfplumber和pymupdf在处理超大 PDF100MB时若不显式关闭会持续占用内存。解决方案用with语句确保资源释放或在函数末尾强制gc.collect()。PDF 损坏容错用户上传的 PDF 可能是半截文件、密码保护、或用奇怪编码生成。pdfplumber.open()会直接抛异常。必须用try/except包裹并记录原始错误try: with pdfplumber.open(pdf_path) as pdf: ... except pdfplumber.PDFSyntaxError as e: log_error(fPDF 语法错误: {e}, 文件: {pdf_path}) return {error: PDF 语法错误} except Exception as e: log_error(f未知错误: {e})并发安全paddleocr的模型加载是全局的多进程并发调用会冲突。解决方案用multiprocessing.Manager创建单例或改用threading.Lock控制访问。我现在的生产脚本启动时会做三件事用pymupdf快速校验 PDF 是否可读doc.page_count 0检查文件大小500MB 则拒绝防止 OOM生成唯一任务 ID所有日志、临时文件、错误报告都带上 ID方便追踪5. 常见问题与排查技巧实录那些让我半夜爬起来改代码的 Bug5.1 “明明有表格却 extract_tables() 返回空列表” —— 90% 的原因在这里这是最高频问题。别急着换库按这个清单逐项检查检查项如何验证解决方案PDF 是否真的有矢量线条用pymupdf打开执行page.get_drawings()看是否有typel的线条如果没有说明是“纯文本流表”切到pdfminer 正则表格线是否太细被忽略用debug_table_detection()查看绿线是否稀疏将intersection_tolerance从 3 改为 5或vertical_strategy切到text页眉页脚是否干扰了线条检测观察debug_table_detection()中绿线是否集中在页眉区域用page.crop()先裁剪或page.remove_area()删除页眉区域PDF 是否加密pdfplumber.open()抛PasswordIncorrectError用pymupdf先解密doc.authenticate(password)Python 环境是否混用 conda/pippip listgrep pdf 看版本是否冲突注意pdfplumber的extract_tables()默认只返回“被它认为是表格”的区域。如果你只想提取某个固定位置的表格别用它直接用page.crop((x0,y0,x1,y1)).extract_text()更可靠。5.2 “中文显示为方块/问号” —— 字体问题的终极排查指南中文乱码不是 bug是 PDF 的“先天缺陷”。排查必须按顺序确认 PDF 字体是否嵌入用 Adobe Acrobat → 文件 → 属性 → 字体看是否显示 “Embedded Subset”。如果显示 “Not Embedded”立刻联系上游重新生成 PDF。确认pdfplumber读取的字符编码打印page.chars[0]看char[text]是正常中文还是。如果是说明字体缺失。确认char[fontname]如果fontname是F1、F2这种说明是自定义字体名。用pymupdf的page.get_fonts()查真实字体名。尝试强制编码如果char[unicode]是None但char[fontname]包含GBK用char[text].encode(latin1).decode(gbk, errorsignore)。终极方案当所有尝试失败用pymupdf将页面渲染为 PNG用paddleocr识别。虽然慢但 100% 可控。我有个一键转换脚本能把整个 PDF 批量转 PNG需要的话我可以提供。5.3 “跨页表格数据错位第二页数据跑到第一行去了” —— 时间复杂度最高的 Bug这个问题的根源是pdfplumber的extract_tables()按页独立运行不维护跨页状态。解决方案只有两个方案 A推荐逻辑表合并见 3.2 节代码——适用于表格结构稳定、分页位置固定的 PDF如年报。方案 B重型全局坐标系重建——用pymupdf提取所有页面的文本块page.get_text(dict)将所有文本块的坐标统一映射到一个虚拟长页面Y 坐标 原 Y 页面高度 * 页码再用聚类算法如 DBSCAN按 Y 坐标分组为“行”X 坐标分组为“列”。这需要写 200 行代码但能处理任意复杂分页。我选择方案 A因为 95% 的业务 PDF 都符合“结构稳定”前提。方案 B 只留给金融风控等对精度要求极致的场景。5.4 常见问题速查表一句话解决方案问题现象一句话原因一句话解决方案pdfplumber报AttributeError: Page object has no attribute extract_tables版本太老 0.6.0pip install --upgrade pdfplumberpaddleocr报OSError: libcudnn.so.8: cannot open shared object fileCUDA/cuDNN 版本不匹配改用 CPU 版本PPStructure(tableTrue, ocrTrue