第8篇:支持PDF/Word/Excel!Java实现多格式文档自动解析入库(Java+AI落地实战系列 | 开箱即用 | 批量一键入库)
-
第1篇:Java程序员学AI/转AI全指南,从CURD到AI应用工程师(零算法,4周落地) -
第2篇:Java 后端转 AI,这条完整链路 90% 的人没搞懂!AI 怎么自动干活、Java转AI链路:LLM、RAG、FunctionCall、ToolCall、Skills、Agent、MCP -
第3篇:别再焦虑了!Java开发做AI,根本不用学Python -
第4篇:10分钟跑通!SpringBoot对接通义千问,实现AI对话功能 -
第5篇:SpringBoot实现流式输出,和ChatGPT体验一模一样 -
第6篇:对话记忆怎么实现?Java版多轮上下文对话完整方案 -
第7篇:从零搭建本地RAG知识库,内网可用,零API费用
上一篇我们搭好了本地RAG知识库,终于解决了大模型幻觉和数据安全的问题。但很多同学跑通之后吐槽:每次入库都要手动复制文本,太反人类了——公司的制度、手册、产品文档全是PDF、Word、Excel,总不能让我一页一页粘吧?
安排。
今天我们就给知识库加上「多格式文档自动解析」能力,上传文件自动提取文本、清洗降噪、智能切片、入库向量库,整个过程不用手动复制内容,几百份文档批量上传就能搞定,真正做到开箱即用。
先看一眼升级前后的差距:

一、先理清楚:文档自动入库的完整流程
别上来就写代码,先把整个链路理清楚,每一步做什么心里有数,后续出问题也好排查。
整个自动入库流程分为5步:
本质上就是给上一篇的RAG知识库,升级了「文档输入层」,后面的检索、问答逻辑完全不用改。

