1. 项目概述为什么“准确获取PDF标题”是个技术活在信息爆炸的时代PDF文件几乎成了数字文档的代名词。无论是学术论文、商业报告、电子书还是官方表格PDF格式因其跨平台、保真度高的特性而被广泛使用。然而当我们面对一个文件夹里成百上千个命名混乱的PDF文件时一个看似简单的问题就变得棘手起来如何快速、准确地知道每个PDF文件真正的“标题”是什么这里的“标题”通常指的是文档内容本身所定义的、最具代表性的那个名称它可能出现在封面页、页眉、或者元数据里但绝不等同于文件名。你可能会说这还不简单看文件名不就行了。但现实往往很骨感。我们下载的PDF文件名可能是“document.pdf”、“download.pdf”、“untitled.pdf”或者是一串毫无意义的数字和字母组合。即便文件名包含了一些信息比如“李大霄投资战略第三版.pdf”这也只是上传者或下载器赋予的名字文件内部的真实标题可能略有不同或者干脆就是“投资战略第三版”。更复杂的是一份PDF的内部标题可能存在于多个地方文档属性中的“标题”元数据字段、第一页的大号加粗文字、目录前的独立标题页甚至是页眉页脚里。这些来源可能一致也可能互相矛盾或者干脆缺失。因此“准确获取PDF文件中的标题”这个需求远不止是读取一个属性那么简单。它涉及到对PDF文档结构的理解、文本内容的智能解析、以及在不同信息源冲突时的决策逻辑。对于文档管理、知识库构建、学术资源整理乃至自动化办公流程来说这都是一个基础且关键的技术环节。无论是想用Python批量整理自己的电子书库还是开发一个智能文档处理系统亦或是为大型语言模型LLM准备高质量的文本数据第一步往往就是给文档“验明正身”。接下来我将结合多年处理文档的经验从原理到实践拆解如何实现这一目标。2. 核心思路与方案选型从“读取”到“理解”的跨越获取PDF标题本质上是一个信息检索与优先级判定的问题。我们不能指望一种方法在所有场景下都百分之百准确但可以设计一套鲁棒的流程在绝大多数情况下得到可靠的结果。这个流程的核心思想是多源采集优先级仲裁后处理校验。2.1 信息源的优先级定义首先我们需要明确PDF中可能包含标题的位置并为它们定义一个合理的检索优先级。根据可靠性和规范性我通常遵循以下顺序文档信息字典中的标题元数据这是最规范、最理想的来源。PDF标准定义了诸如/Title、/Author、/Subject等元数据字段。如果创建者规范地填写了这些信息从这里获取的标题是最准确的。它的优先级最高。文档结构树中的标题一些制作精良的PDF特别是来自LaTeX或专业排版软件会包含一个逻辑结构树其中明确标记了标题、章节等元素。从这里面提取的标题也非常可靠。第一页的视觉标题当元数据缺失时我们退而求其次分析文档第一页通常是封面或首页的版面布局寻找字体最大、位置最显眼如居中、靠上的文本块作为标题候选。文件名这是最不可靠的来源但可以作为最后的手段。通常需要对文件名进行清洗去除扩展名、下载后缀如“_downloaded”等并谨慎使用。注意优先级并非固定不变。例如如果从元数据提取的标题是“Untitled”或“Document”这显然是一个无效的占位符此时就应该降级其优先级转而依赖视觉分析或文件名。2.2 技术方案选型Python生态的优势实现上述思路Python是不二之选其丰富的库生态系统为我们提供了强大的工具。主要涉及以下几类库基础解析与元数据读取PyPDF2、pikepdf、PyMuPDF。这些库可以直接读取PDF的底层对象和元数据。PyMuPDF功能强大速度极快是处理复杂PDF的利器。高级版面分析与OCRpdfplumber、camelot。pdfplumber能提供精确的字符、线条和矩形框的位置信息对于分析页面布局、定位标题区域至关重要。对于扫描版PDF则需要结合pytesseract和pdf2image进行OCR识别。文本处理与清洗re正则表达式、unicodedata。用于清理提取到的文本去除多余空格、换行、非打印字符等。大语言模型辅助理解OpenAI API、LangChain。在传统方法失效或结果模糊时可以调用LLM基于页面内容智能推断文档标题。这是提升“准确率”和“智能化”上限的方案但依赖网络和API成本。为什么选择这样的技术栈PyPDF2或PyMuPDF用于获取“规范信息”元数据、书签这是成本最低、最快速的第一步。pdfplumber用于应对“非规范文档”它不假设文档结构良好而是通过分析页面上每一个字符的坐标和样式来重建版面这种方法更通用。LLM则是作为“终极裁判”和“智能补全”处理前两者都无法解决的边缘案例。这种组合拳兼顾了效率、通用性和智能化。3. 实操步骤一基础信息提取与元数据读取让我们从最简单的开始用代码来实现第一步读取PDF的元数据。3.1 使用PyMuPDF获取元数据首先安装必要的库pip install pymupdf。import fitz # PyMuPDF的导入名是fitz def get_pdf_metadata(pdf_path): 使用PyMuPDF打开PDF并提取元数据。 try: doc fitz.open(pdf_path) metadata doc.metadata doc.close() # metadata 是一个字典可能包含 title, author, subject 等键 title_from_metadata metadata.get(title, ).strip() return title_from_metadata except Exception as e: print(f读取PDF元数据时出错: {e}) return # 示例使用 pdf_path 李大霄投资战略第三版.pdf title get_pdf_metadata(pdf_path) if title: print(f从元数据获取的标题: {title}) else: print(未找到元数据标题。)实操心得PyMuPDF的.metadata属性返回的字典其键都是小写。但并非所有PDF的元数据都完整‘title’字段可能为空字符串、None或者是“Untitled”、“Document”这类无意义的默认值。因此获取到标题后必须进行有效性清洗。一个简单的启发式规则是如果标题为空、长度小于2个字符、或属于已知的无效标题列表如[‘untitled’ ‘document’ ‘无标题’]则视为无效。3.2 元数据标题的清洗与验证仅仅获取到文本还不够我们需要判断它是否是一个“像样”的标题。def is_valid_title(title): 判断提取到的元数据标题是否有效。 if not title: return False title_lower title.lower() # 定义常见的无效标题占位符 invalid_keywords [untitled, document, 无标题, 新文档, new document, ] # 检查是否完全由数字、空格或标点组成过于简单 if title_lower.strip() in invalid_keywords: return False # 可选检查长度太短的可能是无意义字符 if len(title) 2: return False # 可选检查是否包含大量乱码或非常见字符简易版 # 这里可以引入更复杂的文本质量检查 return True def get_and_validate_metadata_title(pdf_path): raw_title get_pdf_metadata(pdf_path) if is_valid_title(raw_title): return raw_title else: return None # 明确返回None表示元数据标题无效这一步是准确性的基石。很多自动化脚本失败就是因为盲目相信了元数据中的“Untitled”。通过添加清洗和验证逻辑我们就能过滤掉这些噪音为后续步骤提供更干净的数据。4. 实操步骤二基于版面分析的视觉标题提取当元数据不可靠或缺失时我们就需要“看”文档的第一页用程序模拟人眼寻找标题的过程。这里pdfplumber库是我们的主力。4.1 安装与基础页面分析安装pip install pdfplumberimport pdfplumber def extract_text_by_position(pdf_path, page_num0): 使用pdfplumber提取指定页面的所有文本并附带位置和字体信息。 titles_candidates [] try: with pdfplumber.open(pdf_path) as pdf: if page_num len(pdf.pages): return titles_candidates first_page pdf.pages[page_num] # 提取所有字符附带详细信息 chars first_page.chars # chars 是一个列表每个元素是字典包含 text, x0, top, x1, bottom, fontname, size 等 # 我们可以根据字体大小和位置进行聚类找出可能是标题的文本块 except Exception as e: print(f使用pdfplumber分析页面时出错: {e}) return titles_candidatespdfplumber的.chars属性提供了极其精细的控制但直接处理字符列表过于底层。更高效的方法是使用.extract_words()或.extract_text()并结合.rects、.lines来分析版面结构。4.2 实现智能标题定位算法一个在实践中效果不错的简单算法是寻找第一页中字体尺寸明显大于正文、且位于页面上半部分通常是顶部1/3区域的文本块。def find_visual_title(pdf_path, page_num0): 通过分析字体大小和位置定位第一页的视觉标题。 try: with pdfplumber.open(pdf_path) as pdf: if page_num len(pdf.pages): return None page pdf.pages[page_num] # 提取所有单词及其属性 words page.extract_words(extra_attrs[size, fontname, top]) if not words: return None # 计算字体大小的统计信息找出“大号”字体 sizes [w[size] for w in words] if not sizes: return None median_size sorted(sizes)[len(sizes) // 2] # 我们认为标题的字体至少是正文中位数的1.5倍 title_size_threshold median_size * 1.5 # 筛选候选字体大且位置靠上例如在页面高度30%以内 page_height page.height top_region_threshold page_height * 0.3 candidate_words [] for w in words: if w[size] title_size_threshold and w[top] top_region_threshold: candidate_words.append(w) if not candidate_words: # 如果没有找到明显的大字体则退回选择第一页最顶部的文本 words_sorted_by_top sorted(words, keylambda x: x[top]) if words_sorted_by_top: # 取顶部前几个单词可能是一行 top_words words_sorted_by_top[:5] # 按x坐标排序拼接成一行 top_words_sorted_by_x sorted(top_words, keylambda x: x[x0]) visual_title .join([w[text] for w in top_words_sorted_by_x]) return visual_title.strip() else: return None # 将候选单词按位置聚类成行简单的基于y坐标的聚类 candidate_words.sort(keylambda x: x[top]) lines [] current_line [candidate_words[0]] line_height_threshold candidate_words[0][size] * 0.5 # 行高容忍度 for w in candidate_words[1:]: if abs(w[top] - current_line[-1][top]) line_height_threshold: current_line.append(w) else: lines.append(current_line) current_line [w] lines.append(current_line) # 通常标题是顶部第一行或前几行 if lines: # 取最上面的一行按x坐标排序后拼接 top_line sorted(lines[0], keylambda x: x[x0]) visual_title .join([w[text] for w in top_line]) return visual_title.strip() else: return None except Exception as e: print(f定位视觉标题时出错: {e}) return None注意事项这个算法是启发式的对于排版规范的文档如论文、报告效果很好。但对于设计花哨、多栏排版或标题字体并不突出的文档可能会失效。line_height_threshold行高容忍度是个经验参数可能需要根据具体文档集微调。太大会把不同行的字合并太小会把一行拆散。有些文档第一页是空白页或封面图pdfplumber可能提取不到文字。这时需要考虑翻到第二页或者结合OCR。5. 实操步骤三处理扫描件与OCR集成对于扫描版的PDF即图片格式上述文本提取方法全部失效。我们必须借助OCR技术。这里我们使用Tesseract配合pdf2image将PDF页面转为图片。5.1 环境准备与OCR识别安装Tesseract从官方GitHub下载安装并确保其可执行文件路径在系统环境变量中。安装Python库pip install pytesseract pdf2image pillowimport pytesseract from pdf2image import convert_from_path import os def extract_title_via_ocr(pdf_path, page_num0, dpi200): 通过OCR识别扫描版PDF第一页的标题。 重点只对页面顶部区域进行OCR以提高速度和准确性。 try: # 将指定页面转换为图片 images convert_from_path(pdf_path, first_pagepage_num1, last_pagepage_num1, dpidpi) if not images: return None img images[0] width, height img.size # 定义标题区域通常为页面顶部 20%-30% 的区域 title_region_height int(height * 0.25) # 裁剪图片只保留顶部区域 title_region img.crop((0, 0, width, title_region_height)) # 配置Tesseract参数只识别大写字母、单行、假设文字较大 custom_config r--oem 3 --psm 6 -c tessedit_char_whitelistABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-:.,;()[] # ps m 6 表示假设为统一的文本块对标题区域比较合适 ocr_text pytesseract.image_to_string(title_region, configcustom_config, langengchi_sim) # 中英文混合 # 对OCR结果进行清洗去除多余空行取第一行或前几行作为标题候选 lines [line.strip() for line in ocr_text.split(\n) if line.strip()] if lines: # 简单策略取OCR结果的第一行非空文本 # 更复杂的策略可以分析字体大小需要Tesseract输出hOCR格式 potential_title lines[0] # 如果第一行非常短可能是页眉页码等可以尝试合并前几行 if len(potential_title) 5 and len(lines) 1: potential_title .join(lines[:2]) return potential_title else: return None except Exception as e: print(fOCR识别标题时出错: {e}) return None实操心得区域裁剪是关键对整个页面进行OCR既慢又不准。只对可能包含标题的顶部区域进行识别能显著提升准确率和速度。语言包务必根据文档语言安装对应的Tesseract语言包如chi_sim简体中文。lang参数可以指定多种语言如‘engchi_sim’。PSM参数Tesseract的页面分割模式Page Segmentation Mode对结果影响巨大。--psm 6假设文本为统一的单栏块适合标题区域。如果效果不好可以尝试--psm 3全自动但无定向或--psm 7将图像视为单行文本。OCR不是万能的对于字体奇特、背景复杂、倾斜严重的扫描件OCR识别率会下降。这是此类方案的固有瓶颈。6. 实操步骤四融合策略与LLM的智能仲裁现在我们有了从多个渠道获取标题的方法元数据、视觉分析、OCR。它们各自的结果可能不同也可能都失败。我们需要一个融合与仲裁策略来输出最终的最佳标题。6.1 设计多源融合决策流一个典型的决策流程可以这样设计def get_pdf_title_robust(pdf_path): 鲁棒性最强的PDF标题获取函数融合多源信息。 final_title None source Unknown # 1. 优先尝试元数据 meta_title get_and_validate_metadata_title(pdf_path) if meta_title: final_title meta_title source Metadata return final_title, source # 元数据有效则直接返回信任度最高 # 2. 尝试视觉分析针对数字版PDF visual_title find_visual_title(pdf_path) if visual_title and is_valid_title(visual_title): # 可以对视觉标题做进一步清洗比如去除页眉、页码等 cleaned_visual clean_extracted_title(visual_title) if cleaned_visual: final_title cleaned_visual source Visual Analysis # 这里不直接返回可以继续用OCR交叉验证如果怀疑是扫描件 # 3. 如果视觉分析结果不佳或为空尝试OCR应对扫描件 # 一个简单的启发如果pdfplumber提取到的字符数极少比如10可能是扫描件 try: with pdfplumber.open(pdf_path) as pdf: if pdf.pages: text_len len(pdf.pages[0].extract_text() or ) except: text_len 0 if text_len 20: # 假设第一页可提取文本少于20个字符可能是扫描件 ocr_title extract_title_via_ocr(pdf_path) if ocr_title and is_valid_title(ocr_title): # 如果OCR结果和视觉分析结果相似可以增强置信度 # 这里简单处理优先使用OCR结果因为视觉分析可能无结果 final_title ocr_title source OCR elif not final_title: # 如果之前视觉分析也没结果 final_title ocr_title # 即使OCR结果不完美也可能比没有强 source OCR (Fallback) # 4. 终极回退使用文件名不含扩展名 if not final_title or not is_valid_title(final_title): import os filename os.path.splitext(os.path.basename(pdf_path))[0] # 简单清洗文件名去除常见下载后缀 filename re.sub(r[\-_]*(downloaded|copy|final|version|\d)$, , filename, flagsre.IGNORECASE).strip() if filename and is_valid_title(filename): final_title filename source Filename else: final_title Unknown Title source Default return final_title, source这个流程体现了优先级元数据 视觉分析 ≈ OCR 文件名。同时加入了简单的扫描件探测逻辑。6.2 引入LLM进行智能修正与推断当传统方法提取的标题质量不高比如提取到了“第1章 引言”而不是真正的文档标题或者多个来源结果冲突时大语言模型可以作为一个强大的“智能仲裁器”。场景我们通过视觉分析从第一页提取到了一段文本“2023年年度报告 财务摘要 公司董事会”。这显然包含了标题但也混入了其他信息。我们可以将第一页的完整文本或前几段发送给LLM让它根据上下文推断出最合适的文档主标题。# 假设已配置好OpenAI API或其他LLM服务 import openai # 或使用LangChain def refine_title_with_llm(raw_extracted_text, candidate_titles): 使用LLM从提取的文本和候选标题中判断或生成最佳标题。 prompt f 你是一个文档处理专家。请分析以下从PDF文档第一页提取的文本内容并判断该文档的正式标题是什么。 如果提供的“候选标题”中有一个是合适的请直接输出那个最合适的标题。 如果都不合适或者没有提供候选标题请根据文本内容自己推断一个简洁、准确的文档标题。 **提取的文本内容开头部分** {raw_extracted_text[:1500]} # 限制长度控制token **目前已提取的候选标题可能不准确** {, .join(candidate_titles) if candidate_titles else 无} 请只输出你认为最准确的文档标题不要有任何额外的解释、引号或标记。 try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, messages[{role: user, content: prompt}], temperature0.1, # 低温度确保输出稳定 max_tokens100 ) llm_title response.choices[0].message.content.strip() # 清理LLM输出可能带的引号 llm_title llm_title.strip(\\“”) return llm_title if is_valid_title(llm_title) else None except Exception as e: print(f调用LLM修正标题时出错: {e}) return None使用策略LLM API调用有成本和延迟不应作为默认第一步。最佳实践是将其用作后处理步骤。例如在get_pdf_title_robust函数返回一个初步标题后如果对该标题的置信度不高例如来源是“Visual Analysis”且标题包含“Chapter”、“Abstract”等词则可以调用refine_title_with_llm传入第一页更多文本和初步标题让LLM做最终裁定。重要提示使用LLM时务必注意数据隐私。如果处理的PDF包含敏感信息不应将其发送到外部API。可以考虑使用本地部署的开源LLM如通过ollama、llama.cpp等。7. 常见问题、优化策略与避坑指南在实际批量处理成千上万个PDF的过程中你会遇到各种意想不到的情况。下面是我踩过坑后总结的一些经验和解决方案。7.1 典型问题与排查表问题现象可能原因排查与解决思路提取到的标题是乱码或“口口口”1. PDF使用非标准字体编码。2. 字体嵌入不全且系统缺少对应字体。1. 尝试使用pdfplumber时指定laparams参数提升布局分析。2. 使用PyMuPDF的get_text(“dict”)查看原始编码尝试用codecs解码。3. 终极方案对该页面进行OCR绕过字体编码问题。元数据标题为空但文件属性里明明有1. 查看文件属性的工具显示的是文件名而非元数据。2. 元数据存储在非标准字段。1. 用PyMuPDF的doc.metadata或exiftool命令行工具确认。2. 尝试用pikepdf库读取PDF的/Info和/Root字典查看所有键值。视觉分析总是抓到页眉或页码1. 页眉/页码的字体和位置可能与标题相似。2. 算法中的“顶部区域”阈值太高。1. 增加过滤规则排除包含“Page”、“第X页”、“- 1 -”等模式的文本。2. 将top_region_threshold从页面的30%降低到20%或15%。3. 尝试识别并忽略靠近页面边缘左、右、上边界5%以内的文本块。OCR识别标题速度很慢对整页进行高DPI OCR。1.务必裁剪只OCR顶部区域。2. 降低DPI如从300降到150在清晰度和速度间权衡。3. 考虑使用更快的OCR引擎如easyocr对中文友好或paddleocr。多栏文档中标题被误识别到右侧栏pdfplumber的extract_words默认按阅读顺序可能不准。1. 使用pdfplumber的extract_text并尝试不同布局分析参数laparams{}。2. 改用基于字符聚类和空间关系的自定义算法优先选择跨越多栏居中的大字体文本。批量处理时内存暴涨或程序崩溃1. 某些PDF损坏或结构异常。2. 未及时关闭文件对象。1. 在每个PDF处理流程外包裹try...except记录错误文件并跳过。2.确保使用with语句或在finally块中显式关闭fitz.Document和pdfplumber.open对象。3. 对于超大型PDF考虑分页处理不要一次性加载全部内容。7.2 性能优化与高级技巧并行处理如果需要处理数万个PDF单线程太慢。可以使用concurrent.futures.ThreadPoolExecutor进行多线程I/O密集型操作如元数据读取或用ProcessPoolExecutor进行多进程CPU密集型操作如OCR。注意OCR引擎通常不是线程安全的每个进程应初始化自己的OCR实例。缓存机制对于已经处理过的PDF可以将提取结果标题、来源、哈希值存入一个轻量级数据库如SQLite或JSON文件中。下次处理时先计算文件哈希如果哈希未变且缓存存在则直接读取结果避免重复计算。标题后处理提取到的原始标题往往需要清洗去除尾部的文件扩展名如“.pdf”被误读进来。合并因换行被拆散的单词。统一空格和标点格式。识别并去除常见的副标题前缀如“摘要”、“报告名称”。def clean_extracted_title(title): import re if not title: return title # 去除常见的引导词 patterns_to_remove [ r^标题[:\s], r^题名[:\s], r^名称[:\s], r^报告名称[:\s], ] for pattern in patterns_to_remove: title re.sub(pattern, , title, flagsre.IGNORECASE) # 合并连续的空白字符 title re.sub(r\s, , title).strip() # 去除首尾的特定标点 title title.strip( :;-–—,.) return title处理加密PDF如果PDF有密码需要在打开时提供。PyMuPDF和pdfplumber都支持open函数传入password参数。对于批量处理可以准备一个密码字典或列表进行尝试。7.3 一个完整的、生产可用的示例函数结合以上所有要点这里给出一个更健壮、可配置的完整函数示例import fitz import pdfplumber import os import re from typing import Tuple, Optional class PDFTitleExtractor: def __init__(self, ocr_enabledFalse, tesseract_pathNone, llm_enabledFalse): self.ocr_enabled ocr_enabled self.tesseract_path tesseract_path if tesseract_path and ocr_enabled: import pytesseract pytesseract.pytesseract.tesseract_cmd tesseract_path self.llm_enabled llm_enabled self._invalid_titles {untitled, document, 无标题, 新文档, } def _is_valid_title(self, title: str) - bool: 增强的标题有效性检查 if not title or not isinstance(title, str): return False t_lower title.lower().strip() if t_lower in self._invalid_titles: return False if len(t_lower) 2: return False # 检查是否大部分为标点或数字不太可能是标题 if re.fullmatch(r[\s\W\d_], t_lower): return False return True def extract(self, pdf_path: str) - Tuple[str, str]: 主提取函数返回标题来源 # 0. 基础检查 if not os.path.exists(pdf_path): return File Not Found, Error # 1. 元数据 meta_title self._get_metadata_title(pdf_path) if meta_title and self._is_valid_title(meta_title): return meta_title, Metadata # 2. 视觉分析 visual_title self._get_visual_title(pdf_path) # 对视觉标题进行强力清洗 cleaned_visual self._clean_title(visual_title) if visual_title else None if cleaned_visual and self._is_valid_title(cleaned_visual): # 检查是否抓到了页眉页脚 if not self._is_header_footer(cleaned_visual): return cleaned_visual, Visual Analysis # 3. 判断是否需要OCR检查第一页文本量 is_likely_scanned self._is_likely_scanned(pdf_path) ocr_title None if (is_likely_scanned or not cleaned_visual) and self.ocr_enabled: ocr_title self._get_ocr_title(pdf_path) cleaned_ocr self._clean_title(ocr_title) if ocr_title else None if cleaned_ocr and self._is_valid_title(cleaned_ocr): return cleaned_ocr, OCR # 4. 回退到文件名 filename_title self._get_title_from_filename(pdf_path) if filename_title and self._is_valid_title(filename_title): return filename_title, Filename # 5. 如果以上都有结果但质量存疑且启用了LLM可以尝试仲裁 candidates [t for t in [meta_title, cleaned_visual, ocr_title, filename_title] if t] if self.llm_enabled and candidates: first_page_text self._get_first_page_text(pdf_path, max_chars2000) if first_page_text: llm_title self._refine_with_llm(first_page_text, candidates) if llm_title: return llm_title, LLM Refined # 6. 最终回退 return os.path.splitext(os.path.basename(pdf_path))[0] or Unknown Title, Fallback # 以下为内部辅助方法的具体实现略可参考前文代码填充 def _get_metadata_title(self, pdf_path): ... def _get_visual_title(self, pdf_path): ... def _is_likely_scanned(self, pdf_path): ... def _get_ocr_title(self, pdf_path): ... def _get_title_from_filename(self, pdf_path): ... def _clean_title(self, title): ... def _is_header_footer(self, text): ... def _get_first_page_text(self, pdf_path, max_chars): ... def _refine_with_llm(self, context, candidates): ... # 使用示例 extractor PDFTitleExtractor(ocr_enabledTrue, llm_enabledFalse) title, source extractor.extract(你的文档.pdf) print(f最终标题: {title} (来源: {source}))这个类将功能模块化便于测试和扩展。你可以根据实际需求开关OCR和LLM功能并调整内部的判断阈值和清洗规则。