一、Reader 抽象:统一的数据入口
LlamaIndex 里所有数据源读取器都继承 BaseReader,只需要实现一个方法:
from llama_index.core.readers.base import BaseReader
from llama_index.core import Document
class MyReader(BaseReader):
def load_data(self, *args, **kwargs) -> list[Document]:
return [Document(text="...", metadata={})]
不论数据源是本地文件、远程 API 还是数据库,最终都返回 list[Document]——后面的切块、嵌入、索引就和数据源脱钩了。这是 LlamaIndex 架构最漂亮的地方之一。
二、SimpleDirectoryReader:本地文件的瑞士军刀
最常用的入门 Reader,能识别一个文件夹里的各种后缀并分派给对应解析器:
from llama_index.core import SimpleDirectoryReader
docs = SimpleDirectoryReader(
input_dir="./data",
recursive=True, # 递归子目录
required_exts=[".pdf", ".md", ".txt"], # 白名单
exclude=["**/draft_*", "**/.DS_Store"], # glob 黑名单
exclude_hidden=True,
filename_as_id=True, # 用文件名做 doc_id,利于增量
num_files_limit=None,
file_metadata=lambda p: {"source_path": p}, # 自定义 metadata
).load_data()
filename_as_id=True 很重要:默认它会给每个文件生成随机 uuid,下次跑全变了,增量索引就废了。打开这个选项后 doc_id = 相对路径,稳定。
内置支持的文件类型
| 后缀 | 解析器 | 备注 |
|---|---|---|
.pdf | pypdf | 纯文字 OK,扫描件/复杂表格差,换 LlamaParse |
.docx | python-docx | 保留段落,表格转文本 |
.pptx | python-pptx | 提取每页文字 |
.xlsx / .csv | pandas | 每行一段,表头做上下文 |
.md / .txt | 内置 | 最干净,推荐 |
.html | BeautifulSoup | 正文提取 OK,但网站级建议用 WebReader |
.ipynb | nbformat | 合并 code + markdown |
.mp3 / .mp4 | whisper | 需装 llama-index-readers-file[audio] |
.jpg / .png | OCR/多模态 | 需配合 ImageReader 或多模态 LLM |
三、LlamaParse:复杂文档的杀手级服务
LlamaIndex 公司(LlamaIndex Inc.)主打的 SaaS,解决开源方案搞不定的三类 PDF:
- 表格多的财报、技术文档——pypdf 会把表格压成乱序文字
- 公式/化学结构——普通提取丢 LaTeX,LlamaParse 能转
$...$ - 扫描件/图文混排——自动 OCR + 版面识别
pip install llama-parse
export LLAMA_CLOUD_API_KEY=llx-... # cloud.llamaindex.ai 免费 1000 页/天
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
parser = LlamaParse(
result_type="markdown", # markdown | text
language="ch_sim", # 中英混合强烈建议设置
num_workers=4, # 并行处理多页
verbose=True,
premium_mode=False, # 扫描/复杂图表时开,贵一些但效果好
)
# 法 1:直接解析单个 PDF
docs = parser.load_data("./reports/Q3-2025.pdf")
# 法 2:配合 SimpleDirectoryReader,按后缀分派
file_extractor = {".pdf": parser}
docs = SimpleDirectoryReader(
"./data", file_extractor=file_extractor
).load_data()
实测心得:
• 财报/招股书之类表格重镇——LlamaParse premium_mode 几乎是唯一选择,pypdf + pdfplumber 都会把合并单元格搞乱
• 纯文本合同/小说——pypdf 够用,没必要花钱
• 先免费跑几个文档看 Markdown 输出,效果满意再规模化
• 财报/招股书之类表格重镇——LlamaParse premium_mode 几乎是唯一选择,pypdf + pdfplumber 都会把合并单元格搞乱
• 纯文本合同/小说——pypdf 够用,没必要花钱
• 先免费跑几个文档看 Markdown 输出,效果满意再规模化
LlamaParse 的参数调优
| 参数 | 含义 | 什么时候开 |
|---|---|---|
premium_mode | 用更强模型 + 版面识别 | 扫描件、手写件、复杂表格 |
parsing_instruction | 给解析器一句自然语言指令 | "保留所有数字不要翻译""把公式转 LaTeX" |
use_vendor_multimodal_model | 用 GPT-4o/Claude 做多模态解析 | 图表密集、图表里含关键数字 |
invalidate_cache | 强制重解析 | 文件变了但 LlamaParse 缓存命中 |
四、远程源 Reader:Notion/Slack/Confluence…
LlamaHub 300+ Reader 绝大部分是"去某个 SaaS 拉数据",核心三件套:
Notion
pip install llama-index-readers-notion
from llama_index.readers.notion import NotionPageReader
reader = NotionPageReader(integration_token="secret_xxx")
# 按 database 拉
docs = reader.load_data(database_ids=["db-id-1", "db-id-2"])
# 按单页拉
docs = reader.load_data(page_ids=["page-id"])
Slack
from llama_index.readers.slack import SlackReader
import os, datetime as dt
reader = SlackReader(
slack_token=os.environ["SLACK_BOT_TOKEN"],
earliest_date=dt.datetime(2025, 1, 1),
latest_date=dt.datetime(2025, 12, 31),
)
docs = reader.load_data(channel_ids=["C01234", "C05678"])
Confluence
from llama_index.readers.confluence import ConfluenceReader
reader = ConfluenceReader(
base_url="https://company.atlassian.net/wiki",
user_name="you@company.com",
api_token="ATATT...",
)
docs = reader.load_data(space_key="ENG", include_attachments=False)
通病:增量拉取——Notion/Slack/Confluence Reader 大多是全量拉。真实项目里几十万页 wiki 每次全量嵌入很贵。
解决方案:① 拉下来后用
解决方案:① 拉下来后用
doc.last_edited_time 做 metadata,后面用 DocstoreStrategy.UPSERTS 增量(Ch4);② 或者加个时间过滤的 cron,只拉最近 N 天。
五、Web Reader:爬网页和抓 URL
pip install llama-index-readers-web
from llama_index.readers.web import (
SimpleWebPageReader, # 直接下 HTML → 正文
TrafilaturaWebReader, # 更强的正文提取
BeautifulSoupWebReader, # 自定义 CSS selector
RssReader, # RSS 订阅
SitemapReader, # 按 sitemap.xml 批量爬
)
# 单页
docs = TrafilaturaWebReader().load_data([
"https://blog.example.com/post-1",
"https://blog.example.com/post-2",
])
# 批量:按 sitemap
docs = SitemapReader().load_data(
sitemap_url="https://docs.example.com/sitemap.xml",
filter=lambda url: "/api/" in url, # 只要 API 文档
)
选型建议:
Trafilatura是当前开源中正文提取最好的——博客/新闻/文档首选SimpleWebPageReader最简单,HTML 会保留样式噪声——不推荐- 动态渲染(Next/Nuxt 的 SPA)——用
PlaywrightWebReader或先走 Puppeteer/Playwright 拿 HTML 再喂 - 反爬严的站点——走代理、降并发、注意 robots.txt
六、数据库 Reader:SQLDatabaseReader 和 DatabaseReader
结构化数据别走 PDF 那一套——数据库一行一个 Document 最合适。
from llama_index.readers.database import DatabaseReader
reader = DatabaseReader(
scheme="postgresql+psycopg",
host="localhost", port=5432,
user="postgres", password="pw",
dbname="mydb",
)
docs = reader.load_data(
query="""
SELECT id, title, body, author, created_at
FROM articles
WHERE status = 'published'
"""
)
# 默认把所有列拼成 text,metadata 空——通常不是你想要的
更精细:自己拼 text 和 metadata
from sqlalchemy import create_engine, text as sql_text
from llama_index.core import Document
engine = create_engine("postgresql+psycopg://postgres:pw@localhost/mydb")
with engine.connect() as conn:
rows = conn.execute(sql_text("SELECT id, title, body, author FROM articles"))
docs = [
Document(
text=f"{r.title}\n\n{r.body}",
doc_id=f"article-{r.id}",
metadata={
"title": r.title,
"author": r.author,
"id": r.id,
},
excluded_embed_metadata_keys=["id"], # id 不该进 embedding
excluded_llm_metadata_keys=["id"],
)
for r in rows
]
手构 vs 自动:DatabaseReader 是 60 分方案,把 SQL 结果简单拼成字符串。生产里强烈建议手构——你能精确控制 title/body 的分隔,哪些字段进 metadata,哪些要隐藏。多写 5 行代码换来 10 分提升。
七、多模态:图片、音频、视频
图片 → ImageDocument
from llama_index.core.schema import ImageDocument
# 方式 A:本地路径 + base64
img_doc = ImageDocument(image_path="./chart.png", metadata={"source": "Q3-report"})
# 方式 B:配合多模态 LLM 先抽取文字描述,再作为普通 Document 入库
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4o")
description = llm.complete(
"描述这张图表的数据和趋势",
image_documents=[img_doc]
).text
音频 → WhisperReader
pip install llama-index-readers-assemblyai # 或 openai-whisper
from llama_index.readers.assemblyai import AssemblyAIAudioTranscriptReader
reader = AssemblyAIAudioTranscriptReader(
file_path="./meeting.mp3",
api_key="...",
)
docs = reader.load_data() # 转写 + 时间戳 + speaker 标签
八、自定义 Reader:继承 BaseReader
LlamaHub 没覆盖你的数据源?五分钟写一个:
from llama_index.core.readers.base import BaseReader
from llama_index.core import Document
import requests
class JiraReader(BaseReader):
def __init__(self, base_url: str, api_token: str):
self.base_url = base_url
self.api_token = api_token
def load_data(self, jql: str) -> list[Document]:
r = requests.get(
f"{self.base_url}/rest/api/3/search",
params={"jql": jql, "fields": "summary,description,status"},
headers={"Authorization": f"Bearer {self.api_token}"},
).json()
docs = []
for issue in r["issues"]:
fields = issue["fields"]
docs.append(Document(
text=f"{fields['summary']}\n\n{fields.get('description','')}",
doc_id=issue["key"],
metadata={
"key": issue["key"],
"status": fields["status"]["name"],
},
))
return docs
docs = JiraReader("https://company.atlassian.net", "token").load_data(
jql="project=ENG AND updated >= -7d"
)
几条写 Reader 的经验法则:
- doc_id 稳定——用业务主键(Jira key、article id、Notion page id),不要用 uuid
- text 是给人/LLM 看的——拼得自然,不要一堆 JSON key
- metadata 只放简单类型——dict 不嵌套 dict,list 扁平化
- 进度和失败要打印——几千条数据拉到一半 token 过期,你要知道从哪重试
九、去重、增量与 doc_id 策略
多次拉同一批数据源,怎么避免重复嵌入?核心靠 doc_id + hash:
- 稳定的 doc_id:同一份文档每次生成的 doc_id 必须一致(文件路径、数据库主键、Notion page id)
- hash 自动算:
text + metadata变了,hash 就变 - DocstoreStrategy.UPSERTS:IngestionPipeline 会拿新 hash 对比 Docstore,没变就跳过,变了就重嵌入
from llama_index.core.ingestion import IngestionPipeline, DocstoreStrategy
from llama_index.core.storage.docstore import SimpleDocumentStore
pipeline = IngestionPipeline(
transformations=[...],
docstore=SimpleDocumentStore.from_persist_dir("./docstore"),
docstore_strategy=DocstoreStrategy.UPSERTS, # 关键
vector_store=vector_store,
)
nodes = pipeline.run(documents=docs) # 只会重新嵌入有变化的文档
pipeline.persist("./docstore")
增量的真实收益:一个 50 万页的 Notion 知识库,全量嵌入可能要 8 小时 + 几十刀;每天只有 500 页变化,增量可以压缩到 5 分钟 + 几毛钱。doc_id + UPSERTS 就是这 100x 的差距。Ch4 会深入讲 Pipeline。
十、Reader 常见坑
- PDF 表格全乱:pypdf 默认按 x 坐标扫,合并单元格会错位。换 LlamaParse 或
pdfplumber+ 自写提取。 - 中文 PDF OCR 乱码:LlamaParse 记得设
language="ch_sim";开源方案用 PaddleOCR 替代 Tesseract。 - Excel 一个 sheet 一大坨字:
pandas默认把所有行拼一起——建议读进 DataFrame 后自己按业务拆成 Document。 - Markdown 丢标题层级:默认只提取文本。用
MarkdownNodeParser在切块阶段保留 h1/h2 层级 metadata(Ch4 讲)。 - 爬网页拿回一堆导航/广告:
SimpleWebPageReader不做正文提取,必须上Trafilatura。 - SaaS Reader 全量拉导致 rate limit:加 retry + 指数退避,或切换到 Webhook/增量 API。
- DatabaseReader 的默认 text 拼接丑:前面说过——手构 Document,别偷懒。
- doc_id 用 uuid:每次跑都变,增量永远失效。用业务主键。
- metadata 里放了大段原文:metadata 应该是关于文档的元信息,不是文档内容本身,否则 embedding 会翻倍噪声。
- 拉完不 persist docstore:下次跑增量比较不到老 hash,又全量重嵌入一次。
十一、选型速查
| 数据形态 | 首选方案 | 备注 |
|---|---|---|
| 本地混合文件夹 | SimpleDirectoryReader | filename_as_id=True |
| 复杂 PDF(表格/扫描/公式) | LlamaParse | premium_mode 按需 |
| Notion/Slack/Confluence | 对应 LlamaHub Reader | + 自己写增量层 |
| 博客/文档站 | TrafilaturaWebReader + SitemapReader | 控制并发 |
| SPA 动态渲染 | PlaywrightWebReader | 或外部先转 HTML |
| 关系数据库 | 手构 Document (SQLAlchemy) | 比 DatabaseReader 干净 |
| NoSQL(Mongo/Elastic) | LlamaHub 对应 Reader 或自写 | |
| S3/GCS/Azure Blob | ObjectStoreReader 系列 | 大量小文件场景 |
| 音视频 | Whisper/AssemblyAI | 转文字后走文本流程 |
| 图片图表 | ImageDocument + 多模态 LLM 描述 | Ch 多模态 RAG |
十二、本章小结
记住:
① Reader 只做一件事——把任何数据源变成
② 本地文件 SimpleDirectoryReader,复杂 PDF 直接上 LlamaParse,别在开源工具链上死磕表格。
③ SaaS Reader 大多全量拉——一定配合稳定
④ 结构化数据(数据库行/API)建议手构 Document——比 Reader 默认输出精细得多,也更利于 metadata 管控。
① Reader 只做一件事——把任何数据源变成
list[Document];继承 BaseReader 五分钟写一个。
② 本地文件 SimpleDirectoryReader,复杂 PDF 直接上 LlamaParse,别在开源工具链上死磕表格。
③ SaaS Reader 大多全量拉——一定配合稳定
doc_id + DocstoreStrategy.UPSERTS 做增量,不然成本爆炸。
④ 结构化数据(数据库行/API)建议手构 Document——比 Reader 默认输出精细得多,也更利于 metadata 管控。