ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python将MIL-STD-975M标准PDF转为可查询SQLite数据库

用Python将MIL-STD-975M标准PDF转为可查询SQLite数据库 简介MIL-STD-975M(NASA)是1994年发布的美军/NASA联合标准为空间飞行硬件及关键地面支持设备提供电气、电子和机电EEE部件的统一选用与采购基线适用于航天系统设计师、元器件工程师及可靠性管理人员。标准首先界定标准件、非标准件及优选非标准件随后对QML合格制造商清单的1、2、3级流程入列准则作出规定并明确了部件应用、质量等级选择、非标准件使用控制及文件冲突处置要求可有效减少高可靠场景中的选型风险。该完整版PDF共1个文件文件大小13.61MB目录涵盖范围、引用文件、定义、一般要求等多层内容有利于按章节快速检索。已有142人学习浏览适合宇航级电子元器件管理、产品研制选型及标准培训等场合用作权威参考。1. 为什么一张没落的 PDF 标准还在消耗研发工时硬件工程师在选型时最常遇到的一种情况是手里已经有一份被外部合同引用的 MIL-STD-975M.pdf却没办法像数据库一样去查它。这份以 NASA 元器件优选清单为底子、后期由军用标准体系维护的文档核心价值不是原理设计而是告诉你“哪些元器件在特定任务里被认可哪些被限制哪些已经删除”。页数多、表格密、版本跨度大PDF 里的表格既有真实文本层也有扫描件或字体错位导致复制出来的行和列完全对不上。做物料管理、做白盒审查、做替代料比对的人都得咬着牙用肉眼逐行看。这篇博客就围绕这份 PDF讲清楚怎么把一个“只能翻”的标准文件变成“能查、能比对、能留痕”的结构化数据。我会从文档结构开始落到可运行的 Python 抽取方案、清洗入库和最后一层的版本验证适合被这种老化标准文档折磨过的一线工程师。2. 剖析 MIL-STD-975M 的文档物理结构先定抽取策略拿到任何一份老标准 PDF直接写代码抽取是大忌。先花 20 分钟摸清文档结构能省掉后面一整天的调试时间。MIL-STD-975M 这类文件通常由目录、正文清单、附录三块组成但 PDF 的物理页序和文档逻辑页码经常不一致前置页用的是 i、ii、iii正文从 1 重新开始。这种偏移如果不处理抽出来的数据就会整体错位。2.1 这类标准 PDF 的页眉页脚、表格与附录分布规律老式军用标准文档大多由排版系统生成版式特征非常稳定。正文部分基本是“每页一个三栏或五栏表格”表头在每页重复出现页脚固定是“MONITOR/STATUS”或“PAGE ___ OF ___”这类信息。表格里常见列包括器件型号、生产厂商、状态码、封装、注释偶尔会有“FIGURE”或“SHEET”跳转列。附录则可能是字母索引、厂商代码对照表或取消件列表这些区域没有统一表格线直接套用表格识别会漏行。另外需要特别留意的是PDF 可能有多个图层文字层、扫描底图层、批注层。用 PDF 阅读器能搜到关键词不代表用代码能按顺序读到文本。我一般先分别检查文字层与图像页分布再决定用文本流抽取还是用 OCR。判断依据很朴素如果一页的文字层字符数不足表格应有的三分之一基本就是扫描页。2.2 用 PyMuPDF 检查文本层与字体信息确认是否可抽取检查文本层的标准工具是 PyMuPDF也就是fitz。下面这段脚本能在一分钟内输出整份 PDF 的关键物理特征用来决定抽取路线。import fitz def inspect_pdf(path): doc fitz.open(path) print(页数:, doc.page_count) print(元数据:, doc.metadata) for i in range(doc.page_count): page doc[i] text page.get_text(text) imgs page.get_images(fullTrue) # 文本长度小于 50 字符的页基本可以判定为扫描页 if len(text.strip()) 50: print(f第{i}页: 文本极短({len(text.strip())}字符), f图片数{len(imgs)}, 疑似扫描页) # 统计字体种类官方 PDF 一般只出现 2-3 种字体 if i in (0, 5, 10): # 抽查前几页 fonts set() for block in page.get_text(dict)[blocks]: for line in block.get(lines, []): for span in line[spans]: fonts.add(span[font]) print(f第{i}页字体: {fonts}) doc.close() inspect_pdf(MIL-STD-975M.pdf)这段代码做了三件重要的事。第一件用page.get_text(text)提取纯文本并统计长度长度低于 50 字符的页大概率是扫描图后续要用 OCR 或绕过。第二件用page.get_images(fullTrue)检查页内图像数量辅助判断扫描页。第三件抽查关键页的字体集合如果字体超过 5 种说明文档可能是多个来源拼合而成表格列宽在不同章节可能不一致抽取参数要做分段处理。提示不要只抽查首页要抽查正文中段。老标准的首页和附录往往是重新排版的字体和页边距完全不同。2.3 根据结构确定按“区域抽取”还是“全文线性抽取”检查完结构后选择抽取策略基本遵循一个原则表格有明确框线且表头稳定用区域抽取文档中夹杂大量说明段落用全文线性抽取再切分。对 MIL-STD-975M 这种以表格为主体的标准我通常选区域抽取也就是直接告诉解析器“这一页的 x 坐标范围到 y 坐标范围是表格区域”。区域抽取的好处是能直接拿到二维行列结构不需要靠换行符推断列。缺点是参数敏感原文档左边距稍变列就可能错位。而全文线性抽取是按阅读顺序拼接文本代码简单但列和行的对应关系要靠字段特征硬猜比如型号通常是“MIL-”开头状态码是单个大写字母封装带数字与单位。实践里我更倾向于混合方案先区域抽取失败的行再用文本兜底。这也是后续几个章节反复用到的思路——不要迷信某一个库的默认行为。检查项区域抽取适用条件全文线性抽取适用条件表格框线线条完整清晰表格线断续或大量白底行表头重复每页表头固定表头偶尔缺失需按上下文补文本层质量字符顺序正确字符乱序但文本整体可读扫描混合情况扫描页占比低于 10%扫描页多于 20%需 OCR 介入3. 用 pdfplumber 把元器件清单抽成结构化表格结构摸清后正式进入抽取环节。pdfplumber是处理这类老式规则表格最顺手的工具它的核心能力是读取页面上的线条和字符坐标再把它们还原成单元格。相比直接调fitz的表格接口pdfplumber 暴露的参数更直观调试时能可视化输出每个单元格的边框这对对齐问题非常有帮助。3.1 抽取单页表格的最小代码先给一份能跑的完整代码定位到指定页并输出 DataFrame。注意这里不直接套用extract_tables()的默认参数而是显式声明策略好让后续调整参数时有基线可比。import pdfplumber def extract_table_from_page(pdf_path, page_index): with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_index] # 保留原表线用 lines 策略 table page.extract_table({ vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, }) if table is None: return [] # 去掉每页重复出现的表头行 # 判断逻辑第一个单元格包含 PART 或 型号 关键词 cleaned [] for row in table: if row and row[0] and PART in row[0].upper(): continue cleaned.append(row) return cleaned rows extract_table_from_page(MIL-STD-975M.pdf, 30) print(rows[:3])参数vertical_strategy和horizontal_strategy都设为lines意思是只按页面上的真实线条切分列和行。对于印刷规范的老文档很合适。snap_tolerance3表示直线距离在 3 磅内的线会被视为同一条线用来消除印刷毛刺。后面cleaned的逻辑丢弃了每页的表头避免最终数据里出现几十行“PART NUMBER”。提示如果extract_table()返回 None多半是这一页的表格线被扫描层盖住检测不到。可以先跑page.lines看线条数量。3.2 批量遍历多个 PDF合并 DataFrame做页码偏移校正一份标准往往由主文档和修订页组成修订页的页码并不连续直接把所有页的表格拼接起来会造成重复和漏项。正确处理方式是把每个 PDF 文件单独抽取然后按文件名记录来源再做逻辑页码与物理页码的映射。import pdfplumber import pandas as pd def extract_all_tables(pdf_path, start_page0, end_pageNone): all_rows [] with pdfplumber.open(pdf_path) as pdf: total len(pdf.pages) end_page end_page or total for page_idx in range(start_page, min(end_page, total)): page pdf.pages[page_idx] table page.extract_table({ vertical_strategy: lines, horizontal_strategy: lines, }) if not table: continue # 记录物理页码和文件名用于追踪 for row in table: if row and row[0] and PART in row[0].upper(): continue all_rows.append([os.path.basename(pdf_path), page_idx 1] row) return all_rows这段代码通过start_page和end_page控制范围配合page_idx 1记录物理页码。抽取完成后你会发现逻辑页码和物理页码经常相差十几页这是因为前置的修订记录占了大量篇幅。数据处理时不要用物理页码做排序要按照文档内出现的顺序重新编号否则后面建索引会出错。另一个常见问题是表格合并单元格pdfplumber 默认会重复填充单元格值比如一个跨两行的“-”会出现在第二行里需要在清洗阶段处理。3.3 参数怎么调vertical_strategy、x_tolerance、explicit_vertical_lines不同页面表格线质量不一时固定策略会漏行。这时要转向text策略即按文本字符的位置推断列边界。这个策略在表格线缺失时很有效但误判率也高。table page.extract_table({ vertical_strategy: text, horizontal_strategy: text, x_tolerance: 2, y_tolerance: 3, })x_tolerance是最值得调的参数。它表示同一列中字符间允许的最大水平间距单位是磅。老文档表头字符间距大默认的 3 磅会在“PART NUMBER”中间断列。我一般从 1 逐级调试到 5观察哪个值让“PART NUMBER”保持完整。y_tolerance控制同一行字符的垂直容差遇到字体字号不一时调大它能把同一行的散落文本聚合起来。如果某页表格线结构特殊直接用explicit_vertical_lines手工指定列线位置牺牲通用性换准确性。lines [page.rects[0].x0, page.rects[2].x0, page.rects[4].x0] table page.extract_table({ vertical_strategy: explicit, explicit_vertical_lines: lines, })我实际碰到最多的情况是一页里前 5 行正常第 6 行开始表线变浅。这时可以分段抽前 5 行走lines策略剩下的行走text策略最后拼接。不必追求一个参数吃遍全文档。4. 数据清洗与入库把混杂文本变成可查询的优选库表格抽出来只是第一步原样入库会带来大量问题器件型号前后有空格、状态码和注释挤在同一单元格、不同页的相同器件被重复收录。直接查你得到的是几百条“-”而不是能用 SQL 筛选的结果。这个阶段的核心是清洗、去重、构建索引。4.1 清洗器件型号、状态码与封装字段用正则归一化老标准文本的典型脏数据包括小写字母混入、全角与半角空格、型号中间被插入换行、单位上下标被拆开。下面是针对性的清洗函数import re def clean_part_number(raw): if not raw: return # 去掉所有空白字符型号内部不允许有空格 text re.sub(r\s, , raw) # 转大写标准型号通常大写 text text.upper() # 去掉常见误识别字符竖线和引号 text text.replace(|, I).replace(’, ) return text def clean_status_code(raw): if not raw: return # 状态码一般是 1-3 个大写字母或数字 match re.search(r[A-Z]{1,3}|\d{1,2}, raw) return match.group(0) if match else def clean_package(raw): if not raw: return text raw.strip() # 统一封装描述的分隔符方便后续分组 text re.sub(r[;/], , text) # 压缩多余空格 text re.sub(r\s, , text) return textclean_part_number里的关键点是“去掉型号内空白”因为 PDF 表格换行常把型号截断成两段。状态码用正则抓取前 1 到 3 个大写字母避免把注释整段带进来。封装字段则做分隔符统一把分号和斜杠都换成空格方便按封装类型做GROUP BY。4.2 建 SQLite 表解决重复器件与版本差异清洗后建库。SQLite 足够支撑单机查询几万行数据不需要引入 Postgres。建表时增加来源文件和行号字段为后续排查错误留证据。import sqlite3 conn sqlite3.connect(mil_std_975.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS parts ( id INTEGER PRIMARY KEY AUTOINCREMENT, part_number TEXT NOT NULL, status_code TEXT, package TEXT, source_file TEXT, page_no INTEGER, row_no INTEGER, UNIQUE(part_number, status_code, package) ) ) conn.commit()UNIQUE(part_number, status_code, package)这个约束直接把完全重复的行挡在门外。但要注意不同页面的同一个器件其状态码可能不同一个被标记为“R”一个标记为“D”这是版本迭代造成的真实差异不能简单去重。这里让“型号状态封装”唯一保留不同状态方便后续做版本验证。插入数据用executemany批量写入避免逐行提交导致的速度问题。如果原始行里有缺失值要先转换成None否则 sqlite 会报类型错误。cursor.executemany( INSERT OR IGNORE INTO parts (part_number, status_code, package, source_file, page_no, row_no) VALUES (?, ?, ?, ?, ?, ?) , clean_rows) conn.commit()4.3 查询示例按类型、状态、发布时间筛选基础设施就绪后下面是三类最常用的查询。按状态筛选找出所有被限制使用的器件SELECT part_number, package, page_no FROM parts WHERE status_code R ORDER BY part_number;按器件类型模糊匹配比如查所有电容器相关条目。LIKE在这里替代人工翻页SELECT DISTINCT part_number, status_code FROM parts WHERE part_number LIKE MIL-C-% OR part_number LIKE %CAP% ORDER BY part_number;按页号和来源文件聚合快速定位某个物理页面的所有条目适合和原文做人工比对SELECT source_file, page_no, COUNT(*) FROM parts GROUP BY source_file, page_no HAVING COUNT(*) 5;最后一个查询的HAVING COUNT(*) 5很有用。如果某页抽出的条目数量异常少说明表格识别漏列了该页需要重新调参。这不是数据分析技巧而是直接拿 SQL 当质量校验工具用。5. 最后一个技巧用版本差异对比验证抽取质量并保留证据链MIL-STD-975M 这类标准在工程现场通常有多个版本并存老的 N 版和当前 M 版印刷在同一本手册里。数据入库后应该立刻做一次全量差异对比——任何一处“看似消失”的器件都可能来自抽取错误或版本真实变更。最好的验证方法是用dframe集合差异输出新增和删除清单再看差异行是否落在可疑区域。import pandas as pd def compare_versions(old_numbers, new_numbers): new_set set(new_numbers) old_set set(old_numbers) added sorted(new_set - old_set) removed sorted(old_set - new_set) return added, removed # 假设已经从数据库读入两个版本 old_parts [row[0] for row in cursor.execute( SELECT part_number FROM parts WHERE source_file LIKE %N%)] new_parts [row[0] for row in cursor.execute( SELECT part_number FROM parts WHERE source_file LIKE %M%)] added, removed compare_versions(old_parts, new_parts) print(新增:, len(added), 删除:, len(removed))这段代码的价值不在于 print 结果而在于把差异限制在可解释的范围内。如果删除数量超过整体条数的 10%那大概率是 M 版文档页序有变不是器件真被删了。这时回到原文用物理页码加 20 页对比往往能在某个扫描页里找回整段数据。验证通过后顺手导出一份标记差异清单连同源 PDF 页码一起存档。后续有人问“这个器件为什么被标记删除”直接看清单里的page_no和status_code就能答复不用再打开 PDF 翻找。这一步不会产生炫酷的可视化但它把抽取工作从一次性脚本变成了可追溯的工程资产。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进