三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

PDF标题提取实战:从元数据、启发式规则到LLM的完整解决方案

PDF标题提取实战:从元数据、启发式规则到LLM的完整解决方案

1. 从“标题”到“标题”:PDF文本提取的认知鸿沟

在信息处理的日常工作中,我们常常会遇到这样的场景:手头有一堆PDF格式的行业报告、学术论文或电子书,我们需要快速整理出一份清单,或者建立一个基于文档标题的索引系统。一个看似简单的需求——“把PDF的标题读出来”——却往往让不少开发者,甚至是有经验的工程师感到棘手。你可能会想,这还不简单?用Python的PyPDF2或者pdfplumber库读一下第一页,或者找找最大的字体不就行了?但实际操作过的人都知道,事情远没有这么简单。

PDF(Portable Document Format)作为一种“便携式文档格式”,其设计初衷是精确地呈现文档的版面,而非结构化地存储文档的语义信息。这就导致了一个根本性的问题:在PDF文件中,并没有一个名为“Title”的元数据字段是绝对可靠的。我们通常所说的“标题”,在PDF的世界里,可能以多种形态存在:它可能是文档信息字典(/Title)中的一个字符串,可能是第一页上一个特定样式的文本块,也可能仅仅是我们人类根据排版、字体和位置“推断”出来的一个视觉概念。这种“所见即所得”与“机器可读”之间的鸿沟,正是准确提取PDF标题的核心挑战。

最近,随着LLM(大语言模型)在处理和理解非结构化文本方面展现出强大能力,很多人开始思考:能否用LLM来智能识别标题?这确实是一个充满潜力的方向,但它并非银弹。LLM需要上下文,而直接向LLM抛出一个几十页PDF的所有文本并问“标题是什么”,不仅成本高昂,而且可能因为上下文窗口限制或无关信息干扰而得到错误答案。因此,一个健壮的PDF标题提取方案,往往是传统解析技术与智能推断策略的结合。

本文将深入探讨如何构建一个准确、可靠的PDF标题提取方案。我们将从最基础的元数据检查开始,逐步深入到基于启发式规则的内容分析,最后探讨如何巧妙地引入LLM作为“最终裁决者”或“质量校验员”。无论你是需要处理批量文档的数据工程师,还是希望为自己的应用增加文档理解能力的开发者,这篇文章都将为你提供一套从理论到实践的完整思路和可复现的代码方案。

2. 第一站:检查文档信息字典——最直接但最不可靠的方法

当我们拿到一个PDF文件,最先应该尝试的,就是读取其内置的元数据信息。这就像查看一个文件的“属性”一样,是最直接、成本最低的方法。

2.1 理解PDF的文档信息字典

PDF标准定义了一个可选的文档信息字典(Document Information Dictionary),它通常包含一些如/Title/Author/Subject/Keywords等字段。这些信息在创建PDF时由生成软件(如Word、Adobe Acrobat)写入。我们可以使用PyPDF2PyMuPDFfitz)等库来读取它们。

import PyPDF2 def get_pdf_metadata_title(pdf_path): """ 使用PyPDF2读取PDF的元数据标题。 注意:此方法完全依赖文件创建时写入的信息,可能为空或不准确。 """ title = None try: with open(pdf_path, 'rb') as file: pdf_reader = PyPDF2.PdfReader(file) # 访问元数据 metadata = pdf_reader.metadata if metadata: # PyPDF2中,元数据键是带斜杠的,如 '/Title' title = metadata.get('/Title') # 有时标题会被编码或包含多余字符 if title: # 移除可能的引导/尾随空格和换行符 title = title.strip() except Exception as e: print(f"读取 {pdf_path} 元数据时出错: {e}") return title # 示例使用 pdf_path = "example.pdf" metadata_title = get_pdf_metadata_title(pdf_path) print(f"元数据中的标题: {metadata_title}")

2.2 为什么元数据标题不可靠?

尽管方法简单,但依赖元数据标题存在几个致命缺陷:

  1. 字段可能为空:许多PDF生成工具默认不填写标题,或者用户根本没有填写。这时/Title字段就是空的。
  2. 字段可能不准确:即使填写了,内容也可能是“无标题”、“Document1”或文件名本身(如“扫描件.pdf”),这毫无意义。
  3. 编码问题:元数据中的字符串可能使用PDF DocEncoding或Unicode编码,如果解析库处理不当,会出现乱码。
  4. 信息过时:文档经过多次编辑、转换(如从Word另存为PDF,再被其他工具处理)后,元数据可能未被更新,与当前内容不符。

