RAG实战:多格式文档处理完全指南

RAG实战:多格式文档处理完全指南

这是RAG系统最核心的实战问题!让我给你一个完整的、可直接使用的解决方案。


📚 一、文档格式处理全览

处理流程总览

原始文档 (PDF/Word/Excel/PPT)
    ↓
【步骤1】文档加载与解析
    ├─ PDF → pdfplumber (文字) + OCR (扫描版)
    ├─ Word → python-docx
    ├─ Excel → pandas
    └─ PPT → python-pptx
    ↓
【步骤2】内容分类提取
    ├─ 纯文本 → 直接提取
    ├─ 表格 → 结构化解析 + 文字描述
    ├─ 图片 → OCR + 多模态理解
    └─ 混合内容 → 分别处理后组合
    ↓
【步骤3】智能分块 (Chunking)
    ├─ 语义完整性优先
    ├─ 保留结构信息
    └─ 添加元数据标记
    ↓
【步骤4】向量化 (Embedding)
    ├─ 文本向量化
    ├─ 表格内容向量化
    └─ 多模态内容处理
    ↓
【步骤5】存储到向量数据库
    ├─ 向量 + 原始内容
    ├─ 元数据(来源、类型、页码)
    └─ 建立索引

🔧 二、统一文档处理器(实战代码)

核心处理类

import os
from pathlib import Path
from typing import List, Dict, Any
import logging

# 文档处理库
import pdfplumber
from docx import Document
import pandas as pd
from pptx import Presentation
from PIL import Image

# AI处理库
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document as LangChainDocument
import pytesseract  # OCR
from paddleocr import PaddleOCR  # 中文OCR更好

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class UniversalDocumentProcessor:
    """统一文档处理器 - 支持所有常见格式"""
    
    def __init__(self):
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=100,
            separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
        )
        # 初始化OCR(中文)
        self.ocr = PaddleOCR(use_angle_cls=True, lang='ch')
        
    def process_document(self, file_path: str) -> List[LangChainDocument]:
        """
        处理任意格式的文档
        返回:分块后的文档列表
        """
        file_path = Path(file_path)
        suffix = file_path.suffix.lower()
        
        logger.info(f"开始处理文档: {file_path.name}")
        
        # 根据文件类型调用不同的处理器
        processors = {
            '.pdf': self.process_pdf,
            '.docx': self.process_word,
            '.doc': self.process_word,
            '.xlsx': self.process_excel,
            '.xls': self.process_excel,
            '.pptx': self.process_ppt,
            '.txt': self.process_text,
            '.md': self.process_text,
        }
        
        if suffix not in processors:
            raise ValueError(f"不支持的文件格式: {suffix}")
        
        # 处理文档
        documents = processors[suffix](file_path)
        
        logger.info(f"处理完成: 生成 {len(documents)} 个文档块")
        return documents

📄 三、PDF处理(最复杂)

