Python处理FCOM程序PDF:从文本抽取到全文检索的完整方案
2026/9/17 20:13:57 网站建设 项目流程

简介:这份FCOM程序参考手册以波音737飞行操作手册为蓝本,系统整理飞行运行中的标准操作与非正常处置逻辑,面向飞行仿真软件设计、航空培训课件开发及程序逻辑校验等场景。资源为单个PDF文档,大小仅18KB,页面紧凑但覆盖完整,已有236人学习/下载。文档从驾驶舱区域分工讲起,包含座椅调整、起飞及着陆标准动作、刹车冷却表查表方法、放行/天气/杰普逊资料认读等正常程序;也细化到中止启动条件、防冰使用规定、发动机起动电门使用时机、空速不可靠现象及处置、V1后发动机严重损坏、无线电通讯失效判断与处置等非正常情况。此外还列明B737侧风标准道面状况及风修正值计算方式、起飞形态警告与发动机火警处置原则。对于需要将操作手册转化为软件规则、检查单或培训材料的人员,这份PDF可提供直接的参考结构和关键参数。

1. 面对FCOM程序参考PDF,先把它当作数据处理而不是文档阅读

航司或维修单位的IT经常接到这样的文件:一个名为“FCOM程序[参考].pdf”的PDF,几千页,双栏排版,目录里的超链接有一半指向错误页码。FCOM是Flight Crew Operating Manual,飞行机组操作手册,里面装的是正常程序、非正常程序、系统描述和性能限制。这类手册的形势很特殊——它永远是“参考版”,永远在修订,读的人却必须依赖它执行关键步骤。

问题是:你不能靠人一页页划线标记,也不能把PDF直接扔进Windows搜索就完事。双栏文本会让关键词命中和实际章节错位;页脚带修订信息,版本之间差异难追踪。这里把结论先放在前面:把这份PDF当作需要批次清洗的数据,按“文本层判断 → 章节抽取 → 表格结构化 → 版本对比 → 全文检索”的顺序处理,比对几千页更可靠。这套做法同样适用船舶操作手册、重型设备维修手册,以及任何带版次控制的工程文档。下面从最容易翻车的第一关开始拆。

2. 拆FCOM程序PDF前的三道关卡:文本层、页眉干扰与编号规则

拿到“FCOM程序[参考].pdf”,第一件事不是写解析器,而是先回答一个问题:这个PDF里面的文字能不能被直接复制出来?很多参考版PDF其实是扫描件套了一层空壳,表面能打开,实际全是图片。跳过这一步,后面所有正则和表格抽取都会白做。

2.1 先判原生文本还是扫描件:一条命令看文本密度

这里常用的做法是用PyMuPDF打开文件,逐页统计文本字符量。原生生成的双栏FCOM,每页通常有2000个以上字符;扫描件一页只有0到几十个字符,差别非常明显:

import fitz # PyMuPDF def text_density_check(pdf_path: str): doc = fitz.open(pdf_path) total_chars = 0 empty_pages = 0 for page_num in range(doc.page_count): page = doc.load_page(page_num) chars = len(page.get_text()) total_chars += chars if chars < 30: empty_pages += 1 avg_chars = total_chars / doc.page_count print(f"页数={doc.page_count}, 总字符={total_chars}, 平均每页={avg_chars:.1f}, 空白页={empty_pages}")

这段代码的核心是把“能不能读”变成“平均每页字符数”这个可量化指标。判断标准可以按下面的经验值走:

平均每页字符数结论后续动作
大于 500原生文本层完整直接用正则和坐标抽取
50 到 500可能是混合排版或纯图页码较多抽样5页人工确认
小于 50基本是扫描件先走OCR管线,再做抽取

注意,用字符数做阈值存在误判空间。FCOM里系统图、极限性能表这些页面本身文字少,如果整份PDF平均在300字符左右,最好再查一下页面对应的内容,而不是直接判定为扫描件。我一般还会顺便统计一下每页的字体列表,原生文本页至少会包含Helvetica或Serif类字体信息,扫描件通常没有可用字体信息。

2.2 页眉页脚与双栏排版:先做坐标过滤,再做阅读顺序修复

FCOM的每一页都有不变的页眉:手册号、章节号、章节标题。页脚则是修订日期、打印日期、页码。如果不过滤它们,章节标题会被错误识别,“Rev 5”会混进正文里。这里需要按坐标过滤,而不是按文本内容过滤,因为页眉页脚的内容本身是合法的。