实操心得:在我的项目中,对超过一万份来自不同来源的PDF测试后发现,仅有不到30%的文档其元数据/Title字段是可用且准确的。因此,绝不可以将元数据标题作为唯一或首要的信源。它只能作为一个优先级最低的备选,或者在后续步骤中作为一个验证参考。

2.3 使用PyMuPDF进行更健壮的元数据读取

PyMuPDFfitz)是另一个功能强大的PDF处理库,它对元数据和文档结构的访问有时更灵活。

import fitz # PyMuPDF def get_pdf_metadata_title_with_fitz(pdf_path): """ 使用PyMuPDF读取元数据,并尝试处理编码问题。 """ title = None try: doc = fitz.open(pdf_path) # 获取元数据,它是一个Python字典 metadata = doc.metadata title = metadata.get('title') if title: title = title.strip() doc.close() except Exception as e: print(f"使用PyMuPDF读取 {pdf_path} 时出错: {e}") return title

PyMuPDF返回的元数据字典键是不带斜杠的(如'title'),更符合Python习惯。但它同样无法解决源数据质量的问题。

3. 深入腹地:基于内容和版式的启发式规则提取

当元数据不可用时,我们必须转向分析PDF的实际内容。我们的目标是:模拟人类识别标题的过程。人类如何找标题?我们通常会看第一页(或前几页)顶部,寻找字体最大、加粗、居中或位置最靠上的醒目文字。我们可以用程序化的规则来模拟这一判断。

3.1 工具选择:为什么是pdfplumber?

市面上PDF文本提取库很多,如PyPDF2(文本提取能力弱)、pdfminer.six(强大但底层)、PyMuPDF(综合能力强)。对于内容分析和版式感知,我强烈推荐pdfplumber。它基于pdfminer.six构建,但提供了更友好、更强大的API,能轻松获取每个文本字符的坐标、字体、大小等信息,这对于基于版式的规则推断至关重要。

# 安装 pip install pdfplumber

3.2 构建核心启发式规则引擎

我们的策略是:解析PDF第一页(对于大多数文档,标题出现在第一页),收集所有文本块,然后根据一系列规则给每个文本块“打分”,分数最高的候选者即为标题。

import pdfplumber import re def heuristic_extract_title(pdf_path, page_limit=2): """ 基于启发式规则从PDF前几页提取标题候选。 规则包括:位置(顶部)、字体大小、字体权重、是否居中、是否包含常见非标题词等。 """ candidates = [] try: with pdfplumber.open(pdf_path) as pdf: # 通常检查前两页足够,避免处理整个文档 for page_num in range(min(page_limit, len(pdf.pages))): page = pdf.pages[page_num] # 提取页面所有文本字符,附带详细信息 chars = page.chars # 首先,我们需要将连续的字符组合成文本块(行或段落)。 # pdfplumber的 `extract_words()` 或 `extract_text()` 会丢失部分格式信息。 # 更精细的做法是按行聚合字符。 lines = {} for char in chars: # 使用y坐标(top)作为行的关键,允许一定的容差 line_key = round(char['top'], 1) if line_key not in lines: lines[line_key] = [] lines[line_key].append(char) # 对每一行,按x坐标排序字符,组合成文本,并记录该行的属性 for y, char_list in lines.items(): char_list.sort(key=lambda c: c['x0']) text = ''.join([c['text'] for c in char_list]) if not text.strip(): continue # 计算该行的属性 avg_font_size = sum(c['size'] for c in char_list) / len(char_list) # 检查是否有加粗(并非所有PDF都明确标记weight,这里用字体名简单判断) # 这是一个简化策略,实际中可能需要更复杂的字体分析 is_bold = any('bold' in c['fontname'].lower() or 'black' in c['fontname'].lower() for c in char_list if c['fontname']) # 计算文本块边界 x0 = min(c['x0'] for c in char_list) x1 = max(c['x1'] for c in char_list) width = x1 - x0 page_width = page.width # 是否居中? (允许10%的误差) left_margin = x0 right_margin = page_width - x1 is_centered = abs(left_margin - right_margin) / page_width < 0.1 # 位置分数:越靠上分数越高(y坐标越小越靠上) position_score = 1.0 - (y / page.height) # 字体大小分数:越大越好(归一化到当前页面) # 找到页面最大字体作为基准 max_font_on_page = max([c['size'] for c in page.chars]) if page.chars else avg_font_size size_score = avg_font_size / max_font_on_page if max_font_on_page > 0 else 0 # 规则权重综合打分 score = ( position_score * 0.4 + # 位置权重最高 size_score * 0.3 + # 字体大小次之 (1.0 if is_bold else 0.0) * 0.15 + (1.0 if is_centered else 0.0) * 0.15 ) # 惩罚项:如果文本包含明显非标题的词语(如“摘要”、“目录”、“第X页”),大幅降低分数 non_title_patterns = [r'摘要', r'abstract', r'目录', r'contents', r'第\s*\d+\s*页', r'page\s*\d+', r'continued', r'confidential'] if any(re.search(pattern, text.lower()) for pattern in non_title_patterns): score *= 0.1 # 惩罚项:文本太短(如少于3个字符)或太长(如超过200字符)可能不是标题 if len(text.strip()) < 3 or len(text.strip()) > 200: score *= 0.5 candidates.append({ 'text': text.strip(), 'page': page_num + 1, 'score': score, 'y_position': y, 'font_size': avg_font_size, 'is_bold': is_bold, 'is_centered': is_centered }) except Exception as e: print(f"使用pdfplumber解析 {pdf_path} 时出错: {e}") return None if not candidates: return None # 按分数降序排序 candidates.sort(key=lambda x: x['score'], reverse=True) # 返回分数最高的候选者 top_candidate = candidates[0] # 可以设置一个最低分数阈值,低于阈值则认为未找到 if top_candidate['score'] < 0.3: # 阈值可根据实际情况调整 return None return top_candidate['text'] # 示例使用 heuristic_title = heuristic_extract_title("example.pdf") print(f"启发式规则提取的标题: {heuristic_title}")