完整的PDF处理方案

    def process_pdf(self, file_path: Path) -> List[LangChainDocument]:
        """
        处理PDF - 支持文字版和扫描版
        """
        documents = []
        
        with pdfplumber.open(file_path) as pdf:
            for page_num, page in enumerate(pdf.pages, start=1):
                logger.info(f"处理PDF第 {page_num}/{len(pdf.pages)} 页")
                
                # 1. 提取文本
                text = page.extract_text()
                
                # 2. 提取表格
                tables = page.extract_tables()
                
                # 3. 检测是否有图片
                images = page.images
                
                # === 场景1:纯文本页面 ===
                if text and not tables and not images:
                    chunks = self._chunk_text(
                        text, 
                        metadata={
                            "source": file_path.name,
                            "page": page_num,
                            "type": "text"
                        }
                    )
                    documents.extend(chunks)
                
                # === 场景2:有表格的页面 ===
                elif tables:
                    # 文本部分
                    if text:
                        text_without_table = self._remove_table_from_text(text, tables)
                        if text_without_table.strip():
                            chunks = self._chunk_text(
                                text_without_table,
                                metadata={
                                    "source": file_path.name,
                                    "page": page_num,
                                    "type": "text"
                                }
                            )
                            documents.extend(chunks)
                    
                    # 表格部分(重点!)
                    for table_idx, table in enumerate(tables):
                        table_docs = self._process_table(
                            table,
                            metadata={
                                "source": file_path.name,
                                "page": page_num,
                                "type": "table",
                                "table_index": table_idx
                            }
                        )
                        documents.extend(table_docs)
                
                # === 场景3:扫描版(OCR)===
                elif not text and images:
                    logger.info(f"第{page_num}页是扫描版,使用OCR")
                    
                    # 转为图片
                    img = page.to_image(resolution=300)
                    pil_img = img.original
                    
                    # OCR识别
                    ocr_result = self.ocr.ocr(pil_img, cls=True)
                    ocr_text = '\n'.join([
                        line[1][0] for line in ocr_result[0]
                    ])
                    
                    if ocr_text.strip():
                        chunks = self._chunk_text(
                            ocr_text,
                            metadata={
                                "source": file_path.name,
                                "page": page_num,
                                "type": "text",
                                "ocr": True
                            }
                        )
                        documents.extend(chunks)
                
                # === 场景4:混合内容 ===
                else:
                    # 文本
                    if text:
                        chunks = self._chunk_text(text, {
                            "source": file_path.name,
                            "page": page_num,
                            "type": "mixed"
                        })
                        documents.extend(chunks)
                    
                    # 图片中的文字(OCR)
                    for img_idx, img in enumerate(images):
                        try:
                            img_text = self._extract_text_from_image(img)
                            if img_text:
                                documents.append(LangChainDocument(
                                    page_content=img_text,
                                    metadata={
                                        "source": file_path.name,
                                        "page": page_num,
                                        "type": "image_text",
                                        "image_index": img_idx
                                    }
                                ))
                        except Exception as e:
                            logger.warning(f"图片{img_idx}处理失败: {e}")
        
        return documents

📊 四、表格处理(核心难点)

为什么表格难处理?

问题:直接向量化表格,效果很差

示例表格:
| 科目 | 及格分数 | 优秀分数 |
|------|---------|---------|
| 语文 | 60      | 90      |
| 数学 | 60      | 90      |

直接向量化:"科目 及格分数 优秀分数 语文 60 90 数学 60 90"
→ 结构信息丢失,语义不清

学生提问:"语文多少分及格?"
→ 检索不到准确答案

表格处理的3种策略

    def _process_table(self, table: List[List], metadata: Dict) -> List[LangChainDocument]:
        """
        表格处理 - 3种策略组合
        """
        if not table or not table[0]:
            return []
        
        documents = []
        
        # === 策略1:结构化存储(保留原始格式)===
        df = pd.DataFrame(table[1:], columns=table[0])
        
        # 转为Markdown格式(更易向量化)
        markdown_table = df.to_markdown(index=False)
        
        documents.append(LangChainDocument(
            page_content=markdown_table,
            metadata={
                **metadata,
                "representation": "structured"
            }
        ))
        
        # === 策略2:生成自然语言描述 ⭐⭐⭐⭐⭐ ===
        # 这是最重要的!让表格可以被语义检索
        
        table_description = self._table_to_natural_language(df, table[0])
        
        documents.append(LangChainDocument(
            page_content=table_description,
            metadata={
                **metadata,
                "representation": "natural_language"
            }
        ))
        
        # === 策略3:逐行拆分(适合大表格)===
        if len(df) > 5:  # 大表格才拆分
            for idx, row in df.iterrows():
                row_text = self._row_to_text(row, table[0])
                documents.append(LangChainDocument(
                    page_content=row_text,
                    metadata={
                        **metadata,
                        "representation": "row",
                        "row_index": idx
                    }
                ))
        
        return documents
    
    def _table_to_natural_language(self, df: pd.DataFrame, headers: List) -> str:
        """
        表格转自然语言描述(关键技术!)
        """
        descriptions = []
        
        # 1. 表格整体描述
        descriptions.append(
            f"这是一个关于{headers[0]}的表格,包含{len(df)}行数据。"
        )
        
        # 2. 列的描述
        descriptions.append(f"表格有以下列:{', '.join(headers)}")
        
        # 3. 逐行转换为自然语言
        for idx, row in df.iterrows():
            # 根据列名生成语句
            # 例如:"语文的及格分数是60分,优秀分数是90分"
            
            sentences = []
            subject = row[headers[0]]  # 第一列通常是主语
            
            for col in headers[1:]:
                value = row[col]
                # 生成类似"语文的及格分数是60分"的句子
                sentences.append(f"{subject}的{col}是{value}")
            
            descriptions.append(";".join(sentences) + "。")
        
        # 4. 如果有数值,生成统计信息
        numeric_cols = df.select_dtypes(include=['number']).columns
        if len(numeric_cols) > 0:
            for col in numeric_cols:
                try:
                    descriptions.append(
                        f"{col}的范围是{df[col].min()}到{df[col].max()}"
                    )
                except:
                    pass
        
        return "\n".join(descriptions)
    
    def _row_to_text(self, row: pd.Series, headers: List) -> str:
        """单行转文本"""
        parts = []
        subject = row[headers[0]]
        
        for col in headers[1:]:
            parts.append(f"{col}:{row[col]}")
        
        return f"{subject},{', '.join(parts)}"

