Java开发者AI转型第十课!化繁为简!Spring AI 全能文档解析器 (Document Readers) 与元数据提取实操
大家好我是直奔標杆今天继续咱们《Spring AI 零基础到实战》系列的第十节分享全程实战干货适合Java开发者跟着练一起吃透Spring AI文档解析核心能力共同进步在上一节《Java开发者AI转型第九课突破知识边界企业级 RAG (检索增强生成) 核心架构与 ETL 管道初探》的分享中咱们已经明确了企业级RAG落地的第一步——ETL数据管道的EExtract抽取环节这也是搭建企业知识库的基础前提。做过企业级开发的小伙伴都清楚咱们日常接触的文件五花八门PDF格式的财报、Word版的简历、HTML网页资料还有平时记笔记用的Markdown文档。但大模型本质上只能处理纯文本字符串根本没法直接“识别”这些夹杂着图片、表格和复杂排版的二进制物理文件。所以咱们迫切需要一款“万能工具”能把这些千奇百怪的文件格式统一提取出纯净文本并且封装到Spring AI标准的Document领域对象中为后续的RAG流程铺路。好在Spring AI已经帮咱们集成了Java领域非常强大的文件解析工具比如Apache Tika和PDFBox今天咱们就手把手代码实战把物理文件成功“加载”到内存中迈出搭建企业知识库的关键一步建议大家跟着敲一遍代码加深理解本节学习目标建议收藏统一抽象掌握Spring生态下标准化的文件加载方式org.springframework.core.io.Resource适配各类文件读取场景。万能解析实战运用TikaDocumentReader一键搞定Word/PPT/HTML等上百种文件格式的文本提取高效又便捷。精准拆分实战使用PagePdfDocumentReader实现PDF文件按页精准读取解决大文件解析痛点。多模态进阶自定义文档读取器深度拆解如何同时提取PDF中的文本与图片打造图文并茂的Document对象适配多模态大模型需求。物理文件 → 数字资产解析流程拆解在动手写代码之前咱们先通过架构逻辑直观理解一下各类格式的物理文件是如何通过Spring AI的解析引擎被“处理”成标准化的Document对象的核心逻辑很简单无论你传入的是哪种格式的文件只要经过DocumentReader的解析处理最终输出的一定是结构统一的Document对象集合——每个对象都包含唯一ID、文本内容Content和溯源元数据Metadata这也是后续RAG检索的核心数据载体。前置准备依赖引入与测试文件在Spring Boot项目中读取文件咱们可以借助底层的org.springframework.core.io.Resource接口非常便捷。建议大家把测试用的PDF或Word文档放在src/main/resources/docs目录下方便后续读取。直奔標杆这里准备了两个测试PDF文件大家可以参考sample.pdf纯文字内容和sample2.pdf包含图片内容用于后续不同场景的实战测试。默认情况下Spring AI自带JSON和纯文本的简单提取器无需额外引入依赖。但如果要解析HTML、PDF、Word这类复杂文档就需要在pom.xml中显式引入对应的读取器组件咱们后续以PDF解析为例直接上依赖代码可直接复制使用!-- 1. Tika解析器万能解析工具支持Word、PPT、HTML、TXT等上百种格式 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-tika-document-reader/artifactId /dependency !-- 2. PDF专用解析器按页精准拆分基于Apache PDFBox适配PDF专项解析场景 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pdf-document-reader/artifactId /dependency实战一TikaDocumentReader——万能解析“神器”Apache Tika相信很多小伙伴都用过它能从几乎所有主流文档格式中提取文本而Spring AI将其封装成了TikaDocumentReader调用起来非常简单两行核心代码就能搞定咱们直接上测试用例注释很详细新手也能看懂// 直奔標杆TikaDocumentReader实战测试 Test void extractWithTika() { // 1. 实例化TikaDocumentReader传入测试文件资源 TikaDocumentReader reader new TikaDocumentReader(samplePdfResource); // 2. 调用get()方法触发底层解析引擎完成全量解析 ListDocument documents reader.get(); // 3. 打印解析结果查看核心信息 Document doc documents.get(0); System.out.println(【文档ID】: doc.getId()); System.out.println(【文档内容前100字】: doc.getText().substring(0, Math.min(100, doc.getText().length()))); System.out.println(【元数据Metadata】: doc.getMetadata()); }运行结果与解析【文档 ID】: f4e9f440-6047-4318-8b9f-2eed92428eb8 【文档内容前100字】: 1. 《春晓》·孟浩然 春眠不觉晓处处闻啼⻦。 夜来⻛⾬声花落知多少。 ... 【元数据 Metadata】: {sourcesample.pdf}这里和大家拆解一下TikaDocumentReader的解析逻辑非常直接高效它会把整个文件的文本内容全部提取出来封装到一个单独的Document对象中。✅ 优点兼容性极强万物皆可解析代码极简适合快速提取各类文件的文本内容不用单独适配不同格式。❌ 痛点解析逻辑比较“粗暴”如果你的PDF有100页Tika会把所有内容揉成一个几十万字的“大文本块”。不仅大模型的Token窗口扛不住更关键的是丢失了页码定位信息后续RAG检索时无法精准溯源这在企业级场景中是比较致命的。实战二PagePdfDocumentReader——PDF精准按页解析为了解决Tika解析的痛点如果咱们的业务场景明确是处理PDF文件建议使用Spring AI基于Apache PDFBox引擎封装的PagePdfDocumentReader。它的核心优势的是按页解析一页生成一个独立的Document对象完美保留页码信息。直接上实战代码包含解析规则配置大家可以按需调整// 直奔標杆PagePdfDocumentReader实战测试PDF按页解析 Test void extractWithPdfReader() { // 1. 配置PDF解析规则优化解析效果忽略页眉页脚避免无关文本干扰 PdfDocumentReaderConfig config PdfDocumentReaderConfig.builder() .withPageTopMargin(0) // 忽略页眉 .withPageBottomMargin(0) // 忽略页脚 .withPageExtractedTextFormatter(ExtractedTextFormatter.builder() .withNumberOfTopTextLinesToDelete(0) .build()) .build(); // 2. 实例化PDF专用解析器传入文件资源和解析配置 PagePdfDocumentReader reader new PagePdfDocumentReader(sample2PdfResource, config); // 3. 触发解析获取解析后的Document集合 ListDocument documents reader.get(); System.out.println(共解析出 documents.size() 页独立内容。); // 4. 遍历打印每一页的核心信息 for (int i 0; i documents.size(); i) { Document doc documents.get(i); System.out.println(\n--- 第 (i 1) 个Document对象 ---); System.out.println(【文档ID】: doc.getId()); System.out.println(【纯文本内容】: doc.getText().trim()); // 重点关注元数据中会包含页码信息方便后续溯源 System.out.println(【元数据Metadata】: doc.getMetadata()); } }运行结果与核心亮点共解析出 2 页独立内容。 --- 第 1 个 Document 对象 --- 【文档 ID】: f6e8b2d5-e030-4e59-9f57-535b7c5dfd0b 【纯文本内容】: 1. 《春晓》·孟浩然... 【元数据 Metadata】: {page_number2, file_namesample2.pdf}这就是企业级场景中推荐的高质量数据清洗方式解析器会自动将当前内容的“页码”“文件名”等信息存入metadata字典中。后续大模型基于这页内容回答用户问题时咱们就能从Metadata中提取出page_number直接在前端标注“此回答参考自第X页”提升回答的可信度和可追溯性这一点非常实用但这里有个小问题原生的PagePdfDocumentReader默认只提取文本内容咱们测试用的sample2.pdf中包含的图片并没有被解析出来——这在多模态大模型时代显然满足不了需求因为图片中往往包含架构图、流程图等关键信息。实战三进阶自定义——分离式多模态PDF读取器随着多模态大模型的普及图片解析已经成为企业级AI应用的常见需求。为了同时提取PDF中的文本和图片直奔標杆这里给大家分享一个自定义读取器SeparatedMultimodalPdfReader核心思路是“分离提取、分类标注”。核心设计理念目前主流向量数据库对图文混排的支持还比较有限因此我们将文本和图片分离提取——一页PDF中的文本生成一个纯文本Document页面中的每一张图片生成一个独立的、携带Media属性的图片Document再通过Metadata给它们打上分类标签确保两者互不干扰后续可分别处理。以下是精简后的核心实现代码关键逻辑已标注大家可直接复用改造/** * 直奔標杆分离式多模态PDF读取器核心代码片段 * 核心功能将PDF中的文本和图片分离提取分别封装为独立的Document对象方便后续多模态处理 */ public class SeparatedMultimodalPdfReader implements DocumentReader { // 自定义Metadata标签用于区分Document类型文本/图片 public static final String METADATA_CONTENT_TYPE content_type; public static final String CONTENT_TYPE_TEXT text; public static final String CONTENT_TYPE_IMAGE image; // ... 省略构造函数与PDFBox初始化代码核心是初始化PDF解析相关资源 ... Override public ListDocument get() { ListDocument readDocuments new ArrayList(); // ... 省略分页遍历逻辑核心是遍历PDF的每一页分别处理文本和图片 ... for (PDPage page : pages) { // 1. 提取当前页纯文本生成文本类型Document String pageText extractText(page); if (StringUtils.hasText(pageText)) { readDocuments.add(createTextDocument(pageText, pageNumber)); } // 2. 提取当前页所有图片生成独立的图片类型Document ListDocument imageDocs extractImageDocumentsFromPage(page, pageNumber); readDocuments.addAll(imageDocs); } return readDocuments; } /** * 核心方法将页面中的每张图片提取为独立的Document对象 */ private ListDocument extractImageDocumentsFromPage(PDPage page, int pageNumber) throws IOException { ListDocument imageDocs new ArrayList(); PDResources resources page.getResources(); // ... 省略资源判空与遍历逻辑核心是遍历页面中的所有图片资源 ... // 发现图片对象PDImageXObject开始提取处理 if (xObject instanceof PDImageXObject pdImage) { BufferedImage image pdImage.getImage(); byte[] imageBytes convertToBytes(image); // 将图片转为字节流方便后续处理 // 1. 构建Spring AI的Media多媒体对象封装图片信息 String mimeType image/ pdImage.getSuffix(); Media media new Media(MimeTypeUtils.parseMimeType(mimeType), new ByteArrayResource(imageBytes)); // 2. 注入自定义Metadata标记图片类型方便后续区分 MapString, Object metadata new HashMap(); metadata.put(page_number, pageNumber); // 保留图片所在页码方便溯源 metadata.put(METADATA_CONTENT_TYPE, CONTENT_TYPE_IMAGE); // 关键标记为图片类型 // 3. 构造携带图片的Document对象加入集合 imageDocs.add(new Document(media, metadata)); } return imageDocs; } // ... 省略createTextDocument等辅助方法核心是生成文本类型Document ... }多模态解析实战测试关键一步有了这个自定义读取器咱们就可以同时提取PDF中的文本和图片并且能直接将图片传入多模态大模型如GPT-4o解析图片内容或者存入向量数据库实现“以图搜图”完美适配多模态场景需求。直接上测试代码演示如何区分文本和图片并调用大模型解析图片// 直奔標杆多模态解析实战测试提取文本图片并解析图片内容 Test void extractWithSeparatedMultimodalPdfReader() { SeparatedMultimodalPdfReader reader new SeparatedMultimodalPdfReader(sample2PdfResource, config); ListDocument documents reader.get(); // 遍历解析结果区分文本和图片分别处理 for (Document doc : documents) { // 利用自定义Metadata标签区分当前Document是文本还是图片 String contentType (String) doc.getMetadata().get(SeparatedMultimodalPdfReader.METADATA_CONTENT_TYPE); if (SeparatedMultimodalPdfReader.CONTENT_TYPE_TEXT.equals(contentType)) { System.out.println(【纯文本内容】: doc.getText().trim()); } else { // 如果是图片Document直接调用大模型的视觉能力Vision解析图片内容 UserMessage userMessage UserMessage.builder() .text(详细描述这张图的内容重点说明核心信息) .media(Media.builder() .mimeType(MimeTypeUtils.IMAGE_PNG) .data(doc.getMedia().get(0).getData()) .build()) .build(); // 调用大模型获取图片解析结果 String imageDesc chatClientBuilder.build().prompt() .messages(userMessage) .call() .content(); System.out.println(【图片AI解析内容】: imageDesc); } } }运行结果与总结共解析出 4 页独立内容。 --- 第 1 个 Document 对象 (图片) --- 【图片 AI 解析内容】: 这张图片的内容是关于设计和实现一个基于知识图谱KG与检索增强生成RAG的考研智能问答系统。核心在于构建结构化知识并研发能够精准解答问题的智能检索引擎...这样一来PDF中隐藏的图片就被完美提取并解析了大模型相当于充当了“高级OCR”的角色帮我们精准识别图片中的复杂内容比如架构图、流程图这在企业级知识库搭建中非常实用能最大程度保留文件中的关键信息。本节课核心总结必看今天咱们实战了三种文档解析方式TikaDocumentReader万能解析、PagePdfDocumentReaderPDF精准按页解析、自定义SeparatedMultimodalPdfReader多模态解析虽然用法不同但它们有一个共同的核心使命——产出标准化的ListDocument。这个Document对象就是后续所有RAG检索、向量运算的“通用数据载体”相当于咱们搭建企业知识库的“基础积木”。通过本节课的实战咱们成功跑通了RAG的第一步ETL - E抽取环节把物理文件转换成了内存中的标准化数字资产。但新的挑战也随之而来如果用Tika解析100页的PDF会被揉成一个庞大的Document哪怕是按页解析的PagePdf一页的文字也可能高达2000字。而大模型的上下文窗口是有限的咱们绝对不能把这么大段的内容直接用于Embedding向量化和入库——必须把它切得更小、更精细这就是咱们下一节课的核心内容。下节预告持续跟进不迷路下一节咱们将进入《Java开发者AI转型第十一课文本切分术Spring AI 智能分块 (Text Splitters) 与 Overlap 语义防割裂指南》重点学习RAG ETL管道中的TTransform转换环节如何精准将大段文本切分成500字左右的小片段Chunk什么是Chunk Size块大小和Overlap重叠度核心作用是什么如果不设置Overlap会导致“一刀切断上下文”的严重问题吗如何避免干货持续输出跟着直奔標杆一起从零基础吃透Spring AI顺利完成Java开发者的AI转型咱们下节课见往期回顾方便大家连贯学习Java开发者AI转型第七课AI失忆症克星ChatMemory对话历史管理与上下文实战Java开发者AI转型第八课避开Token陷阱Spring AI记忆裁剪源码解析与Token级防溢出核心技巧Java开发者AI转型第九课突破知识边界企业级 RAG (检索增强生成) 核心架构与 ETL 管道初探我是直奔標杆专注Java开发者AI转型实战分享每节课都贴合实际开发场景拒绝纸上谈兵。大家在实战过程中有任何问题欢迎在评论区留言交流一起学习、一起进步共同成为AI时代的Java标杆开发者