Chapter 03

数据连接器 · LlamaHub 300+ 来源

Document 构造搞懂了,下一个问题就是数据从哪来。PDF、Word、Notion、Slack、数据库、网页……LlamaIndex 的 Reader 就是把这些异构来源都变成统一的 Document 列表。这一章把主流 Reader 的用法和坑过一遍。

一、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 = 相对路径,稳定。

内置支持的文件类型

后缀解析器备注
.pdfpypdf纯文字 OK,扫描件/复杂表格差,换 LlamaParse
.docxpython-docx保留段落,表格转文本
.pptxpython-pptx提取每页文字
.xlsx / .csvpandas每行一段,表头做上下文
.md / .txt内置最干净,推荐
.htmlBeautifulSoup正文提取 OK,但网站级建议用 WebReader
.ipynbnbformat合并 code + markdown
.mp3 / .mp4whisper需装 llama-index-readers-file[audio]
.jpg / .pngOCR/多模态需配合 ImageReader 或多模态 LLM

三、LlamaParse:复杂文档的杀手级服务

LlamaIndex 公司(LlamaIndex Inc.)主打的 SaaS,解决开源方案搞不定的三类 PDF:

  1. 表格多的财报、技术文档——pypdf 会把表格压成乱序文字
  2. 公式/化学结构——普通提取丢 LaTeX,LlamaParse 能转 $...$
  3. 扫描件/图文混排——自动 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用更强模型 + 版面识别扫描件、手写件、复杂表格
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 文档
)

选型建议:

六、数据库 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 的经验法则:

  1. doc_id 稳定——用业务主键(Jira key、article id、Notion page id),不要用 uuid
  2. text 是给人/LLM 看的——拼得自然,不要一堆 JSON key
  3. metadata 只放简单类型——dict 不嵌套 dict,list 扁平化
  4. 进度和失败要打印——几千条数据拉到一半 token 过期,你要知道从哪重试

九、去重、增量与 doc_id 策略

多次拉同一批数据源,怎么避免重复嵌入?核心靠 doc_id + hash:

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 常见坑

  1. PDF 表格全乱:pypdf 默认按 x 坐标扫,合并单元格会错位。换 LlamaParse 或 pdfplumber + 自写提取。
  2. 中文 PDF OCR 乱码:LlamaParse 记得设 language="ch_sim";开源方案用 PaddleOCR 替代 Tesseract。
  3. Excel 一个 sheet 一大坨字:pandas 默认把所有行拼一起——建议读进 DataFrame 后自己按业务拆成 Document。
  4. Markdown 丢标题层级:默认只提取文本。用 MarkdownNodeParser 在切块阶段保留 h1/h2 层级 metadata(Ch4 讲)。
  5. 爬网页拿回一堆导航/广告:SimpleWebPageReader 不做正文提取,必须上 Trafilatura
  6. SaaS Reader 全量拉导致 rate limit:加 retry + 指数退避,或切换到 Webhook/增量 API。
  7. DatabaseReader 的默认 text 拼接丑:前面说过——手构 Document,别偷懒。
  8. doc_id 用 uuid:每次跑都变,增量永远失效。用业务主键。
  9. metadata 里放了大段原文:metadata 应该是关于文档的元信息,不是文档内容本身,否则 embedding 会翻倍噪声。
  10. 拉完不 persist docstore:下次跑增量比较不到老 hash,又全量重嵌入一次。

十一、选型速查

数据形态首选方案备注
本地混合文件夹SimpleDirectoryReaderfilename_as_id=True
复杂 PDF(表格/扫描/公式)LlamaParsepremium_mode 按需
Notion/Slack/Confluence对应 LlamaHub Reader+ 自己写增量层
博客/文档站TrafilaturaWebReader + SitemapReader控制并发
SPA 动态渲染PlaywrightWebReader或外部先转 HTML
关系数据库手构 Document (SQLAlchemy)比 DatabaseReader 干净
NoSQL(Mongo/Elastic)LlamaHub 对应 Reader 或自写
S3/GCS/Azure BlobObjectStoreReader 系列大量小文件场景
音视频Whisper/AssemblyAI转文字后走文本流程
图片图表ImageDocument + 多模态 LLM 描述Ch 多模态 RAG

十二、本章小结

记住:
① Reader 只做一件事——把任何数据源变成 list[Document];继承 BaseReader 五分钟写一个。
② 本地文件 SimpleDirectoryReader,复杂 PDF 直接上 LlamaParse,别在开源工具链上死磕表格。
③ SaaS Reader 大多全量拉——一定配合稳定 doc_id + DocstoreStrategy.UPSERTS 做增量,不然成本爆炸。
④ 结构化数据(数据库行/API)建议手构 Document——比 Reader 默认输出精细得多,也更利于 metadata 管控。