表格处理示例

# 输入表格:
"""
| 科目 | 及格分数 | 优秀分数 | 满分 |
|------|---------|---------|------|
| 语文 | 60      | 90      | 100  |
| 数学 | 60      | 90      | 100  |
| 英语 | 60      | 90      | 100  |
"""

# 策略1输出(Markdown):
"""
| 科目 | 及格分数 | 优秀分数 | 满分 |
|------|---------|---------|------|
| 语文 | 60      | 90      | 100  |
| 数学 | 60      | 90      | 100  |
| 英语 | 60      | 90      | 100  |
"""

# 策略2输出(自然语言)⭐ 最重要:
"""
这是一个关于科目的表格,包含3行数据。
表格有以下列:科目, 及格分数, 优秀分数, 满分

语文的及格分数是60;语文的优秀分数是90;语文的满分是100。
数学的及格分数是60;数学的优秀分数是90;数学的满分是100。
英语的及格分数是60;英语的优秀分数是90;英语的满分是100。

及格分数的范围是60到60
优秀分数的范围是90到90
满分的范围是100到100
"""

# 策略3输出(逐行):
"""
语文,及格分数:60, 优秀分数:90, 满分:100
数学,及格分数:60, 优秀分数:90, 满分:100
英语,及格分数:60, 优秀分数:90, 满分:100
"""

# 现在学生问:"语文多少分及格?"
# → 可以准确检索到"语文的及格分数是60"!✅

📝 五、Word文档处理

    def process_word(self, file_path: Path) -> List[LangChainDocument]:
        """
        处理Word文档
        """
        doc = Document(file_path)
        documents = []
        
        current_section = ""
        current_content = []
        
        for element in doc.element.body:
            # 处理段落
            if element.tag.endswith('p'):
                para = element
                text = ''.join(node.text for node in para.iter() if node.text)
                
                # 检测是否是标题
                if self._is_heading(para):
                    # 保存之前的内容
                    if current_content:
                        documents.extend(self._chunk_text(
                            '\n'.join(current_content),
                            metadata={
                                "source": file_path.name,
                                "section": current_section,
                                "type": "text"
                            }
                        ))
                    
                    # 开始新章节
                    current_section = text
                    current_content = []
                else:
                    current_content.append(text)
            
            # 处理表格
            elif element.tag.endswith('tbl'):
                # 保存之前的文本
                if current_content:
                    documents.extend(self._chunk_text(
                        '\n'.join(current_content),
                        metadata={
                            "source": file_path.name,
                            "section": current_section,
                            "type": "text"
                        }
                    ))
                    current_content = []
                
                # 提取表格
                table_data = self._extract_word_table(element)
                table_docs = self._process_table(
                    table_data,
                    metadata={
                        "source": file_path.name,
                        "section": current_section,
                        "type": "table"
                    }
                )
                documents.extend(table_docs)
        
        # 处理最后的内容
        if current_content:
            documents.extend(self._chunk_text(
                '\n'.join(current_content),
                metadata={
                    "source": file_path.name,
                    "section": current_section,
                    "type": "text"
                }
            ))
        
        return documents
    
    def _extract_word_table(self, table_element) -> List[List]:
        """从Word中提取表格数据"""
        from docx.table import Table
        table = Table(table_element, None)
        
        data = []
        for row in table.rows:
            row_data = [cell.text.strip() for cell in row.cells]
            data.append(row_data)
        
        return data