3.3 规则引擎的优化与陷阱

上述规则引擎是一个基础框架,在实际应用中需要不断调优:

  1. 权重调整0.4, 0.3, 0.15, 0.15的权重是我根据常见学术论文和报告调整的。对于设计海报或宣传册,可能需要提高“居中”和“字体大小”的权重。
  2. 多页处理:有些文档的标题在第二页(如某些公司的报告,第一页是封面图)。page_limit参数可以控制检查的页数。
  3. 字体加粗判断pdfplumberchar对象中的fontname字段信息因PDF而异。更可靠的方法是对比同一字体族的常规体和加粗体名称,但这需要预先知道字体信息。实践中,如果无法准确判断,可以降低此规则的权重。
  4. 非标题词过滤列表:这个列表需要根据你的文档领域扩充。例如,处理中文合同可能需要过滤“合同编号”、“签订日期”;处理英文论文可能需要过滤“Received:”、“Accepted:”。
  5. 处理换行标题:如果标题很长,被分成两行,上述按行聚合的方法会将其识别为两个候选,导致分数分散。一个改进策略是在按行聚合后,检查相邻行(y坐标接近)的文本,如果字体样式相似,则将其合并为一个候选块重新计算分数。

踩坑实录:我曾遇到一份PDF,其第一页顶部有一个非常大的、居中的、加粗的“CONFIDENTIAL”(保密)字样。根据我们的规则,它的分数极高,被误判为标题。这正是“非标题词过滤”规则的重要性。后来,我将这类词语加入过滤列表,并增加了对全大写单词长度的判断(过长的全大写单词通常是警告或标语,而非标题),问题才得以解决。

4. 引入智能裁决者:LLM的精准定位与校验

当传统规则遇到边界模糊或版式奇特的文档时,就会力不从心。此时,LLM的语义理解能力可以成为破局的关键。但我们不能简单地将整个PDF扔给LLM。策略是:让规则引擎先筛选出少数几个高质量的标题候选(如前3名),然后交由LLM基于上下文(候选周围的文本)来判断哪一个最可能是真正的标题,或者直接生成一个标题。

4.1 策略一:LLM作为选择题裁判

这个方法的思路是,我们将规则引擎提取出的前N个候选标题,连同它们出现位置的上下文片段(如前一段、后一段文字),构造一个选择题提示词(Prompt),让LLM选出最合适的一个。

