Spring AI 文档处理全指南:从 PDF 到 Markdown,一文掌握所有 DocumentReader
上一期我们搭好了向量数据库,但数据还躺在各种文件里。今天我们就来系统学习 Spring AI 的 ETL 管道,彻底搞定文档的“读”与“拆”。
在《Spring AI 向量数据库实战:给大模型装上“语义搜索引擎”》中,我们用 SimpleVectorStore 和 PGVector 完成了数据的存储与检索。但有一个前置步骤被我们刻意跳过了:
数据是怎么从 PDF、Word、HTML、JSON 这些杂七杂八的格式,变成向量库能“消化”的标准 Document 对象的?
答案就是 ETL 管道(Extract, Transform, Load)。今天这篇文章,我们就聚焦 ETL 的第一个环节——DocumentReader,把 Spring AI 支持的所有文档读取器一网打尽。
什么是 ETL 管道?
在 RAG(检索增强生成)应用场景中,数据处理流程遵循经典的 ETL 模型:
|
|
|
|
|---|---|---|
| Extract(提取) | DocumentReader |
|
| Transform(转换) | DocumentTransformer |
|
| Load(加载) | DocumentWriter |
|
Spring AI 为这三个环节提供了统一的接口抽象:
-
DocumentReader实现Supplier<List<Document>> -
DocumentTransformer实现Function<List<Document>, List<Document>> -
DocumentWriter实现Consumer<List<Document>>
这种函数式设计让你可以像搭积木一样,把 reader → transformer → writer 链式组合起来。
今天我们先吃透 DocumentReader。
JSON 文档读取:JsonReader
很多企业的数据是以 JSON 格式存储的,比如产品目录、API 返回结果。JsonReader 能帮你精准提取 JSON 中的关键字段。
1. 示例数据
[ {"id": "bike-001","name": "山地探险者 X1","brand": "拓路者","price": 1299.99,"description": "一款坚固的全避震山地自行车,专为越野小径和复杂地形下坡而设计。","content": "车架:铝合金材质,29英寸轮组。前避震行程140毫米,后避震行程130毫米。配备液压碟刹系统。" }, {"id": "bike-002","name": "城市通勤专家版","brand": "优行","price": 899.50,"description": "轻量化混合动力自行车,非常适合日常通勤和城市休闲骑行。","content": "车架:铬钼钢材质,700c轮径。禧玛诺21速变速系统,搭配防刺穿轮胎。" }, {"id": "bike-003","name": "公路竞速 3000","brand": "速行者","price": 2450.00,"description": "高性能碳纤维公路自行车,适用于竞技比赛和快速团队骑行。","content": "车架:碳纤维一体成型。禧玛诺 Ultegra 套件,气动碳纤维轮组,22速变速。" }]
2. 代码实现
import org.springframework.ai.document.Document;import org.springframework.ai.reader.JsonReader;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyJsonReader{// 注意此处resource是org.springframework.core.io包下的Resource类型,privatefinal Resource resource; MyJsonReader(@Value("classpath:bikes.json") Resource resource) { // 3. 构造函数注入资源this.resource = resource; }/** * 加载 JSON 并转换为文档 * @return */public List<Document> loadJsonAsDocuments(){// JsonReader 是 Spring AI 提供的工具类// - 第一个参数:要读取的资源// - 后续参数:指定 JSON 对象中哪些字段拼接为文档的“内容”(content) JsonReader jsonReader = new JsonReader(this.resource, "description", "content");// 返回 Document 列表 System.out.println(jsonReader.get("/0"));return jsonReader.get(); }}
3. 进阶技巧
-
JSON Pointer 定位:如果 JSON 层级很深,可以用
get(String pointer)只读取特定部分。jsonReader.get("/0") -
自定义元数据生成器:通过
JsonMetadataGenerator将 JSON 中的其他字段(如price、brand)存入Document的元数据,方便后续过滤。
纯文本读取:TextReader
最简单也最常用。适合处理 .txt、.log 等纯文本文件。
import org.springframework.ai.document.Document;import org.springframework.ai.reader.TextReader;import org.springframework.ai.transformer.splitter.TokenTextSplitter;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyTextReader{privatefinal Resource resource; MyTextReader(@Value("classpath:text-source.txt") Resource resource) {this.resource = resource; }public List<Document> loadText(){// 创建文本读取器 TextReader textReader = new TextReader(this.resource);// 向文档的自定义元数据中添加一个键值对 textReader.getCustomMetadata().put("filename", "text.txt");// 执行读取,返回 Document 列表(默认会将整个文件内容作为一个 Document) List<Document> documents = textReader.read();// 对 Document 列表进行分词 List<Document> splitDocuments = new TokenTextSplitter().apply(documents);return splitDocuments; }}
注意:
TextReader会把整个文件读成一个Document。如果文件很大,需要配合TokenTextSplitter进行切分(我们下一篇文章会详细讲)。
HTML 文档读取:JsoupDocumentReader
网页抓取是 RAG 的重要数据来源。Spring AI 基于 Jsoup 提供了强大的 HTML 解析能力。
引入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-jsoup-document-reader</artifactId></dependency>
示例 HTML:
<article><h2>什么是 Spring AI?</h2><p>Spring AI 是一个用于简化 AI 应用开发的框架...</p><p>了解更多信息,请访问 <ahref="https://spring.io">Spring AI 官网</a>。</p></article>
代码实现:
import org.springframework.ai.document.Document;import org.springframework.ai.reader.jsoup.JsoupDocumentReader;import org.springframework.ai.reader.jsoup.config.JsoupDocumentReaderConfig;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyHtmlReader{privatefinal Resource resource; MyHtmlReader(@Value("classpath:/test.html") Resource resource) {this.resource = resource; }public List<Document> loadHtml(){// 构建 HTML 解析配置 JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()// 只提取 <article> 标签内的 <p> 段落 .selector("article p")// 指定文件编码(这里使用 UTF-8) .charset("UTF-8")// 在元数据中包含页面内的所有链接 URL .includeLinkUrls(true)// 提取 HTML <meta> 中的 author 和 date .metadataTags(List.of("author", "date"))// 添加自定义元数据 .additionalMetadata("source", "my-page.html") .build();// 创建读取器并读取 JsoupDocumentReader reader = new JsoupDocumentReader(this.resource, config);return reader.get(); }}
关键配置项解读
|
|
|
|---|---|
selector |
|
groupByElement |
Document |
allElements |
<body> 文本 |
includeLinkUrls |
|
Markdown 文档读取:MarkdownDocumentReader
技术文档、README 通常都是 Markdown 格式。Spring AI 能智能识别标题、代码块、引用块,并可按需拆分。
引入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-markdown-document-reader</artifactId></dependency>
代码实现:
import org.springframework.ai.document.Document;import org.springframework.ai.reader.markdown.MarkdownDocumentReader;import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyMarkdownReader{privatefinal Resource resource;// 构造函数注入,加载类路径下的 code.md 文件 MyMarkdownReader(@Value("classpath:test.md") Resource resource) {this.resource = resource; }public List<Document> loadMarkdown(){// 构建 Markdown 解析配置 MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()// 遇到水平分割线(---)时,分割为新文档 .withHorizontalRuleCreateDocument(true)// 不将代码块内容纳入文档内容 .withIncludeCodeBlock(false)// 不将引用块内容纳入文档内容 .withIncludeBlockquote(false)// 为每个文档添加自定义元数据 .withAdditionalMetadata("filename", "code.md") .build();// 创建读取器并读取 MarkdownDocumentReader reader = new MarkdownDocumentReader(this.resource, config);return reader.get(); }}
独特能力
-
Front Matter 自动转为元数据:Markdown 头部的 YAML 信息(如 title、date)会自动存入Document的元数据。 -
按标题层级拆分:未来版本可能支持按 #、##标题切分。
PDF 文档读取:两种模式任选
PDF 是企业文档的“重灾区”,Spring AI 提供了两种精细度的读取器。
1. 按页读取:PagePdfDocumentReader
每页生成一个 Document,适合页数不多、每页内容独立的场景。
引入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId></dependency>
代码实现:
import org.springframework.ai.document.Document;import org.springframework.ai.reader.ExtractedTextFormatter;import org.springframework.ai.reader.pdf.PagePdfDocumentReader;import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyPagePdfDocumentReader{public List<Document> getDocsFromPdf(){//创建 PDF 读取器 PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(// 指定类路径下的 PDF 文件"classpath:/智力题.pdf",// 构建配置对象 PdfDocumentReaderConfig.builder()// 设置页面顶部边距裁剪 .withPageTopMargin(0) .withPageExtractedTextFormatter( ExtractedTextFormatter.builder()// 删除提取文本的前 N 行 .withNumberOfTopTextLinesToDelete(0) .build() )// 每个 Document 包含的 PDF 页数 .withPagesPerDocument(1) .build() );// 执行读取,返回 Document 列表return pdfReader.read(); }}
2. 按段落读取:ParagraphPdfDocumentReader
利用 PDF 内嵌的目录(Catalog)信息,按章节/段落切分。注意:并非所有 PDF 都包含目录结构。
import org.springframework.ai.document.Document;import org.springframework.ai.reader.ExtractedTextFormatter;import org.springframework.ai.reader.pdf.PagePdfDocumentReader;import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;import org.springframework.stereotype.Component;import java.util.List;@ComponentpublicclassMyPagePdfDocumentReader{public List<Document> getDocsFromPdfWithCatalog(){// 创建段落级 PDF 读取器 ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(// 类路径下的 PDF 文件"classpath:/智力题.pdf", PdfDocumentReaderConfig.builder()// 不裁剪页面顶部边距 .withPageTopMargin(0) .withPageExtractedTextFormatter( ExtractedTextFormatter.builder()// 不删除提取文本的前 N 行 .withNumberOfTopTextLinesToDelete(0) .build() )// 每个逻辑单位处理 1 页 .withPagesPerDocument(1) .build() );return pdfReader.read(); }}
万能读取器:TikaDocumentReader
如果你不想为每种格式单独引入依赖,或者需要处理 Word (.docx)、PPT (.pptx)、Excel (.xlsx) 等办公文档,TikaDocumentReader 是你的最佳选择。它基于 Apache Tika,支持超过 1000 种文件格式。
引入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-tika-document-reader</artifactId></dependency>
代码实现:
@ComponentpublicclassMyTikaDocumentReader{privatefinal Resource resource; MyTikaDocumentReader(@Value("classpath:/孩子为什么越来越难教.docx") Resource resource) {this.resource = resource; }public List<Document> loadText(){ TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(this.resource);// 一行代码搞定 Word、PDF、PPT、HTML...return tikaDocumentReader.read(); }}
格式支持一览
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
总结与最佳实践
今天我们系统学习了 Spring AI 提供的 7 种 DocumentReader,帮你打通了从原始文件到标准 Document 对象的“最后一公里”。
快速选型指南
|
|
|
|
|---|---|---|
|
|
JsonReader |
|
|
|
TextReader |
|
|
|
JsoupDocumentReader |
|
|
|
MarkdownDocumentReader |
|
|
|
PagePdfDocumentReader |
|
|
|
ParagraphPdfDocumentReader |
|
| 混合/未知格式 | TikaDocumentReader |
万能,推荐首选 |
最佳实践建议
-
**优先使用 TikaDocumentReader**:除非有特殊需求(如需要精细控制 HTML 选择器),否则一个 Tika 全搞定。 -
Reader 只负责“读”,不负责“切”:读完的 Document可能很大,下一步必须接TokenTextSplitter进行切分(这也是我们下一篇文章的重点)。 -
善用元数据:读取时尽量把文件名、页码、章节标题等信息存入元数据,后续检索时可以按这些条件过滤。
如果觉得文章对你有帮助,欢迎 点赞、在看、转发 三连支持!我们下期见。
夜雨聆风