📈 六、Excel处理

    def process_excel(self, file_path: Path) -> List[LangChainDocument]:
        """
        处理Excel - 支持多Sheet
        """
        documents = []
        
        # 读取所有sheet
        excel_file = pd.ExcelFile(file_path)
        
        for sheet_name in excel_file.sheet_names:
            logger.info(f"处理Excel Sheet: {sheet_name}")
            
            df = pd.read_excel(file_path, sheet_name=sheet_name)
            
            # 跳过空sheet
            if df.empty:
                continue
            
            # === 策略1:整个Sheet作为一个表格 ===
            if len(df) <= 20:  # 小表格
                table_data = [df.columns.tolist()] + df.values.tolist()
                table_docs = self._process_table(
                    table_data,
                    metadata={
                        "source": file_path.name,
                        "sheet": sheet_name,
                        "type": "table"
                    }
                )
                documents.extend(table_docs)
            
            # === 策略2:大表格分块处理 ===
            else:
                # 每10行一组
                chunk_size = 10
                for i in range(0, len(df), chunk_size):
                    chunk_df = df.iloc[i:i+chunk_size]
                    table_data = [df.columns.tolist()] + chunk_df.values.tolist()
                    
                    table_docs = self._process_table(
                        table_data,
                        metadata={
                            "source": file_path.name,
                            "sheet": sheet_name,
                            "type": "table",
                            "chunk_start_row": i,
                            "chunk_end_row": min(i+chunk_size, len(df))
                        }
                    )
                    documents.extend(table_docs)
        
        return documents

🎨 七、图片和多模态内容处理

    def _extract_text_from_image(self, image) -> str:
        """
        从图片中提取文字(OCR)
        """
        try:
            # 使用PaddleOCR(中文效果更好)
            result = self.ocr.ocr(image, cls=True)
            if result and result[0]:
                text = '\n'.join([line[1][0] for line in result[0]])
                return text
        except Exception as e:
            logger.warning(f"OCR失败: {e}")
        
        return ""
    
    def process_image_with_multimodal(self, image_path: str) -> Dict:
        """
        使用多模态模型理解图片(可选,高级功能)
        
        适用场景:
        - 课本插图需要理解图意
        - 流程图、架构图等
        - 包含关键信息的图表
        """
        from transformers import Qwen2VLForConditionalGeneration
        
        model = Qwen2VLForConditionalGeneration.from_pretrained(
            "Qwen/Qwen2-VL-7B"
        )
        
        prompt = """
        请详细描述这张图片的内容,包括:
        1. 图片类型(插图/流程图/表格/公式等)
        2. 主要内容和信息
        3. 图中的文字
        4. 这张图想要表达的核心概念
        
        请用教育性的语言描述,便于学生理解。
        """
        
        response = model.generate(image_path, prompt)
        
        return {
            "description": response,
            "ocr_text": self._extract_text_from_image(image_path),
            "image_path": image_path
        }

✂️ 八、智能分块策略(Chunking)

为什么分块很重要?

❌ 糟糕的分块:
Chunk 1: "光合作用是植物利用光能,将二氧化碳和"
Chunk 2: "水转化为有机物的过程。这个过程需要三个"
→ 语义被破坏,检索效果差

✅ 好的分块:
Chunk 1: "光合作用是植物利用光能,将二氧化碳和水转化为有机物的过程。"
Chunk 2: "光合作用需要三个条件:光照、叶绿素和原料(二氧化碳和水)。"
→ 语义完整,检索准确

