LangChain 读取 PDF 文档
本文是 refine-rag 系列教程的第四篇,深入讲解如何使用多种方案处理 PDF 文档。
目录
- 前言
- 环境准备
- 方案一:PyPDF(轻量快速)
- 方案二:PyMuPDF(功能强大)
- 方案三:Unstructured(智能解析)
- 方案四:父子文档结构解析
- 方案对比与选择
- 常见问题
- 下一步学习
前言
PDF 是最常见的文档格式,但也是最难处理的格式之一。不同于纯文本,PDF 包含:
- 复杂的排版布局
- 多列文本
- 表格和图片
- 嵌入字体
- 扫描件(需要 OCR)
本文将介绍 4 种主流的 PDF 处理方案,从简单到复杂,从快速到智能,可根据自己的实际使用场景,选择合适的方案。
环境准备
1. 安装依赖包
- # 方案一:PyPDF(最轻量)
- pip install pypdf langchain-community
- # 方案二:PyMuPDF(功能强大)
- pip install pymupdf
- # 方案三:Unstructured(智能解析)
- pip install "unstructured[pdf]" langchain-unstructured
- # 中文 OCR 支持(可选)
- brew install tesseract-lang # macOS
- # 或
- sudo apt-get install tesseract-ocr-chi-sim # Ubuntu
- # 一键安装所有依赖
- pip install pypdf pymupdf "unstructured[pdf]" langchain-community langchain-unstructured
复制代码 2. 准备测试 PDF
准备一个测试 PDF 文件,可以是:
方案一:PyPDF(轻量快速)
最简单的 PDF 处理方案,适合纯文本提取。
文件名: 01-PyPDF读取.py- from langchain_community.document_loaders import PyPDFLoader
- # 加载 PDF 文件
- loader = PyPDFLoader("../99-doc-data/黑悟空/黑神话悟空.pdf")
- data = loader.load()
- # 遍历每一页
- for i, page in enumerate(data):
- print(f"=== 第 {i+1} 页 ===")
- print(page.page_content)
- print(f"元数据: {page.metadata}")
- print("-" * 50)
复制代码 输出结果:- [
- Document(
- page_content='黑神话:悟空\n\n游戏简介...',
- metadata={'source': '黑神话悟空.pdf', 'page': 0}
- ),
- Document(
- page_content='第一章 黑风山...',
- metadata={'source': '黑神话悟空.pdf', 'page': 1}
- )
- ]
复制代码 特点:
✅ 优势:
- 安装简单,无外部依赖
- 速度快,内存占用低
- 按页分割,结构清晰
- 适合纯文本 PDF
❌ 劣势:
- 不支持复杂布局
- 无法识别文档结构(标题、段落)
- 不支持扫描件 OCR
- 表格提取效果差
适用场景:
- 简单的文本 PDF
- 需要快速提取内容
- 对文档结构要求不高
方案二:PyMuPDF(功能强大)
更强大的 PDF 处理库,提供丰富的元数据和控制能力。
文件名: 02-PyMuPDF.py- import pymupdf
- # 打开 PDF 文件
- doc = pymupdf.open("../../99-doc-data/黑悟空/黑神话悟空.pdf")
- # 提取所有页面的文本
- text = [page.get_text() for page in doc]
- print(text)
- # 获取文档元数据
- print("=== PyMuPDF 基本信息提取 ===")
- print(f"文档页数: {len(doc)}")
- print(f"文档标题: {doc.metadata.get('title', 'N/A')}")
- print(f"文档作者: {doc.metadata.get('author', 'N/A')}")
- print(f"创建时间: {doc.metadata.get('creationDate', 'N/A')}")
- print(f"完整元数据: {doc.metadata}")
- # 遍历每一页,提取详细信息
- for page_num, page in enumerate(doc):
- print(f"\n--- 第 {page_num + 1} 页 ---")
- # 提取文本
- text = page.get_text()
- print(f"文本内容: {text[:200]}...") # 显示前 200 个字符
- # 提取图片
- images = page.get_images()
- print(f"图片数量: {len(images)}")
- # 获取页面链接
- links = page.get_links()
- print(f"链接数量: {len(links)}")
- # 获取页面尺寸
- width, height = page.rect.width, page.rect.height
- print(f"页面尺寸: {width:.2f} x {height:.2f}")
- doc.close()
复制代码 进阶功能:- import pymupdf
- doc = pymupdf.open("document.pdf")
- page = doc[0]
- # 1. 提取图片并保存
- for img_index, img in enumerate(page.get_images()):
- xref = img[0]
- base_image = doc.extract_image(xref)
- image_bytes = base_image["image"]
-
- # 保存图片
- with open(f"image_{img_index}.png", "wb") as f:
- f.write(image_bytes)
- # 2. 提取表格(需要额外处理)
- tables = page.find_tables()
- for table in tables:
- df = table.to_pandas()
- print(df)
- # 3. 搜索文本
- text_instances = page.search_for("关键词")
- for inst in text_instances:
- print(f"找到位置: {inst}")
- # 4. 提取带格式的文本
- blocks = page.get_text("dict")["blocks"]
- for block in blocks:
- if block["type"] == 0: # 文本块
- for line in block["lines"]:
- for span in line["spans"]:
- print(f"文字: {span['text']}, 字体: {span['font']}, 大小: {span['size']}")
- doc.close()
复制代码 特点:
✅ 优势:
- 速度快,性能优秀
- 丰富的元数据(作者、创建时间等)
- 可以提取图片、链接
- 支持表格识别
- 可以获取文本格式(字体、大小)
- 内存占用少
❌ 劣势:
- 不自动识别文档结构
- 需要手动处理布局分析
- 扫描件需要额外 OCR
适用场景:
- 需要提取图片和元数据
- 需要精细控制 PDF 处理
- 性能要求高
- 需要处理大量 PDF
方案三:Unstructured(智能解析)
最智能的 PDF 处理方案,自动识别文档结构。
文件名: 03-LangChain-Unstrucured-PDF.py- from langchain_unstructured import UnstructuredLoader
- # 中文 PDF
- loader = UnstructuredLoader(
- file_path="../99-doc-data/山西文旅/云冈石窟-ch.pdf",
- strategy="hi_res", # 高分辨率策略
- languages=["chi_sim"] # 简体中文 OCR
- )
- # 英文 PDF
- # loader = UnstructuredLoader(
- # file_path="../99-doc-data/山西文旅/云冈石窟-en.pdf",
- # strategy="hi_res"
- # )
- docs = []
- # lazy_load() 延迟加载,节省内存
- for doc in loader.lazy_load():
- docs.append(doc)
- # 查看解析结果
- for i, doc in enumerate(docs[:5]): # 只显示前 5 个
- print(f"\n=== 元素 {i+1} ===")
- print(f"类型: {doc.metadata.get('category')}")
- print(f"内容: {doc.page_content[:100]}...")
- print(f"页码: {doc.metadata.get('page_number')}")
- print(f"元素ID: {doc.metadata.get('element_id')}")
- print(f"父元素ID: {doc.metadata.get('parent_id')}")
复制代码 strategy 参数说明:
策略说明速度准确度适用场景fast快速模式⚡⚡⚡⭐⭐纯文本 PDFhi_res高分辨率⚡⭐⭐⭐⭐复杂布局、扫描件ocr_only仅 OCR⚡⚡⭐⭐⭐纯图片 PDF使用 partition 函数(更底层):
文件名: 04-Unstrctured-使用partition函数解析PDF-v1.py- from unstructured.partition.auto import partition
- filename = "../99-doc-data/黑悟空/黑神话悟空.pdf"
- # 使用 partition 函数解析 PDF
- elements = partition(
- filename=filename,
- content_type="application/pdf"
- )
- # 展示解析出的元素类型和内容
- print("PDF 解析后的 Elements 类型:")
- for i, element in enumerate(elements[:5]):
- print(f"\nElement {i+1}:")
- print(f"类型: {type(element).__name__}")
- print(f"内容: {str(element)[:100]}...")
- print("-" * 50)
- # 统计不同类型元素的数量
- element_types = {}
- for element in elements:
- element_type = type(element).__name__
- element_types[element_type] = element_types.get(element_type, 0) + 1
- print("\nElements 类型统计:")
- for element_type, count in element_types.items():
- print(f"{element_type}: {count} 个")
复制代码 输出示例:- Elements 类型统计:
- Title: 12 个
- NarrativeText: 45 个
- ListItem: 8 个
- Table: 3 个
- Image: 2 个
复制代码 特点:
✅ 优势:
- 自动识别文档结构(标题、段落、列表)
- 支持扫描件 OCR
- 可以识别表格
- 保留父子关系
- 适合复杂 PDF
❌ 劣势:
- 速度较慢
- 依赖外部工具(Tesseract)
- 内存占用较大
适用场景:
- 复杂布局的 PDF
- 需要保留文档结构
- 扫描件处理
- 构建知识图谱
方案四:父子文档结构解析
保留文档的层级关系,实现更精准的检索。
4.1 使用 LangChain 封装
文件名: 05-父子文档解析-Unstructured-LangChain.py- from langchain_unstructured import UnstructuredLoader
- file_path = '../99-doc-data/山西文旅/云冈石窟-en.pdf'
- # 加载 PDF
- loader = UnstructuredLoader(
- file_path=file_path,
- strategy="hi_res"
- )
- docs = []
- for doc in loader.lazy_load():
- docs.append(doc)
- # 仅筛选第一页的文档
- page_number = 1
- page_docs = [doc for doc in docs if doc.metadata.get("page_number") == page_number]
- # 打印每个元素的详细信息
- for i, doc in enumerate(page_docs, 1):
- print(f"Doc {i}:")
- print(f" 内容: {doc.page_content}")
- print(f" 分类: {doc.metadata.get('category')}")
- print(f" ID: {doc.metadata.get('element_id')}")
- print(f" Parent ID: {doc.metadata.get('parent_id')}")
- print("=" * 50)
- # 构建父子关系
- title_dict = {}
- # 收集 Title,建立 parent_id -> Title 的映射
- for doc in docs:
- if (doc.metadata.get("category") == "Title" and
- doc.metadata.get("page_number") == page_number):
- title_id = doc.metadata.get("element_id")
- title_text = doc.page_content.strip()
- if title_text not in [data["title"] for data in title_dict.values()]:
- title_dict[title_id] = {"title": title_text, "content": []}
- # 关联 Title 和其对应的 Text
- for doc in docs:
- if (doc.metadata.get("category") in ["NarrativeText", "Text"] and
- doc.metadata.get("page_number") == page_number):
- parent_id = doc.metadata.get("parent_id")
- if parent_id in title_dict:
- content = doc.page_content.strip()
- if content:
- title_dict[parent_id]["content"].append(content)
- # 输出结构化结果
- for title_data in title_dict.values():
- if title_data["content"]:
- print("\n=== " + title_data["title"] + " ===")
- for content in title_data["content"]:
- print(content)
- print()
复制代码 4.2 使用原生 Unstructured API
文件名: 06-父子文档-Unstructured-ParitionPDF.py- from unstructured.documents.elements import Title, NarrativeText, Text
- from unstructured.partition.pdf import partition_pdf
- file_path = '../99-doc-data/山西文旅/云冈石窟-en.pdf'
- # 使用 unstructured 直接读取 PDF
- elements = partition_pdf(
- filename=file_path,
- strategy="hi_res"
- )
- # 查看第一个元素的完整信息
- if elements:
- first_elem = elements[0]
- print("=== 第一个元素的详细信息 ===")
- print(f"类型: {type(first_elem)}")
- print(f"文本: {first_elem.text}")
- print(f"Metadata: {vars(first_elem.metadata)}")
- print("=" * 50)
- # 仅筛选第一页的元素
- page_number = 1
- page_elements = [
- elem for elem in elements
- if getattr(elem.metadata, "page_number", None) == page_number
- ]
- # 打印每个元素的详细信息
- for i, elem in enumerate(page_elements, 1):
- print(f"\nElement {i}:")
- print(f" 内容: {elem.text}")
- print(f" 分类: {type(elem).__name__}")
- print(f" ID: {getattr(elem, '_element_id', None)}")
- print("=" * 50)
- # 构建父子关系
- title_dict = {}
- # 收集 Title(使用类型检查)
- for elem in elements:
- if (isinstance(elem, Title) and
- getattr(elem.metadata, "page_number", None) == page_number):
- title_id = getattr(elem, '_element_id', None)
- title_text = elem.text.strip()
- if title_text not in [data["title"] for data in title_dict.values()]:
- title_dict[title_id] = {"title": title_text, "content": []}
- # 关联 Title 和其对应的 Text
- for elem in elements:
- if (isinstance(elem, (NarrativeText, Text)) and
- getattr(elem.metadata, "page_number", None) == page_number):
- parent_id = getattr(elem.metadata, "parent_id", None)
- if parent_id in title_dict:
- content = elem.text.strip()
- if content:
- title_dict[parent_id]["content"].append(content)
- # 输出结构化结果
- for title_data in title_dict.values():
- if title_data["content"]:
- print("\n=== " + title_data["title"] + " ===")
- for content in title_data["content"]:
- print(content)
- print()
复制代码 两种方式的区别:
特性LangChain 封装原生 Unstructured返回类型Document 对象Element 对象内容访问doc.page_contentelem.text类型判断doc.metadata["category"] == "Title"isinstance(elem, Title)元数据访问doc.metadata["parent_id"]elem.metadata.parent_id类型检查字符串比较Python 类型检查(更安全)用途LangChain/RAG 集成底层文档处理parent_id 的生成机制:
Unstructured 库在解析 PDF 时会:
- 分析文档的层级结构
- 识别标题、段落、列表等元素
- 根据排版、字体大小、位置推断父子关系
- 为每个元素生成唯一的 element_id
- 为子元素设置 parent_id 指向父元素
示例结构:- [
- {
- "element_id": "abc123",
- "category": "Title",
- "content": "云冈石窟简介",
- "parent_id": None # 顶级标题
- },
- {
- "element_id": "def456",
- "category": "NarrativeText",
- "content": "云冈石窟位于山西省...",
- "parent_id": "abc123" # 属于上面的标题
- }
- ]
复制代码 方案对比与选择
性能对比
[table][tr]方案速度内存准确度结构识别OCR 支持[/tr][tr][td]PyPDF[/td][td]⚡⚡⚡[/td][td]
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |