环境准备

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 快速模式 ⚡⚡⚡ ⭐⭐ 纯文本 PDF
hi_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_content elem.text
类型判断 doc.metadata["category"] == "Title" isinstance(elem, Title)
元数据访问 doc.metadata["parent_id"] elem.metadata.parent_id
类型检查 字符串比较 Python 类型检查(更安全)
用途 LangChain/RAG 集成 底层文档处理

parent_id 的生成机制:

Unstructured 库在解析 PDF 时会:

  1. 分析文档的层级结构
  2. 识别标题、段落、列表等元素
  3. 根据排版、字体大小、位置推断父子关系
  4. 为每个元素生成唯一的 element_id
  5. 为子元素设置 parent_id 指向父元素

示例结构:

[
    {
        "element_id": "abc123",
        "category": "Title",
        "content": "云冈石窟简介",
        "parent_id": None  # 顶级标题
    },
    {
        "element_id": "def456",
        "category": "NarrativeText",
        "content": "云冈石窟位于山西省...",
        "parent_id": "abc123"  # 属于上面的标题
    }
]

方案对比与选择

性能对比

方案 速度 内存 准确度 结构识别 OCR 支持
PyPDF ⚡⚡⚡ 💾 ⭐⭐
PyMuPDF ⚡⚡⚡ 💾 ⭐⭐⭐ 部分
Unstructured 💾💾💾 ⭐⭐⭐⭐

选择建议

使用 PyPDF:

  • ✅ 简单的纯文本 PDF
  • ✅ 需要快速提取
  • ✅ 按页分割即可

使用 PyMuPDF:

  • ✅ 需要提取图片、元数据
  • ✅ 需要精细控制
  • ✅ 处理大量 PDF
  • ✅ 性能要求高

使用 Unstructured:

  • ✅ 复杂布局的 PDF
  • ✅ 需要识别文档结构
  • ✅ 扫描件处理
  • ✅ 构建知识图谱
  • ✅ 需要父子关系

混合方案

实际项目中,可以结合多种方案:

def smart_pdf_loader(pdf_path):
    """智能选择 PDF 加载方案"""
    import pymupdf
    
    # 1. 先用 PyMuPDF 快速检查
    doc = pymupdf.open(pdf_path)
    page_count = len(doc)
    has_images = any(page.get_images() for page in doc)
    doc.close()
    
    # 2. 根据特征选择方案
    if page_count < 10 and not has_images:
        # 简单文档,用 PyPDF
        from langchain_community.document_loaders import PyPDFLoader
        loader = PyPDFLoader(pdf_path)
        return loader.load()
    elif has_images:
        # 有图片,用 Unstructured
        from langchain_unstructured import UnstructuredLoader
        loader = UnstructuredLoader(pdf_path, strategy="hi_res")
        return list(loader.lazy_load())
    else:
        # 默认用 PyMuPDF
        text = []
        doc = pymupdf.open(pdf_path)
        for page in doc:
            text.append(page.get_text())
        doc.close()
        return text

# 使用
docs = smart_pdf_loader("document.pdf")

常见问题

1. 中文 OCR 识别不准

# 安装中文语言包
# macOS
brew install tesseract-lang

# Ubuntu
sudo apt-get install tesseract-ocr-chi-sim

# 验证安装
tesseract --list-langs
# 应该能看到 chi_sim
# 使用时指定语言
loader = UnstructuredLoader(
    file_path="chinese.pdf",
    strategy="hi_res",
    languages=["chi_sim", "eng"]  # 中英文混合
)

2. Unstructured 解析速度慢

# 优化策略
loader = UnstructuredLoader(
    file_path="large.pdf",
    strategy="fast",  # 使用快速模式
    # 或者只处理部分页面
)

# 分批处理
def process_pdf_in_batches(pdf_path, batch_size=10):
    import pymupdf
    doc = pymupdf.open(pdf_path)
    total_pages = len(doc)
    
    for start in range(0, total_pages, batch_size):
        end = min(start + batch_size, total_pages)
        # 提取部分页面到临时文件
        # 然后用 Unstructured 处理
        pass

3. 内存占用过高

# 使用 lazy_load 延迟加载
loader = UnstructuredLoader(file_path="large.pdf")

for doc in loader.lazy_load():
    # 逐个处理,不一次性加载到内存
    process_document(doc)

4. 表格提取效果差

# 使用 PyMuPDF 的表格识别
import pymupdf

doc = pymupdf.open("document.pdf")
for page in doc:
    tables = page.find_tables()
    for table in tables:
        df = table.to_pandas()
        print(df)

# 或者使用专门的表格提取库
# pip install camelot-py pdfplumber

5. 扫描件识别不出来

# 确保使用 hi_res 策略
loader = UnstructuredLoader(
    file_path="scanned.pdf",
    strategy="hi_res",  # 必须使用高分辨率
    languages=["chi_sim"]
)

# 或者使用 ocr_only
loader = UnstructuredLoader(
    file_path="scanned.pdf",
    strategy="ocr_only"  # 纯 OCR 模式
)

进阶技巧

1. 批量处理 PDF

from pathlib import Path
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader

# 批量加载目录下的所有 PDF
loader = DirectoryLoader(
    "./documents/",
    glob="**/*.pdf",
    loader_cls=PyPDFLoader,
    show_progress=True
)

docs = loader.load()
print(f"共加载 {len(docs)} 个文档")

2. PDF 预处理

import pymupdf

def preprocess_pdf(input_path, output_path):
    """PDF 预处理:去除水印、调整对比度等"""
    doc = pymupdf.open(input_path)
    
    for page in doc:
        # 移除注释和水印
        page.clean_contents()
        
        # 可以添加更多预处理逻辑
    
    doc.save(output_path)
    doc.close()

# 使用
preprocess_pdf("original.pdf", "cleaned.pdf")

3. 构建 PDF 索引

from langchain_unstructured import UnstructuredLoader
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings

# 1. 加载 PDF
loader = UnstructuredLoader("document.pdf", strategy="hi_res")
docs = list(loader.lazy_load())

# 2. 创建向量索引
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = FAISS.from_documents(docs, embeddings)

# 3. 检索
query = "云冈石窟的历史"
results = vectorstore.similarity_search(query, k=3)

for doc in results:
    print(f"内容: {doc.page_content}")
    print(f"来源: {doc.metadata}")
    print("-" * 50)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