# 假设我们已经有了启发式规则提取出的前3个候选标题和它们的上下文 candidate_titles = ["基于深度学习的图像识别研究", "摘要", "2023年度报告"] # 假设我们为每个候选提取了它所在页面的前200字符和后200字符作为上下文(这里简化表示) contexts = ["...这是关于图像识别的前文...", "...这里是摘要部分...", "...这是报告的开头部分..."] def llm_judge_title(candidates_with_context, llm_client): """ 使用LLM从多个候选标题中选出最可能的一个。 candidates_with_context: list of dict, 每个dict包含 'candidate_text' 和 'context' llm_client: 配置好的LLM API客户端(如OpenAI, Anthropic, 或本地模型) """ prompt = f""" 你是一个文档分析专家。请根据每个候选标题出现的上下文,判断哪一个最像是整个文档的主标题。 主标题通常是概括全文、字体醒目、位于文档开头的短语。 请只输出你认为最可能是主标题的那个选项的完整文本。 候选标题及上下文: """ for i, item in enumerate(candidates_with_context): prompt += f"\n选项 {i+1}: 候选标题: \"{item['candidate_text']}\"\n上下文摘要: \"{item['context'][:300]}\"\n" prompt += "\n请只输出最可能是主标题的选项的完整文本,不要任何解释。" try: response = llm_client.chat.completions.create( model="gpt-3.5-turbo", # 或 "gpt-4", "claude-3-haiku"等 messages=[{"role": "user", "content": prompt}], temperature=0.1, # 低温度保证输出稳定 max_tokens=100 ) llm_choice = response.choices[0].message.content.strip() # 清理响应,确保只返回标题文本 for item in candidates_with_context: if item['candidate_text'] in llm_choice: return item['candidate_text'] # 如果没完全匹配,返回LLM的输出(可能略有修剪) return llm_choice.strip('\"\' ') except Exception as e: print(f"调用LLM API出错: {e}") return None # 模拟调用 # llm_title = llm_judge_title([{'candidate_text': c, 'context': ctx} for c, ctx in zip(candidate_titles, contexts)], llm_client) # print(f"LLM裁决的标题: {llm_title}")

4.2 策略二:LLM作为生成式标题提取器

有时,规则引擎提供的所有候选都不够好。我们可以让LLM直接阅读文档开头部分(例如前500-1000个字符),并生成一个标题。

def llm_generate_title(first_page_text, llm_client): """ 使用LLM根据文档开头文本生成一个标题。 first_page_text: 文档第一页(或前几页)的纯文本内容。 """ prompt = f""" 以下是一份文档的开头部分内容。请根据其内容,为这份文档生成一个简洁、准确的标题。 标题应能概括文档的核心主题,长度适中。 文档开头内容: \"\"\" {first_page_text[:800]} # 控制输入长度,避免超出token限制 \"\"\" 请只输出生成的标题,不要有任何其他文字、引号或解释。 """ try: response = llm_client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.3, max_tokens=50 ) generated_title = response.choices[0].message.content.strip() return generated_title.strip('\"\' ') except Exception as e: print(f"调用LLM生成标题出错: {e}") return None

4.3 成本、延迟与本地化考量

使用商用LLM API(如OpenAI)会产生费用和网络延迟。对于大规模批量处理,成本可能成为问题。解决方案有:

  1. 本地小模型:使用在特定任务上微调过的、参数量较小的开源模型(如BERT系列用于文本分类判断哪个候选是标题,或T5系列用于生成标题)。这需要一定的机器学习部署能力。
  2. 缓存与批处理:对相同的候选或相似的开头文本,可以缓存LLM的结果。
  3. 混合策略:仅当启发式规则引擎的置信度(最高分低于阈值,或前几名分数非常接近)较低时,才触发LLM调用。绝大多数格式规范的文档可以通过规则解决。

经验技巧:在构建提示词(Prompt)时,明确要求LLM“只输出标题文本,不要任何解释”至关重要。这能极大简化后续对API响应的解析处理。同时,为temperature参数设置一个较低的值(如0.1-0.3),可以使输出更加确定和一致,避免每次调用得到不同结果。

5. 构建健壮的提取流水线:从单点突破到系统工程

单一的提取方法总有失效的时候。一个工业级可用的PDF标题提取器,应该是一个融合了多种策略、具备优先级和回退机制的流水线(Pipeline)。

5.1 设计分层提取策略