def clean_blocks(page): blocks = page.get_text("blocks") cleaned = [] width, height = page.rect.width, page.rect.height for block in blocks: x0, y0, x1, y1, text, *_ = block # 顶部12%是页眉区,底部6%是页脚区 if y0 < height * 0.12 or y1 > height * 0.94: continue if len(text.strip()) < 2: continue cleaned.append((round(y0, 1), round(x0, 1), text.strip())) # 按阅读顺序排序:先按y轴分桶,再按x轴从左到右 cleaned.sort(key=lambda item: (int(item[0] // 20), item[1])) return cleaned

代码里的height * 0.12height * 0.94是两个可调参数,前者圈定页眉区,后者圈定页脚区。FCOM不同机型的排版略有差异,有的页眉更厚实,建议把12%调整到15%再跑一遍,对比抽取结果的标题数量。阅读顺序排序那行是关键:int(item[0] // 20)把页面按20磅高度切成一条条横带,在每条横带内再按x坐标排序。20这个值来自对FCOM双栏字号的观察——正文字高通常在10到14磅,一行约18到20磅,用20做桶高基本能保证同一物理行落在一个桶里。若桶高设置成200,双栏右栏会被整体排到左栏后面。

2.3 程序步骤与章节编号的正则特征:先定规则再写逻辑

FCOM程序章节的编号规律非常稳定,例如1.2.3 LANDING,章节号后紧跟大写英文标题;程序步骤则常以“PFO”、“P”这类缩写开头。利用规则能快速切分章节边界:

import re SECTION_RE = re.compile(r"^\s*(\d+(?:\.\d+){1,2})\s+([A-Z][A-Z0-9 /&_-]{2,})$") STEP_RE = re.compile(r"^\s*(\[?[A-Z]{1,3}/[A-Z ]{1,4}\]?)\s+(.+)$")

SECTION_RE用来匹配数字.数字[.数字] 大写标题,比如3.2.1 ENGINE FIRESTEP_RE匹配PFOP/FO这类程序标识符。写正则时要注意两个坑:一是FCOM标题可能包含斜杠和连字符,字符集里必须显式加上;二是程序的“条件-动作-结果”表格里,第一列经常是缩写加斜杠,直接命中STEP_RE的误报率很高,所以抽取后要有一步“表格优先还是文本优先”的仲裁逻辑——表格里的内容优先按表格处理,不要混进文本流。

提示:这套正则不是为了覆盖所有机型FCOM,而是为了够用。不同厂家的手册编号规则不完全一致,但章节层级和程序标识符的大结构是类似的,规则可以复用。

3. 用Python搭一个FCOM程序PDF抽取管道

过了文本层判断这关,接下来要解决的是把整本PDF变成结构化数据。这里说的“结构化”,指的是能回答三个问题:某条程序在哪个章节、在PDF的哪一页、表格里有几列内容。下面给出一套最小可运行的管道,分三层递进。

3.1 最小可用抽取管道:文本、表格、元数据一次成型

读取PDF时不要把文本、表格、图片分开处理,那样后面还要再做页面级对齐。这里直接让每页输出一个JSON对象,包含文本块列表、表格对象和原始页面尺寸:

import json from pathlib import Path import fitz import pdfplumber def extract_fcom(pdf_path: str, header_ratio=0.12, footer_ratio=0.94): result = {} with pdfplumber.open(pdf_path) as pdf: for page_idx, page in enumerate(pdf.pages): width, height = page.width, page.height texts = [] tables = [] for block in page.extract_text_lines(): x0, top, x1, bottom = block["x0"], block["top"], block["x1"], block["bottom"] if top < height * header_ratio or bottom > height * footer_ratio: continue texts.append({ "text": block["text"], "x0": round(x0, 1), "top": round(top, 1), "x1": round(x1, 1), "bottom": round(bottom, 1), }) for table in page.extract_tables(): tables.append(table) result[page_idx + 1] = { "size": [width, height], "texts": texts, "tables": tables, } return result data = extract_fcom("FCOM程序[参考].pdf") Path("fcom_pages.json").write_text( json.dumps(data, ensure_ascii=False, indent=2) )

这套抽取方案里,pdfplumber负责表格和精细坐标,PyMuPDF负责快速文本密度检查,分工明确。extract_text_lines()extract_text()好用很多,因为它会返回每一行的bbox坐标,方便后续做阅读顺序排序。参数header_ratiofooter_ratio在函数签名里直接暴露,调优时不用翻主逻辑。输出JSON里保留x0和top值,是为了给后续的标注工具和问题复现留原始证据,这是处理工程手册时容易被忽略的一步。

3.2 章节树组装:把散落文本拼成可检索的结构

拿到逐页数据后,下一步是根据章节正则把文本块归入对应章节。不要试图一次性构建完整树,先做“章节号 → 起始页”的映射,再逐页追加:

from collections import OrderedDict def build_chapter_map(pages, section_re): chapter_map = OrderedDict() current_h2 = None current_h3 = None for page_no, page_data in pages.items(): for line in page_data["texts"]: text = line["text"].strip() match = section_re.match(text) if not match: continue section_id = match.group(1) section_title = match.group(2) depth = len(section_id.split(".")) if depth == 2: current_h2 = section_id current_h3 = None chapter_map.setdefault( f"{current_h2} {section_title}", {"start_page": page_no, "children": []} ) elif depth == 3 and current_h2: current_h3 = section_id chapter_map[f"{current_h2} {page_data['texts'][0]['text']}"]["children"].append( {f"{section_id} {section_title}": page_no} ) return chapter_map

这里的核心参数是depth。FCOM的标题层级一般到三级,二级是“正常程序”这种大类,三级是具体程序。二级标题出现时重置三级指针,三级标题挂到当前二级节点下。这个逻辑能处理最常见的版本:一个二级标题后面跟着若干个三级标题。注意,上面代码中使用page_data['texts'][0]['text']做键是为了避免维护另一个状态变量,如果页面上第一个文本不是真正的二级标题,这里会出错,更稳妥的做法是在current_h2变化时直接记录标题字符串。

3.3 表格抽取:FCOM程序表格的列与行策略

FCOM程序章节中有大量“条件-动作-响应”型表格,这类表格在PDF上的表现是带完整边框的。用pdfplumber的extract_tables()时,我会显式指定策略:

import pdfplumber table_settings = { "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, "join_tolerance": 3, "edge_min_length": 5, } with pdfplumber.open("FCOM程序[参考].pdf") as pdf: for page_no, page in enumerate(pdf.pages, 1): tables = page.extract_tables(table_settings) for table in tables: print(f"第{page_no}页 表格:{len(table)}行 x {len(table[0])}列")

vertical_strategyhorizontal_strategy都设为lines,含义是只按实际存在的线条切分表格,这在FCOM的线框表格上效果最稳。snap_tolerancejoin_tolerance是处理断线连接的参数,扫描版的表格线经常会断裂,调大到5可以多救回几列。实际使用常见的问题是表头跨页:一个表格从第132页断到第133页,后一页没有表头。处理方式很简单——检测到表格首行全是纯数字或空单元格时,把上一页表格的最后一行结构复制过来作为表头。

还有一个容易踩坑的点:PDF物理页码与FCOM印刷页码不一致。参考版大概率是重新排版过的,PDF第3页可能是印刷页1-00-02。做章节树和后续diff时,数据库里需要单独存两个页码字段:

字段示例说明
pdf_page45物理页索引,从1开始
printed_page1-31-05FCOM印刷页号,出现在页眉
section_id3.2.4所属章节号

印刷页号是追踪版本修订的主键,物理页码只是定位手段,这个关系别搞反。

4. 从difflib到SQLite FTS5:FCOM程序版次对比与全文检索

参考版FCOM最大的价值在“修订对比”。航空公司每年会收到几轮修订,需要知道新版改了哪几页、哪个程序步骤。直接对整个PDF做文本diff是灾难——只要前面插入一行,后面所有行都会标红。更实用的做法是先把文档按章节切好,再逐章做差异对比。

4.1 逐章diff:先切章节再比较,避免全局错位

import difflib def diff_section(old_lines: list[str], new_lines: list[str], section_name: str): differ = difflib.Differ() diff = list(differ.compare(old_lines, new_lines)) added = [line for line in diff if line.startswith("+ ")] removed = [line for line in diff if line.startswith("- ")] print(f"[ {section_name} ] 新增 {len(added)} 行,删除 {len(removed)} 行") for line in diff: if line.startswith(("+ ", "- ")): print(line) if len(line) > 200: break

调用前,需要把第三章的抽取结果按section_id切分,提取每个章节下的正文文本。difflib.Differ()输出的是带标记的行,+表示新增,-表示删除。逐章diff的好处是章节标题和页眉被排除后,步骤行号即使改变,也不会影响其他章节的比较结果。commonprefix这类算法在整书级别会失效,章节级别仍然可控。

4.2 基于SQLite FTS5建一个可查询的全文索引

版本对比做完,把新旧版本里“存活”的内容写入SQLite的FTS5虚拟表。FTS5是SQLite自带的全文检索模块,适合这种单机、千页级、需要快速命中的场景,不需要额外起一个Elasticsearch实例:

import sqlite3 conn = sqlite3.connect("fcom.db") conn.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS fcom_fts USING fts5( section_id, printed_page, content, tokenize='unicode61' ) """) rows = [] for sec_id, page_no, text in prepared_texts: rows.append((sec_id, str(page_no), text)) conn.executemany( "INSERT INTO fcom_fts(section_id, printed_page, content) VALUES (?, ?, ?)", rows, ) conn.commit()

FTS5的匹配语法默认把空格当作AND,pitch attitude会匹配两个词同时出现的行。要精确搜短语时需要加引号:"pitch attitude"snippet函数可以把命中上下文截取出来,减少应用层的拼装工作。

4.3 用FTS5的snippet参数控制命中上下文

query = '"pitch attitude"' sql = """ SELECT section_id, printed_page, snippet(fcom_fts, 2, '[', ']', '…', 15) FROM fcom_fts WHERE content MATCH ? """ for row in conn.execute(sql, (query,)): print(f"{row[0]} | {row[1]} | {row[2]}")

snippet函数的最后一个参数15表示输出15个token,前后用方括号标出命中词。FTS5检索词里带特殊符号时,程序步骤里的“1/2”这种文本很容易触发语法错误。稳妥做法是在组装query时对用户输入做一层转义:

def escape_match(query: str) -> str: query = query.replace('"', '""') special = ['(', ')', 'AND', 'OR', 'NOT', 'NEAR', '*'] for token in special: query = query.replace(token, f'"{token}"') return query.strip()

这里把可能干扰FTS5语法的token强制转成普通字符串,避免用户输入“P/FO AND NOT”这类组合时查询崩掉。FTS5的unicode61分词器对大小写不敏感,但斜杠会被当成普通字符保留下来,搜索“P/FO”时记得先做同样的转义。

注意:SQLite FTS5默认不处理中文分词,但FCOM正文以英文为主,unicode61足够。若要处理中文版手册,就得换分词器或引入额外的分词组件,这块不在本次标题范围内。

5. 给FCOM程序PDF的产出做一个可用的检索接口

前面的章节把抽取、入库都做完了,最后一步是给“FCOM程序[参考].pdf”这篇文章一个随时能查的入口。直接启动一个Web服务可能是过度设计,一个命令行接口在大多数内部场景里更实用,因为它不依赖额外依赖,也容易接进CI流程。

5.1 把查询包成一个十几行的CLI

#!/usr/bin/env python3 import argparse import sqlite3 def search_query(query: str, limit: int = 5): clean = query.replace('"', '""') conn = sqlite3.connect("fcom.db") sql = """ SELECT section_id, printed_page, snippet(fcom_fts, 2, '[', ']', '…', 15) FROM fcom_fts WHERE content MATCH ? LIMIT ? """ for section_id, page, snippet in conn.execute(sql, (clean, limit)): print(f"{section_id} | {page} | {snippet}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="FCOM program search") parser.add_argument("query", help="search text, e.g. 'engine fire'") parser.add_argument("--limit", type=int, default=5) args = parser.parse_args() search_query(args.query, args.limit)

这种接口的好处在于它是无状态、可编排的。内部工具链里可以直接调它验证“新版手册是否删除了某条程序步骤”,也能把输出接到企业微信机器人或内部Wiki,每天自动跑一遍关键步骤索引,检查新增了哪些修订页。

5.2 一个能直接复用的技巧:用NEAR运算符约束程序步骤距离

FCOM程序里经常出现这样的场景:搜“engine fire”想定位到非正常程序,但“engine”出现在第一行,“fire”出现在表格第三列,两个词相距上百个token,普通AND匹配会把大量系统描述页也带进来。这里可以手动在query里拼NEAR

query = 'engine NEAR/15 fire'

NEAR/15表示两个词在15个token的距离内必须同时出现,专门用于处理表格里“条件”和“动作”分布在同段落但位置错开的情况。这个距离值可以根据上一步的表格抽取情况调整,程序表格里同一行的token距离基本在5到10个以内。如果搜出来的结果仍然发散,把15收窄到8,精确度会显著提升。

把这条查询接到CLI里:

python fcom_cli.py "engine NEAR/15 fire" --limit 10

返回结果会直接从SQLite里把所在章节、印刷页码和命中上下文打出来。对于“FCOM程序[参考].pdf”这种三级标题加上百个程序步骤的手册,这套流程已经足够支撑日常的版本对比、程序查询和修订追踪。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询