简介:这份资源面向工程技术人员、软件开发者和需批量处理图纸的企业管理者,聚焦PDF工程图纸信息的自动化提取。它结合OCR识别、PDF解析与机器学习,将图纸转为机器可读形式后,专项抽取标题栏数据并统计单重、数量、总重等参数写入Excel,同时按上传模板匹配尺寸符号与名义尺寸,生成结构化数据表,适合需要快速批量处理同类型图纸、提升信息化管理水平的场景。资源包共1个文件,为PDF格式,大小约393KB,内含需求说明与图纸解析抓取流程,涵盖标题栏截取、模板对照、尺寸数据汇总等关键环节。已有102人学习下载。读者可借此理解本地化部署自定义AI模型的整体思路,掌握从图纸识别到数据表输出的完整链路,并参考数据集收集与标注方法训练优化模型,为同类图纸自动化项目提供可落地的实现参考。
1. 从一堆 PDF 工程图纸里把数据抠出来:这套自动化提取系统到底解决什么问题
做过设备管理、产线改造或者工厂数字化的人,大概率都经历过这样的场景:甲方甩过来一个压缩包,里面躺着几百张 PDF 格式的工程图纸,有电气原理图、有管路布置图、有设备装配图,格式还五花八门——有的是原生 CAD 导出的矢量 PDF,有的干脆是扫描件。你需要从里面把设备位号、线缆规格、管径、材料清单这些信息一条条抄进电子表格,几百张图抄下来,眼睛花了不说,错漏率还高得离谱。
这就是「基于 PDF 的工程图纸信息自动化提取系统」要干的事:用程序把 PDF 图纸里的文字、表格、标注信息自动识别出来,结构化之后写入电子表格或数据库,替代人工翻图抄录。它适合两类人——一类是手头有大量历史图纸需要数字化的工程师,另一类是负责搭建企业图纸管理平台、需要把非结构化 PDF 变成可检索数据的开发人员。
核心难点在于:工程图纸不是普通文档。它的文字可能嵌在矢量图形里,可能旋转了 90 度,可能被线条穿过,扫描件还有噪声和倾斜。所以这套系统通常不是单一工具能搞定的,需要组合 PDF 解析库、OCR 引擎和规则后处理,必要时还要引入 AI 模型做版面分析和文字检测。下面把我实际落地过的方案拆开讲,从技术选型到代码实现到踩坑记录,尽量让看完的人能直接动手复现。
2. 图纸 PDF 的解析路线怎么选:矢量文字、扫描件和混合型的分流策略
2.1 先判断 PDF 类型,再决定用哪条解析链路
很多人拿到 PDF 直接上 OCR,结果发现原生矢量 PDF 里的文字本来就能直接读出来,白白浪费算力还引入识别误差。所以第一步永远是判断 PDF 的类型。
判断方法很简单:用 PyMuPDF(也就是 fitz)打开文件,遍历每一页,看page.get_text()返回的文本长度。如果一页图纸返回的字符数超过某个阈值(比如 50 个字符),说明这张图里有可提取的矢量文字层;如果返回空字符串或者只有零星几个字符,那基本就是扫描件,需要走 OCR 路线。
import fitz # PyMuPDF def classify_pdf_page(page, text_threshold=50): """ 判断单个 PDF 页面的类型 text_threshold: 矢量文字字符数阈值,低于此值判定为扫描件 返回: 'vector' | 'scanned' | 'mixed' """ text = page.get_text("text").strip() images = page.get_images(full=True) text_len = len(text) has_large_image = any(img[2] > 800 for img in images) # 宽像素 > 800 视为大图 if text_len >= text_threshold and not has_large_image: return 'vector' elif text_len < text_threshold and has_large_image: return 'scanned' else: return 'mixed' # 有文字层但覆盖了大图,需要两者结合这段代码的逻辑是:矢量文字足够多且没有大面积底图,判定为纯矢量 PDF;文字极少但有大尺寸嵌入图片,判定为扫描件;两者都有的,走混合处理。text_threshold这个参数需要根据你的图纸实际情况调——有些图纸标题栏有文字但图面是扫描的,阈值设太高会误判,我一般先用 50 试一批,看分类结果再微调。
2.2 矢量 PDF 用 PyMuPDF 直接抽取,注意文字块的坐标和旋转
对于矢量 PDF,PyMuPDF 的get_text("dict")能返回每个文字块的详细坐标、字体、大小和方向信息。工程图纸里经常出现竖排文字(比如侧边的设备位号),这些文字在 PDF 内部可能被标记了旋转角度,直接按阅读顺序提取会乱序。
def extract_vector_text(page): """ 从矢量 PDF 页面提取带坐标的文字块 返回: [{'text': str, 'bbox': (x0,y0,x1,y1), 'direction': (cos,sin)}, ...] """ blocks = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE)["blocks"] results = [] for block in blocks: if block["type"] != 0: # type 0 是文字块,1 是图片块 continue for line in block["lines"]: direction = line["dir"] # (cos, sin) 表示文字方向 text = "".join(span["text"] for span in line["spans"]) if text.strip(): results.append({ "text": text.strip(), "bbox": line["bbox"], "direction": direction }) return results关键参数是line["dir"],它是一个单位向量。水平文字是(1, 0),垂直向上是(0, -1),垂直向下是(0, 1)。后续做信息归类时,可以根据这个方向把文字分成水平组和垂直组,分别处理。TEXT_PRESERVE_WHITESPACE这个 flag 能保留空格,对表格类信息的列对齐有帮助。
2.3 扫描件走 OCR:PaddleOCR 和 Tesseract 的实际取舍
扫描件必须上 OCR。我试过 Tesseract 和 PaddleOCR,结论是:工程图纸场景优先用 PaddleOCR。原因有三个——第一,PaddleOCR 对中文和数字混排的识别率明显高于 Tesseract 默认的英文模型;第二,PaddleOCR 自带文字检测(DBNet)和方向分类,能处理图纸里旋转的文字;第三,它的angle_cls参数开启后会自动纠正 180 度颠倒的文字,这在扫描图纸里很常见。
from paddleocr import PaddleOCR # 初始化 OCR 引擎,首次运行会自动下载模型 ocr = PaddleOCR( use_angle_cls=True, # 开启方向分类,处理旋转文字 lang='ch', # 中英文混合识别 det_db_thresh=0.3, # 文字检测阈值,图纸线条多可适当降低 rec_batch_num=6 # 识别批大小,显存够可以调大 ) def ocr_scanned_page(image_path): """ 对扫描件图片做 OCR 返回: [{'text': str, 'confidence': float, 'bbox': [[x,y],...]}, ...] """ result = ocr.ocr(image_path, cls=True) items = [] for line in result[0]: bbox, (text, conf) = line if conf > 0.6: # 置信度低于 0.6 的结果直接丢弃 items.append({"text": text, "confidence": conf, "bbox": bbox}) return itemsdet_db_thresh这个参数值得说一下。默认值是 0.3,但工程图纸里线条密集,文字容易被线条干扰导致检测框断裂。如果发现文字被切碎,可以降到 0.2 试试;如果发现大量误检(把线条交叉点当成文字),就升到 0.4。rec_batch_num影响识别速度,6 是 4GB 显存下的保守值,8GB 以上可以调到 12。
2.4 混合型 PDF 的分层处理策略
混合型 PDF 是最麻烦的——图面是扫描图,但标题栏、明细表可能是矢量文字。我的做法是分层:先用 PyMuPDF 提取矢量文字层,记录每段文字的坐标;再把页面渲染成图片(page.get_pixmap(dpi=300)),对图片做 OCR;最后把两批结果按坐标做去重合并——如果 OCR 结果的坐标和矢量文字坐标重叠超过 70%,就丢弃 OCR 结果,保留矢量文字(因为矢量文字没有识别误差)。
这个去重逻辑用简单的 IoU(交并比)就能实现,不需要引入复杂模型。合并后的结果按 Y 坐标排序,再按 X 坐标排序,基本能还原图纸的阅读顺序。
3. 从文字到结构化数据:字段抽取、表格还原和电子表格输出
3.1 用正则和规则做第一轮字段抽取
拿到带坐标的文字块之后,下一步是把它们变成有意义的字段。工程图纸里最常见的信息类型有:设备位号(如 P-101A、V-203)、线缆规格(如 YJV-3x4)、管径(如 DN50、Φ89x4)、材料(如 Q235B、304SS)、图号(如 EQ-2024-001)。
这些字段的格式相对固定,用正则表达式能覆盖大部分场景:
import re # 常见工程字段的正则模式 PATTERNS = { "equipment_tag": re.compile(r'\b([A-Z]{1,3}-\d{2,4}[A-Z]?)\b'), "cable_spec": re.compile(r'\b(YJV|VV|KVV|BV)-?\d+[xX×]\d+(\.\d+)?\b'), "pipe_diameter": re.compile(r'\b(DN|Φ|φ|Ø)\s*\d+(\.\d+)?\b'), "material": re.compile(r'\b(Q235[AB]?|304SS|316L?|20#|16Mn)\b'), "drawing_no": re.compile(r'\b([A-Z]{2,4}-\d{4}-\d{3,5})\b'), } def extract_fields(text_blocks): """ 从文字块列表中抽取结构化字段 text_blocks: [{'text': str, 'bbox': tuple, 'direction': tuple}, ...] 返回: {'equipment_tag': [...], 'cable_spec': [...], ...} """ fields = {key: [] for key in PATTERNS} for block in text_blocks: text = block["text"] for field_name, pattern in PATTERNS.items(): matches = pattern.findall(text) for m in matches: value = m if isinstance(m, str) else m[0] fields[field_name].append({ "value": value, "bbox": block["bbox"], "source_text": text }) return fields这段代码的逻辑是遍历所有文字块,对每个块用所有正则模式去匹配。匹配到的结果连同坐标一起保存,方便后续追溯。注意findall在有多组括号时返回的是元组,所以加了一个isinstance判断来取第一个分组。
正则的局限在于:它只能匹配格式固定的字段。如果图纸里设备位号的命名规则不统一(有的用 P-101,有的用 PUMP-101),正则就会漏。这时候需要补充规则或者引入序列标注模型。
3.2 表格区域识别与还原
工程图纸的明细表(BOM 表)是信息密度最高的区域。表格还原的难点在于:PDF 里没有「表格」这个概念,只有一堆带坐标的文字和线条。我的做法是先用线条检测定位表格区域,再按坐标聚类还原行列。
import fitz import numpy as np def detect_table_regions(page, min_lines=4): """ 通过检测水平和垂直线段定位表格区域 min_lines: 一个区域至少需要的线条数 返回: [fitz.Rect, ...] 表格区域列表 """ drawings = page.get_drawings() h_lines, v_lines = [], [] for d in drawings: for item in d["items"]: if item[0] == "l": # 线段 p1, p2 = item[1], item[2] if abs(p1.y - p2.y) < 2: # 水平线 h_lines.append((min(p1.x, p2.x), max(p1.x, p2.x), p1.y)) elif abs(p1.x - p2.x) < 2: # 垂直线 v_lines.append((min(p1.y, p2.y), max(p1.y, p2.y), p1.x)) if len(h_lines) < min_lines or len(v_lines) < min_lines: return [] # 用线条的坐标范围聚类,找出表格边界 h_ys = sorted(set(round(l[2]) for l in h_lines)) v_xs = sorted(set(round(l[2]) for l in v_lines)) # 简单策略:取线条最密集的区域作为表格区域 regions = [] if len(h_ys) >= 3 and len(v_xs) >= 3: rect = fitz.Rect(min(v_xs), min(h_ys), max(v_xs), max(h_ys)) regions.append(rect) return regions拿到表格区域后,把落在区域内的文字块按 Y 坐标分行、按 X 坐标分列,就能还原出二维表格。行高和列宽的判定用聚类:Y 坐标差值小于行高阈值的归为同一行,X 坐标差值小于列宽阈值的归为同一列。阈值一般取文字块平均高度的 0.5 倍和平均宽度的 0.3 倍。
3.3 输出到电子表格:openpyxl 写入与格式保留
结构化数据最终要落到电子表格里。用 openpyxl 写入时,有几个细节值得注意:表头加粗、列宽自适应、数字和文本格式区分。
from openpyxl import Workbook from openpyxl.styles import Font, Alignment from openpyxl.utils import get_column_letter def export_to_excel(data_rows, headers, output_path): """ 将结构化数据写入 Excel data_rows: [[value1, value2, ...], ...] headers: ['设备位号', '规格', '数量', ...] """ wb = Workbook() ws = wb.active ws.title = "提取结果" # 写表头 for col_idx, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_idx, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal="center") # 写数据 for row_idx, row in enumerate(data_rows, 2): for col_idx, value in enumerate(row, 1): ws.cell(row=row_idx, column=col_idx, value=value) # 列宽自适应(按内容最大长度 + 4) for col_idx in range(1, len(headers) + 1): max_len = max( len(str(ws.cell(row=r, column=col_idx).value or "")) for r in range(1, len(data_rows) + 2) ) ws.column_dimensions[get_column_letter(col_idx)].width = max_len + 4 wb.save(output_path)列宽自适应那段用了一个简单的策略:遍历该列所有单元格,取内容最大长度加 4 作为列宽。实际用的时候如果列数很多(超过 20 列),这个循环会有点慢,可以改成只采样前 100 行来估算。
3.4 引入 AI 模型做版面分析的时机
什么时候需要上 AI 模型?我的判断标准是:如果规则方法的字段召回率低于 80%,或者图纸版式超过 5 种且差异很大,就值得引入版面分析模型。常用的方案是用 LayoutLM 或者 YOLO 做区域分类——把页面切成标题栏、明细表、图面注释、图框等区域,再对每个区域用对应的抽取策略。
但要注意,AI 模型不是银弹。它需要标注数据来微调,推理也需要 GPU。如果图纸量不大(几百张以内),规则方法加人工复核可能更快。我一般建议先用规则跑一遍,统计漏抽和错抽的比例,再决定要不要上模型。
4. 避坑与排查:图纸信息提取中最容易翻车的五个地方
4.1 中文乱码:字体未嵌入导致提取出方框
现象:PyMuPDF 提取出来的文字全是问号或者方框,复制到记事本里也显示不正常。
原因:PDF 制作时没有嵌入字体,或者用了非标准编码的自定义字体。这种情况下 PDF 里存的是字形索引而不是 Unicode 码点,任何工具都读不出正确文字。
解决:先用page.get_fonts()检查字体列表,看有没有嵌入。如果没有嵌入,只能走 OCR 路线——把页面渲染成高分辨率图片(300 DPI 以上),用 PaddleOCR 识别。渲染时用page.get_pixmap(dpi=300, colorspace=fitz.csRGB)确保颜色空间正确。
4.2 文字被线条穿过导致 OCR 检测框断裂
现象:OCR 识别出来的文字缺字、断句,比如「DN50」识别成「DN5」和「0」两段。
原因:工程图纸里文字经常和线条重叠,DBNet 的文字检测模型会把线条当成文字边界,导致检测框被切断。
解决:两个方向。一是预处理时做线条去除——用 OpenCV 的形态学操作检测长直线,然后涂白。二是调低det_db_thresh让检测框更宽松,再在识别后做拼接。我一般先用第二种,因为第一种容易误删文字笔画。
import cv2 import numpy as np def remove_long_lines(image_path, min_line_length=100): """ 去除图片中的长直线,减少对 OCR 的干扰 min_line_length: 超过此长度的线段才去除 """ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) binary = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 15, 10) # 水平线检测 h_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (min_line_length, 1)) h_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, h_kernel) # 垂直线检测 v_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, min_line_length)) v_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, v_kernel) # 从原图中减去线条区域 mask = cv2.bitwise_or(h_lines, v_lines) mask = cv2.dilate(mask, np.ones((3,3), np.uint8), iterations=1) result = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA) return resultmin_line_length这个参数要根据图纸尺寸调。A3 图纸 300 DPI 渲染后大约 5000 像素宽,线条长度阈值设 100 比较合适;A1 图纸要设到 200 以上。
4.3 坐标排序错乱:竖排文字和横排文字混在一起
现象:提取出来的文字顺序完全不对,标题栏的内容跑到了图面注释中间。
原因:PDF 里的文字块顺序是绘制顺序,不是阅读顺序。竖排文字和横排文字的坐标混在一起排序就会乱。
解决:先按direction字段把文字块分成水平组和垂直组,各自按坐标排序后再合并。水平组按 Y 降序、X 升序排;垂直组按 X 降序、Y 升序排。合并时以图面区域划分为准——标题栏通常在右下角,可以先按坐标把页面分成几个区域,每个区域内部再排序。
4.4 OCR 置信度阈值设太高导致漏抽
现象:明明图上有的字段,提取结果里就是没有。
原因:OCR 置信度阈值设得太高(比如 0.8),一些稍微模糊的文字被过滤掉了。
解决:把阈值降到 0.5 甚至 0.4,然后对低置信度的结果做二次校验——用正则匹配看是否符合字段格式,符合的保留,不符合的标记为待人工确认。这样既不会漏,也不会把噪声当成有效数据。
4.5 大批量处理时内存溢出
现象:处理到第几十张图纸时程序崩溃,报 MemoryError。
原因:PyMuPDF 的page.get_pixmap()返回的位图对象如果不手动释放,会一直占着内存。PaddleOCR 的模型也常驻显存。
解决:每处理完一页就del pixmap并调用gc.collect()。PaddleOCR 可以设置use_gpu=False走 CPU,虽然慢但内存稳定。如果必须用 GPU,每处理 50 页重启一次 OCR 引擎进程,用多进程池来管理。
5. 批量处理的工程化技巧:从单张调试到流水线跑通
单张图纸调通只是第一步,真正要落地得解决批量处理的问题。我一般会把整个流程拆成三个阶段:预处理阶段、提取阶段、后处理阶段,每个阶段用独立的进程池,中间结果落盘。
预处理阶段负责把 PDF 拆页、分类、渲染成图片。这一步是 IO 密集型的,用多线程就能跑满磁盘带宽。提取阶段是 CPU/GPU 密集型的,用多进程,每个进程独立加载 OCR 模型。后处理阶段做去重、字段校验和 Excel 写入,单线程就够了。
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor import os def batch_process(pdf_dir, output_dir, max_workers=4): """ 批量处理目录下所有 PDF max_workers: 提取阶段的并行进程数,建议不超过 GPU 数量的 2 倍 """ os.makedirs(output_dir, exist_ok=True) pdf_files = [f for f in os.listdir(pdf_dir) if f.lower().endswith('.pdf')] # 阶段一:多线程拆页和分类 with ThreadPoolExecutor(max_workers=8) as executor: page_tasks = [] for pdf_file in pdf_files: future = executor.submit(split_and_classify, os.path.join(pdf_dir, pdf_file), output_dir) page_tasks.append(future) # 阶段二:多进程提取 all_pages = [] for future in page_tasks: all_pages.extend(future.result()) with ProcessPoolExecutor(max_workers=max_workers) as executor: extract_results = list(executor.map(extract_single_page, all_pages)) # 阶段三:合并和输出 merged = merge_results(extract_results) export_to_excel(merged['rows'], merged['headers'], os.path.join(output_dir, '提取结果.xlsx')) return merged这个框架的关键在于阶段之间的解耦。预处理的结果存成中间文件(图片 + JSON 元数据),提取阶段崩了可以从中间文件恢复,不用从头跑。max_workers的设置要看硬件——4 核 CPU 加一张 8GB 显存的卡,提取阶段设 4 个进程比较稳,再多会抢显存。
还有一个实用技巧:给每张图纸生成一个处理日志,记录分类结果、OCR 置信度均值、抽取到的字段数量。批量跑完之后先看日志,把置信度低或者字段数为零的图纸挑出来人工复核,不用全部重跑。
最后说一个我自己的习惯:每次调整正则或者 OCR 参数之后,先拿 10 张有代表性的图纸跑回归测试,对比调整前后的字段召回率和准确率。没有这个对比,你根本不知道改动是优化还是劣化。这套系统不是调一次就完事的,图纸来源变了、格式变了,参数就得跟着调。希望帮到你。
本文还有配套的精品资源,点击获取