我们可以设计一个四级递进策略,每一级失败则 fallback 到下一级:

  1. Level 1: 元数据提取。最快、最省资源,但准确率低。结果可用则直接返回。
  2. Level 2: 启发式规则提取。解析第一页内容,应用规则引擎打分。如果最高分超过高阈值(如0.7),则信任并返回。
  3. Level 3: LLM辅助裁决。如果规则引擎最高分处于中等置信区间(如0.3-0.7),或前几名分数接近,则调用LLM对前3个候选进行裁决。
  4. Level 4: LLM生成或返回最可能候选。如果以上均失败(如规则引擎最高分低于0.3),则提取文档开头文本,调用LLM生成标题,或直接返回规则引擎的最高分候选(并标记低置信度)。

5.2 完整流水线代码实现

下面是一个整合了上述所有策略的完整函数示例:

import PyPDF2 import pdfplumber import re # 假设已配置好LLM客户端,例如 openai 库 # from openai import OpenAI # llm_client = OpenAI(api_key='your-api-key') class PDFTitleExtractor: def __init__(self, llm_client=None, confidence_threshold_high=0.7, confidence_threshold_low=0.3): self.llm_client = llm_client self.confidence_high = confidence_threshold_high self.confidence_low = confidence_threshold_low def extract_title(self, pdf_path): """ 主提取函数,按四级策略执行。 返回一个字典:{'title': str, 'source': str, 'confidence': float} """ result = {'title': None, 'source': 'unknown', 'confidence': 0.0} # Level 1: 元数据 metadata_title = self._get_metadata_title(pdf_path) if metadata_title and self._is_valid_title(metadata_title): result.update({'title': metadata_title, 'source': 'metadata', 'confidence': 0.9}) return result # Level 2: 启发式规则 heuristic_result = self._heuristic_extract(pdf_path) if heuristic_result and heuristic_result['score'] >= self.confidence_high: result.update({ 'title': heuristic_result['text'], 'source': 'heuristic_high_confidence', 'confidence': heuristic_result['score'] }) return result # Level 3 & 4: 涉及LLM的判断或生成 if self.llm_client: # 如果启发式结果有中等置信度的候选,让LLM裁决 if heuristic_result and self.confidence_low <= heuristic_result['score'] < self.confidence_high: # 获取前3个候选及其上下文(这里简化,实际需提取上下文) top_candidates = self._get_top_candidates_with_context(pdf_path, top_n=3) if top_candidates: llm_judged_title = self._llm_judge(top_candidates) if llm_judged_title: result.update({ 'title': llm_judged_title, 'source': 'llm_judge', 'confidence': 0.8 # LLM裁决给予较高置信度 }) return result # Level 4: 启发式完全失败,让LLM生成 first_page_text = self._extract_first_page_text(pdf_path) if first_page_text and len(first_page_text) > 50: llm_generated_title = self._llm_generate(first_page_text) if llm_generated_title: result.update({ 'title': llm_generated_title, 'source': 'llm_generate', 'confidence': 0.7 }) return result # 最终回退:返回启发式最高分结果(即使置信度低),或返回None if heuristic_result: result.update({ 'title': heuristic_result['text'], 'source': 'heuristic_low_confidence_fallback', 'confidence': heuristic_result['score'] }) else: result['title'] = None result['source'] = 'failed' return result # --- 以下为内部辅助方法 (简化版) --- def _get_metadata_title(self, pdf_path): # 实现同前文的 get_pdf_metadata_title pass def _is_valid_title(self, title): """简单验证标题是否有效(非空、非默认名、非过长过短)""" if not title: return False invalid_patterns = [r'^无标题$', r'^document\d*$', r'^未命名$', r'^scan\d*\.pdf$', r'^新建文件$'] if any(re.match(pattern, title.lower()) for pattern in invalid_patterns): return False if len(title) < 2 or len(title) > 300: return False return True def _heuristic_extract(self, pdf_path): # 实现同前文的 heuristic_extract_title,但返回包含分数和文本的字典 pass def _get_top_candidates_with_context(self, pdf_path, top_n=3): # 扩展启发式提取函数,返回前top_n个候选及其前后文 pass def _extract_first_page_text(self, pdf_path): # 使用pdfplumber提取第一页纯文本 pass def _llm_judge(self, candidates_with_context): # 实现同前文的 llm_judge_title pass def _llm_generate(self, text): # 实现同前文的 llm_generate_title pass # 使用示例 # extractor = PDFTitleExtractor(llm_client=llm_client) # title_info = extractor.extract_title("your_document.pdf") # print(f"提取结果: {title_info['title']} (来源: {title_info['source']}, 置信度: {title_info['confidence']:.2f})")