二、技术选型:Java生态最稳的文档解析工具
我们选Apache全家桶,开源免费、无版权风险,企业项目里用了十几年,稳定性拉满:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
踩坑提醒:老格式.doc/.xls兼容性很差,问题极多,建议统一转成.docx/.xlsx再处理,本篇也针对主流的新格式实现。
新增Maven依赖
在上一篇的pom基础上,新增文档解析的依赖:
<dependencies><!-- 省略上一篇已有的SpringBoot、LangChain4j、Ollama依赖 --><!-- Apache POI 处理Word/Excel --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.3</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency><!-- Apache PDFBox 处理PDF --><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.32</version></dependency></dependencies>
三、第一步:文档解析工具类
新建DocumentParserUtil,统一封装不同格式的文本提取逻辑,外加文本清洗方法,直接复制就能用。
/*** 文档解析工具类* 支持PDF、DOCX、XLSX、TXT格式文本提取*/public class DocumentParserUtil {/*** 根据文件类型自动解析提取文本*/public static String extractText(MultipartFile file) throws IOException {String fileName = file.getOriginalFilename();if (fileName == null) {throw new IllegalArgumentException("文件名不能为空");}String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();return switch (suffix) {case "txt" -> extractTxt(file);case "docx" -> extractWord(file);case "pdf" -> extractPdf(file);case "xlsx" -> extractExcel(file);default -> throw new IllegalArgumentException("不支持的文件格式:" + suffix);};}/*** 解析TXT文本*/private static String extractTxt(MultipartFile file) throws IOException {return new String(file.getBytes(), StandardCharsets.UTF_8);}/*** 解析Word文档(仅支持.docx格式)*/private static String extractWord(MultipartFile file) throws IOException {try (XWPFDocument document = new XWPFDocument(file.getInputStream())) {XWPFWordExtractor extractor = new XWPFWordExtractor(document);return extractor.getText();}}/*** 解析PDF文档*/private static String extractPdf(MultipartFile file) throws IOException {try (PDDocument document = PDDocument.load(file.getInputStream())) {PDFTextStripper stripper = new PDFTextStripper();// 按顺序提取文本,避免排版混乱stripper.setSortByPosition(true);return stripper.getText(document);}}/*** 解析Excel文档(仅支持.xlsx格式)* 按行读取,单元格之间用空格分隔,每行换行*/private static String extractExcel(MultipartFile file) throws IOException {StringBuilder content = new StringBuilder();try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {// 遍历所有sheetfor (int i = 0; i < workbook.getNumberOfSheets(); i++) {Sheet sheet = workbook.getSheetAt(i);content.append("【Sheet页:").append(sheet.getSheetName()).append("】\n");// 遍历所有行for (Row row : sheet) {StringBuilder rowContent = new StringBuilder();for (Cell cell : row) {// 统一转成字符串读取cell.setCellType(CellType.STRING);rowContent.append(cell.getStringCellValue()).append(" ");}content.append(rowContent.toString().trim()).append("\n");}content.append("\n");}}return content.toString();}/*** 文本清洗:去除多余空格、换行、无效字符*/public static String cleanText(String text) {if (StrUtil.isBlank(text)) {return "";}// 1. 替换多个换行、空格为单个text = text.replaceAll("\\s+", " ");// 2. 去除常见的页眉页脚固定字符(可根据自己业务扩展)text = text.replaceAll("第\\d+页 / 共\\d+页", "");text = text.replaceAll("版权所有.*", "");// 3. 去除首尾空白return text.trim();}}
重点说下cleanText清洗方法:很多新手解析完文档直接入库,结果里面全是页眉、页脚、多余空行,检索的时候一堆无效内容,准确率特别差。清洗这一步看似简单,对检索效果的提升远超你的想象。
四、第二步:改造文档入库服务
在上一篇的DocumentService里新增文件入库方法,把「解析→清洗→切片→入库」全流程串起来。
@Servicepublic class DocumentService {@Autowiredprivate EmbeddingStore<TextSegment> embeddingStore;@Autowiredprivate EmbeddingModel embeddingModel;/*** 上一篇的纯文本入库方法,保留不变*/public void addTextDocument(String content, String docName) {String cleanContent = DocumentParserUtil.cleanText(content);DocumentSplitter splitter = DocumentSplitters.recursive(500, 50);List<TextSegment> segments = splitter.split(Document.from(cleanContent));segments.forEach(segment -> segment.metadata().put("docName", docName));EmbeddingStoreIngestor.builder().documentSplitter(splitter).embeddingModel(embeddingModel).embeddingStore(embeddingStore).build().ingest(segments);}/*** 新增:文件自动解析入库* @param file 上传的文档文件*/public void addFileDocument(MultipartFile file) throws IOException {String fileName = file.getOriginalFilename();// 1. 提取文本String rawText = DocumentParserUtil.extractText(file);// 2. 文本清洗String cleanText = DocumentParserUtil.cleanText(rawText);// 3. 文档切片DocumentSplitter splitter = DocumentSplitters.recursive(500, 50);List<TextSegment> segments = splitter.split(Document.from(cleanText));// 4. 打上文件名元数据,方便溯源segments.forEach(segment -> {segment.metadata().put("docName", fileName);segment.metadata().put("uploadTime", LocalDateTime.now().toString());});// 5. 向量化入库EmbeddingStoreIngestor.builder().documentSplitter(splitter).embeddingModel(embeddingModel).embeddingStore(embeddingStore).build().ingest(segments);}}
五、第三步:新增文件上传接口
在RagController里加上传接口,前端直接传文件就能自动入库。
@RestController@RequestMapping("/api/rag")public class RagController {@Autowiredprivate DocumentService documentService;@Autowiredprivate RagChatService ragChatService;/*** 上一篇的纯文本入库接口,保留不变*/@PostMapping("/addDoc")public Result<String> addDocument(@RequestBodyDocAddRequest request) {documentService.addTextDocument(request.getContent(), request.getDocName());return Result.success("文档入库成功");}/*** 新增:文件上传自动入库接口*/@PostMapping("/uploadDoc")public Result<String> uploadDocument(MultipartFile file) {try {documentService.addFileDocument(file);return Result.success("文件解析入库成功");} catch (IOException e) {return Result.fail("文件解析失败:" + e.getMessage());} catch (IllegalArgumentException e) {return Result.fail(e.getMessage());}}/*** 知识库问答接口,和上一篇一致*/@GetMapping("/chat")public Result<String> chat(String question) {String answer = ragChatService.chatWithKnowledge(question);return Result.success(answer);}// 省略实体类、统一返回类,和之前一致}
六、测试验证
第一步:上传文件
用Postman/Apifox调用上传接口,选择一个PDF/Word文件:
-
请求地址:POST http://localhost:8080/api/rag/uploadDoc -
请求类型:form-data -
参数名:file,选择本地文档
返回「文件解析入库成功」,就说明文档已经自动解析、切片、存入向量库了。
第二步:问答验证
调用问答接口,提问文档里的内容,比如上传了公司报销制度,就问「二线城市住宿报销标准是多少」,能准确返回答案就说明成功了。

七、新手必踩的5个坑,提前帮你避了
坑1:POI版本冲突,启动直接报错
这是90%的人会遇到的问题,项目里本来就有旧版POI依赖,新版本加进去直接报NoClassDefFoundError。
解决方案:用mvn dependency:tree查冲突的依赖,exclude掉旧版本,统一POI版本号,所有poi相关的包版本必须完全一致。
坑2:PDF解析乱码,全是方框和问号
Linux服务器上尤其常见,本质是服务器缺少中文字体,PDFBox渲染不出来。
解决方案:服务器安装宋体、黑体等中文字体,或者用PDFBox的字体配置指定字体路径;Windows/Mac本地一般不会有这个问题。
坑3:大Excel文件解析,直接内存溢出
几十兆的Excel用普通XSSFWorkbook读取,很容易把内存撑爆。
解决方案:大文件改用SXSSFWorkbook流式读取,逐行处理,不会一次性把整个文件加载到内存里。
坑4:扫描版PDF解析出来是空的
扫描版PDF本质是图片,没有文本层,PDFBox提取不到内容。
解决方案:需要接入OCR文字识别(比如Tesseract、百度OCR),先把图片转成文本再入库,属于进阶功能。
坑5:切片大小一刀切,检索效果差
Excel表格、制度条文、技术文档用同一个切片大小,要么切太碎语义不完整,要么切太大检索不准。
解决方案:不同类型文档用不同切片策略——表格类文档切小一点(300字符),长文本文档切大一点(600字符),按需调整。
八、生产环境进阶优化方向
现在这套已经能满足中小规模的文档入库需求,真正落地企业级项目,还可以继续扩展:

九、本篇小结
今天我们给RAG知识库升级了文档输入能力,从「手动粘贴文本」进化到「上传文件自动入库」,支持PDF、Word、Excel、TXT四大常用格式,附带文本清洗、智能切片,已经可以满足绝大多数内部工具、小团队的知识库需求。
到这里,我们的RAG知识库已经完成了「问答→自动入库」的闭环,离生产可用又近了一大步。
下篇预告
现在我们用的还是内存向量库,文档少了没问题,一旦文档多了、服务重启,数据就全丢了,根本撑不起生产环境。所以下一篇我们讲:
第9篇:别再用内存向量库了!Milvus向量数据库Java对接全指南
内容会覆盖:
-
向量数据库选型,为什么选Milvus -
Docker一键部署Milvus单机版 -
LangChain4j对接Milvus完整代码 -
数据持久化、批量检索、性能优化 -
内存库平滑迁移到Milvus方案
🎁 粉丝福利
本篇完整代码已更新进系列源码包,包含:
-
多格式文档解析工具类全量代码 -
文件上传自动入库完整实现 -
文本清洗+智能切片逻辑 -
配套接口+测试用例
关注公众号Java-AI工程师,后台回复 系列源码 即可免费领取。每更新一篇,我都会往资料包里新增对应源码,跟着系列就能从零搭出完整的企业级AI系统。
⭐ 记得星标公众号,每周更新一篇,跟着走不迷路。觉得文章有用,欢迎点赞、在看、转发给身边的同事,大家一起少踩坑。
夜雨聆风