让 RAG 读懂真实文件:从 TXT 到 PDF、PPT、HTML 的实战
从玩具到真实场景
上一篇跑通的 RAG 只处理了 6 行手写的纯文本。这只能证明流程能跑,证明不了它有用。真实场景的文档长这样:
- 财报 PDF,66 页,图文混排,有表格和目录
- 产品 PPT,每页一个 slide,文本框散落在不同位置
- 网页 HTML,内容被
<script>和<style>包围
要让 RAG 处理这些,核心动作只有一个:在向量化之前,先把文件内容抽成纯文本。之后的切块、向量化、检索、生成,全部复用上一篇的流程。
不同格式,不同解析器
| 格式 | 解析库 | 抽取方式 |
|---|---|---|
pypdf 或 pdfplumber | 逐页抽取文字层 | |
| PPT/PPTX | python-pptx | 遍历每页的文本框 |
| HTML | beautifulsoup4 | 剔除脚本样式后取文本 |
| TXT/MD | 内置 | 直接读取 |
安装依赖:
bash
cd /opt/rag-nvidia source venv/bin/activate pip install pypdf python-pptx beautifulsoup4
文件解析层
python
from pypdf import PdfReader
from pptx import Presentation
from bs4 import BeautifulSoup
from pathlib import Path
def parse_pdf(path):
reader = PdfReader(path)
return "\n".join(page.extract_text() or "" for page in reader.pages)
def parse_pptx(path):
prs = Presentation(path)
texts = []
for slide in prs.slides:
for shape in slide.shapes:
if shape.has_text_frame:
texts.append(shape.text_frame.text)
return "\n".join(texts)
def parse_html(path):
html = Path(path).read_text(encoding="utf-8", errors="ignore")
soup = BeautifulSoup(html, "html.parser")
for tag in soup(["script", "style"]):
tag.decompose()
return soup.get_text(separator="\n")
def parse_text(path):
return Path(path).read_text(encoding="utf-8", errors="ignore")
PARSERS = {
".pdf": parse_pdf,
".ppt": parse_pptx,
".pptx": parse_pptx,
".html": parse_html,
".htm": parse_html,
".txt": parse_text,
".md": parse_text,
}
def parse_file(path):
ext = Path(path).suffix.lower()
if ext not in PARSERS:
raise ValueError(f"不支持的文件类型:{ext}")
return PARSERS[ext](path)
文本切块:让检索更精准
真实的 PDF 抽出来的是一整段连续文本。如果直接整篇送进 Embedding,向量会丢失局部语义,检索准确率大幅下降。
解决方案是滑动窗口切块,并在相邻块之间保留一段重叠,避免切在句子中间时丢失上下文:
python
CHUNK_SIZE = 800
CHUNK_OVERLAP = 100
def chunk_text(text, chunk_size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
text = "\n".join(line.strip() for line in text.splitlines() if line.strip())
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
800/100 是经验值。段落长就调大,短句列表就调小。
实测:66 页财报 PDF
上传的是一份 66 页的财报 PDF(KOKOPELLI Inc. 2026 财年第一季度)。在运行完整的 RAG 脚本之前,先做一步快速检查——确认 PDF 有没有文字层:
bash
python3 -c "
from pypdf import PdfReader
r = PdfReader('data/koko.pdf')
print('pages:', len(r.pages))
for i, p in enumerate(r.pages[:3]):
t = p.extract_text() or ''
print(f'page {i+1}: {len(t)} chars, preview: {t[:80]!r}')
"
输出:
text
pages: 66 page 1: 146 chars, preview: 'Financial Results\nKOKOPELLI Inc. (stock code: 4167)...' page 2: 231 chars, preview: '01 Company & Business Overview\nP.3-15\n02 Highlights...' page 3: 30 chars, preview: '01\nCompany & Business Overview'
有文字层,可以直接处理。如果每页都是 0 字符,说明是扫描版图片 PDF,需要走 OCR 路径。
完整流程
bash
python3 rag.py data/koko.pdf
脚本会依次执行:
- 解析 PDF → 抽取全部文字
- 切块 → 生成数百个片段
- 向量化 → 调用 NVIDIA Embedding API
- 存入 Qdrant 本地数据库
- 进入交互式问答
踩坑记录
1. 扫描版 PDF 抽出空白pypdf 只能读 PDF 的文字层。扫描版 PDF 本质是图片,没有文字层。判断方法就是看每页的字符数是否为 0。是的话,需要先用 pytesseract + pdf2image 做 OCR,但速度慢、占内存,在资源紧张的 VPS 上要谨慎。
2. PDF 段落结构丢失pypdf 抽出来的文本会丢失原始布局,表格、代码块可能变成乱序文本。对排版要求高的场景,可以换成 pdfplumber,它对布局的保留更好:
python
import pdfplumber
def parse_pdf(path):
with pdfplumber.open(path) as pdf:
return "\n".join(page.extract_text() or "" for page in pdf.pages)
3. 大文档触发 API 限流
66 页 PDF 会产生几百个片段,每个片段一次 Embedding 调用。NVIDIA 免费层约 40 请求/分钟,大文档很容易触发 429。解决方案是加批处理 + 重试:
python
import time
def embed_texts(texts, input_type="passage", batch_size=10, max_retries=5):
all_vectors = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
for attempt in range(max_retries):
try:
resp = client.embeddings.create(
model=os.getenv("EMBEDDING_MODEL"),
input=batch,
extra_body={"input_type": input_type},
)
all_vectors.extend(d.embedding for d in resp.data)
break
except Exception as e:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt
print(f"重试 {attempt+1}/{max_retries},等待 {wait}s:{e}")
time.sleep(wait)
time.sleep(1) # 批次间暂停,避免触发限流
return all_vectors
4. PPT 里的图表和图片读不到python-pptx 只能读文本框内容。图表数据、图片里的文字无法抽取,需要另外处理。
5. HTML 里的动态内容读不到BeautifulSoup 只能解析静态 HTML。SPA 或需要 JavaScript 渲染的页面,要先用 playwright 或 selenium 渲染后再解析。
交互式问答
脚本索引完成后会进入问答循环:
text
输入问题开始问答(q 退出) 问题:
输入问题后会同时显示检索到的原始片段和LLM 生成的回答,方便对比验证检索质量。
适合测试这份财报的问题:
- FY2026 第一季度的营收是多少?
- 公司的主要业务是什么?
- 这份财报提到了哪些关键话题?
- KOKOPELLI 的股票代码是多少?
总结
从纯文本到真实文件,RAG 的复杂度主要增加在解析层。一旦文件被抽成纯文本,后面的切块、向量化、检索、生成,全部复用之前的逻辑。
关键原则:
- 先验证文字层,扫描版 PDF 直接换 OCR 路径
- 切块加重叠,避免语义断裂
- 大文档加批处理和重试,应对 API 限流
- 解析库按格式选,PDF 用
pypdf/pdfplumber,PPT 用python-pptx,HTML 用BeautifulSoup
这套方案的核心优势仍然是极简:不部署额外服务,不引入新的容器,所有逻辑在一个脚本里。后续要服务化,只需把 QdrantClient(path=...) 改成 QdrantClient(url=...),业务代码不用改。