智能分块实现

    def _chunk_text(self, text: str, metadata: Dict) -> List[LangChainDocument]:
        """
        智能分块 - 保持语义完整性
        """
        # 基础分块
        chunks = self.text_splitter.split_text(text)
        
        # 增强元数据
        documents = []
        for i, chunk in enumerate(chunks):
            # 提取关键信息
            keywords = self._extract_keywords(chunk)
            
            enhanced_metadata = {
                **metadata,
                "chunk_index": i,
                "total_chunks": len(chunks),
                "keywords": keywords,
                "char_count": len(chunk),
                "has_formula": self._contains_formula(chunk),
                "has_number": self._contains_number(chunk),
            }
            
            documents.append(LangChainDocument(
                page_content=chunk,
                metadata=enhanced_metadata
            ))
        
        return documents
    
    def _extract_keywords(self, text: str, top_k: int = 5) -> List[str]:
        """提取关键词"""
        import jieba.analyse
        
        keywords = jieba.analyse.extract_tags(
            text, 
            topK=top_k, 
            withWeight=False
        )
        return keywords
    
    def _contains_formula(self, text: str) -> bool:
        """检测是否包含公式"""
        # 简单检测:包含化学符号或数学符号
        formula_indicators = ['=', '→', '↓', '↑', '₂', '²', '³', '+', '-', '×', '÷']
        return any(indicator in text for indicator in formula_indicators)
    
    def _contains_number(self, text: str) -> bool:
        """检测是否包含数字"""
        import re
        return bool(re.search(r'\d', text))

🗄️ 九、向量化与存储

完整的向量化流程

from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

class VectorStoreManager:
    """向量数据库管理器"""
    
    def __init__(self, persist_directory: str = "./chroma_db"):
        # 使用BGE中文嵌入模型
        self.embeddings = HuggingFaceEmbeddings(
            model_name="BAAI/bge-large-zh-v1.5",
            model_kwargs={'device': 'cuda'},  # 或 'cpu'
            encode_kwargs={'normalize_embeddings': True}
        )
        
        self.vectorstore = Chroma(
            persist_directory=persist_directory,
            embedding_function=self.embeddings
        )
    
    def add_documents(self, documents: List[LangChainDocument], batch_size: int = 100):
        """
        批量添加文档到向量数据库
        """
        total = len(documents)
        logger.info(f"开始向量化 {total} 个文档")
        
        for i in range(0, total, batch_size):
            batch = documents[i:i+batch_size]
            
            # 向量化并存储
            self.vectorstore.add_documents(batch)
            
            logger.info(f"已处理 {min(i+batch_size, total)}/{total}")
        
        # 持久化
        self.vectorstore.persist()
        logger.info("向量化完成")
    
    def search(self, query: str, k: int = 3, filter_dict: Dict = None):
        """
        检索相关文档
        """
        if filter_dict:
            # 带过滤的检索
            results = self.vectorstore.similarity_search(
                query, 
                k=k,
                filter=filter_dict
            )
        else:
            results = self.vectorstore.similarity_search(query, k=k)
        
        return results

🚀 十、完整的使用示例

实战代码

def main():
    """完整的文档处理流程"""
    
    # 1. 初始化处理器
    processor = UniversalDocumentProcessor()
    vector_manager = VectorStoreManager()
    
    # 2. 处理文档目录
    document_folder = Path("./textbooks")
    all_documents = []
    
    for file_path in document_folder.glob("*"):
        if file_path.suffix.lower() in ['.pdf', '.docx', '.xlsx', '.pptx']:
            try:
                logger.info(f"\n{'='*60}")
                logger.info(f"处理文件: {file_path.name}")
                logger.info(f"{'='*60}")
                
                # 处理文档
                docs = processor.process_document(file_path)
                all_documents.extend(docs)
                
                logger.info(f"✅ {file_path.name} 处理完成,生成 {len(docs)} 个文档块")
                
            except Exception as e:
                logger.error(f"❌ 处理失败: {file_path.name}, 错误: {e}")
                continue
    
    # 3. 向量化并存储
    logger.info(f"\n{'='*60}")
    logger.info(f"开始向量化,总文档数: {len(all_documents)}")
    logger.info(f"{'='*60}")
    
    vector_manager.add_documents(all_documents)
    
    # 4. 测试检索
    logger.info(f"\n{'='*60}")
    logger.info("测试检索功能")
    logger.info(f"{'='*60}")
    
    test_queries = [
        "什么是光合作用?",
        "语文多少分及格?",
        "细胞的结构有哪些?"
    ]
    
    for query in test_queries:
        logger.info(f"\n问题: {query}")
        results = vector_manager.search(query, k=2)
        
        for i, doc in enumerate(results, 1):
            logger.info(f"  结果{i}:")
            logger.info(f"    内容: {doc.page_content[:100]}...")
            logger.info(f"    来源: {doc.metadata.get('source')}")
            logger.info(f"    类型: {doc.metadata.get('type')}")
    
    logger.info("\n✅ 全部完成!")


