第8篇:支持PDF/Word/Excel!Java实现多格式文档自动解析入库
(Java+AI落地实战系列 | 开箱即用 | 批量文档一键入库)
本文是《Java+AI落地实战 从入门到生产级》系列第8篇 往期回顾:
- 第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全家桶,开源免费、无版权风险,企业项目里用了十几年,稳定性拉满:
| 文件格式 | 解析工具 | 说明 |
|---|---|---|
| Apache PDFBox | Apache官方出品,支持文本提取、加密PDF处理 | |
| Word(.docx) | Apache POI | 处理Office文档的标准工具,兼容绝大多数格式 |
| Excel(.xlsx) | Apache POI | 支持单元格读取、表格转文本 |
| TXT | 原生JDK | 直接读取,处理编码即可 |
踩坑提醒:老格式
.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())) {
// 遍历所有sheet
for (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里新增文件入库方法,把「解析→清洗→切片→入库」全流程串起来。
@Service
public class DocumentService {
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
private 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 {
@Autowired
private DocumentService documentService;
@Autowired
private RagChatService ragChatService;
/**
* 上一篇的纯文本入库接口,保留不变
*/
@PostMapping("/addDoc")
public Result<String> addDocument(@RequestBody DocAddRequest 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字符),按需调整。
八、生产环境进阶优化方向
现在这套已经能满足中小规模的文档入库需求,真正落地企业级项目,还可以继续扩展:
- 批量异步入库:大量文件上传时,用线程池异步处理,接口立刻返回,后台慢慢解析,不会超时
- OCR识别:支持扫描版PDF、图片文档的文字识别,覆盖更多业务场景
- 文档版本管理:同一份文件更新版本,自动删除旧版本的向量数据,避免重复内容
- 文档去重:自动识别重复内容,避免相同文档反复入库占用空间
- 分类标签:给文档打部门、类型标签,检索时按权限过滤,不同人只能看自己权限内的文档
九、本篇小结
今天我们给RAG知识库升级了文档输入能力,从「手动粘贴文本」进化到「上传文件自动入库」,支持PDF、Word、Excel、TXT四大常用格式,附带文本清洗、智能切片,已经可以满足绝大多数内部工具、小团队的知识库需求。
到这里,我们的RAG知识库已经完成了「问答→自动入库」的闭环,离生产可用又近了一大步。
下篇预告
现在我们用的还是内存向量库,文档少了没问题,一旦文档多了、服务重启,数据就全丢了,根本撑不起生产环境。 所以下一篇我们讲:
第9篇:别再用内存向量库了!Milvus向量数据库Java对接全指南
内容会覆盖:
- 向量数据库选型,为什么选Milvus
- Docker一键部署Milvus单机版
- LangChain4j对接Milvus完整代码
- 数据持久化、批量检索、性能优化
- 内存库平滑迁移到Milvus方案
🎁 粉丝福利
本篇完整代码已更新进系列源码包,包含:
- 多格式文档解析工具类全量代码
- 文件上传自动入库完整实现
- 文本清洗+智能切片逻辑
- 配套接口+测试用例
关注Java-AI工程师,打出 系列源码 四字即可。
每更新一篇,我都会往资料包里新增对应源码,跟着系列就能从零搭出完整的企业级AI系统。
觉得文章有用,欢迎点赞、在看、转发给身边的同事,大家一起少踩坑。