5.3 性能优化与错误处理

在生产环境中,还需要考虑:

  1. 异步处理:如果处理大量PDF,LLM调用是主要瓶颈。可以使用异步IO(如asyncioaiohttp)来并发调用API,或者将任务放入队列(如Celery)异步执行。
  2. 超时与重试:网络请求和LLM API调用必须设置超时,并实现指数退避的重试机制。
  3. 日志与监控:记录每一级策略的结果、置信度和来源。这对于后续分析错误案例、优化规则权重至关重要。
  4. 结果缓存:对于相同的PDF文件(通过哈希判断),可以缓存提取结果,避免重复计算。

6. 特殊PDF类型的处理与实战避坑指南

不是所有PDF都生而平等。以下是一些常见“刺头”及其应对策略:

6.1 扫描件PDF(图片型PDF)

这是最大的挑战。扫描件PDF内部是图像,没有可提取的文本。解决方案是OCR(光学字符识别)。

  • 工具pytesseract(Tesseract OCR的Python封装) +pdf2image(将PDF页面转为图像)。
  • 流程
    1. 使用pdf2image将PDF第一页转换为高分辨率图像。
    2. 使用pytesseract对图像进行OCR,获取文本。
    3. 将OCR得到的文本,送入前述的启发式规则引擎或LLM进行处理。
  • 注意:OCR质量受图像清晰度、对比度、语言设置影响巨大。需要预处理图像(如二值化、去噪)并正确配置Tesseract的语言包(如chi_sim+eng)。
from pdf2image import convert_from_path import pytesseract import cv2 import numpy as np def extract_title_from_scanned_pdf(pdf_path): """从扫描版PDF中提取标题(OCR方案)""" try: # 1. 转换第一页为图像 images = convert_from_path(pdf_path, first_page=1, last_page=1, dpi=300) if not images: return None pil_image = images[0] # 2. 可选的图像预处理(提高OCR精度) # 转换为OpenCV格式 (BGR) open_cv_image = np.array(pil_image) # 转换为灰度图 gray = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2GRAY) # 二值化 _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV) # 3. OCR # 配置Tesseract,假设中英文混合 custom_config = r'--oem 3 --psm 6 -l eng+chi_sim' ocr_text = pytesseract.image_to_string(thresh, config=custom_config) # 4. 从OCR文本中寻找标题(这里可以复用之前的启发式规则,但需要对行进行分割) lines = ocr_text.split('\n') # 简单的规则:取最前面非空、长度适中的几行作为候选 candidates = [line.strip() for line in lines if 10 < len(line.strip()) < 150] if candidates: # 这里可以调用启发式规则函数,但需要适配OCR文本没有坐标信息的特点 # 一个简单策略:返回第一行看起来像标题的文本 return candidates[0] if candidates else None return None except Exception as e: print(f"处理扫描PDF {pdf_path} 时出错: {e}") return None

6.2 复杂版式PDF(如杂志、宣传册)

这类PDF标题可能不是简单的顶部文字,而是与图片、艺术字混合。启发式规则容易失效。

  • 策略:更多地依赖LLM。将OCR或提取出的第一页主要文本(可能包含图片注释、栏目名等)交给LLM,并给出更详细的指令,如“忽略‘专栏’、‘本期导读’等字样,找出代表本期主题的标题”。
  • 预处理:可以尝试使用pdfplumberfilter功能,先过滤掉太小的字体(可能是正文)和太大的字体(可能是装饰性文字),保留中等大小的文本块作为候选。

6.3 加密或权限受限PDF

如果PDF有打开密码或复制限制,上述所有文本提取方法都会失败。

  • 有密码:如果知道密码,PyPDF2pdfplumber都支持在打开时传入password参数。
  • 无密码但禁止复制:这通常是通过将文本绘制为曲线或图像实现的,本质上变成了扫描件。只能走OCR路线,且效果可能更差。

重要避坑点:处理用户上传的PDF时,务必在try-except中包裹所有文件操作和库调用。PDF文件可能损坏、版本特殊、或包含恶意构造的内容,导致解析库崩溃。一个健壮的服务不能因为一个坏文件而整体瘫痪。

← 返回列表