if __name__ == "__main__":
    main()

运行结果示例

============================================================
处理文件: 初中生物九年级.pdf
============================================================
INFO:__main__:处理PDF第 1/120 页
INFO:__main__:处理PDF第 2/120 页
...
INFO:__main__:发现表格,使用表格处理策略
INFO:__main__:生成自然语言描述:这是一个关于科目的表格...
✅ 初中生物九年级.pdf 处理完成,生成 458 个文档块

============================================================
开始向量化,总文档数: 1234
============================================================
INFO:__main__:已处理 100/1234
INFO:__main__:已处理 200/1234
...
INFO:__main__:向量化完成

============================================================
测试检索功能
============================================================

问题: 语文多少分及格?
  结果1:
    内容: 这是一个关于科目的表格,包含3行数据。
          语文的及格分数是60;语文的优秀分数是90...
    来源: 成绩标准.xlsx
    类型: table
  
  结果2:
    内容: 语文,及格分数:60, 优秀分数:90, 满分:100
    来源: 成绩标准.xlsx
    类型: table

✅ 全部完成!

📋 十一、最佳实践检查清单

文档处理清单

□ PDF处理
  ✓ 支持文字版PDF提取
  ✓ 支持扫描版OCR识别
  ✓ 表格单独处理
  ✓ 图片文字提取

□ Word处理
  ✓ 段落提取
  ✓ 表格提取
  ✓ 标题识别
  ✓ 保留章节结构

□ Excel处理
  ✓ 多Sheet支持
  ✓ 表格转自然语言
  ✓ 大表格分块
  ✓ 数值统计

□ 通用处理
  ✓ 元数据完整
  ✓ 关键词提取
  ✓ 公式识别
  ✓ 图片描述

性能优化建议

# 1. 批量处理
batch_size = 100  # 一次处理100个文档块

# 2. 并行处理
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=4) as executor:
    results = executor.map(processor.process_document, file_list)

# 3. 缓存重复计算
@lru_cache(maxsize=1000)
def extract_keywords(text):
    # 缓存关键词提取结果
    pass

# 4. 增量更新
# 只处理新文档或修改过的文档
def process_incremental(files, db):
    for file in files:
        if file.mtime > db.get_last_update(file):
            # 只处理修改过的
            process_document(file)

🎯 十二、常见问题解决

Q1: 表格检索不准确?

# 解决方案:多表示策略
# 同时存储3种形式

# 1. 原始Markdown(精确匹配)
# 2. 自然语言(语义检索)⭐
# 3. 逐行拆分(细粒度检索)

Q2: 公式向量化效果差?

# 解决方案:公式 + 文字描述

def process_formula(formula):
    return {
        "original": "6CO₂ + 6H₂O → C₆H₁₂O₆ + 6O₂",
        "description": "六分子二氧化碳加六分子水生成一分子葡萄糖和六分子氧气",
        "keywords": ["二氧化碳", "水", "葡萄糖", "氧气"]
    }

Q3: 图片信息丢失?

# 解决方案:OCR + 多模态模型 + 人工标注

# 自动:OCR提取文字
# 自动:多模态模型生成描述
# 人工:关键图片手动标注(最准确)

Q4: 大文件处理慢?

# 解决方案:
# 1. 流式处理(不要一次加载全部)
# 2. 并行处理
# 3. GPU加速向量化
# 4. 分批存储

posted @ 2026-01-16 17:24  XiaoZhengTou  阅读(2)  评论(0)    收藏  举报