第8篇:支持PDF/Word/Excel!Java实现多格式文档自动解析入库 (Java+AI落地实战系列 | 开箱即用 | 批量文档一键入库)

0 阅读10分钟

第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步:

  1. 文件上传:前端/接口上传文档,后端接收文件
  2. 格式解析:根据文件后缀,调用对应工具提取纯文本内容
  3. 文本清洗:去除多余空格、换行、页眉页脚、乱码等无效内容
  4. 智能切片:按照语义长度切成合适的文本片段
  5. 向量入库:片段向量化,存入向量数据库

本质上就是给上一篇的RAG知识库,升级了「文档输入层」,后面的检索、问答逻辑完全不用改。 在这里插入图片描述

二、技术选型:Java生态最稳的文档解析工具

我们选Apache全家桶,开源免费、无版权风险,企业项目里用了十几年,稳定性拉满:

文件格式解析工具说明
PDFApache PDFBoxApache官方出品,支持文本提取、加密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字符),按需调整。


八、生产环境进阶优化方向

现在这套已经能满足中小规模的文档入库需求,真正落地企业级项目,还可以继续扩展: 在这里插入图片描述

  1. 批量异步入库:大量文件上传时,用线程池异步处理,接口立刻返回,后台慢慢解析,不会超时
  2. OCR识别:支持扫描版PDF、图片文档的文字识别,覆盖更多业务场景
  3. 文档版本管理:同一份文件更新版本,自动删除旧版本的向量数据,避免重复内容
  4. 文档去重:自动识别重复内容,避免相同文档反复入库占用空间
  5. 分类标签:给文档打部门、类型标签,检索时按权限过滤,不同人只能看自己权限内的文档

九、本篇小结

今天我们给RAG知识库升级了文档输入能力,从「手动粘贴文本」进化到「上传文件自动入库」,支持PDF、Word、Excel、TXT四大常用格式,附带文本清洗、智能切片,已经可以满足绝大多数内部工具、小团队的知识库需求。

到这里,我们的RAG知识库已经完成了「问答→自动入库」的闭环,离生产可用又近了一大步。


下篇预告

现在我们用的还是内存向量库,文档少了没问题,一旦文档多了、服务重启,数据就全丢了,根本撑不起生产环境。 所以下一篇我们讲:

第9篇:别再用内存向量库了!Milvus向量数据库Java对接全指南

内容会覆盖:

  • 向量数据库选型,为什么选Milvus
  • Docker一键部署Milvus单机版
  • LangChain4j对接Milvus完整代码
  • 数据持久化、批量检索、性能优化
  • 内存库平滑迁移到Milvus方案

🎁 粉丝福利

本篇完整代码已更新进系列源码包,包含:

  • 多格式文档解析工具类全量代码
  • 文件上传自动入库完整实现
  • 文本清洗+智能切片逻辑
  • 配套接口+测试用例

关注Java-AI工程师,打出 系列源码 四字即可。 每更新一篇,我都会往资料包里新增对应源码,跟着系列就能从零搭出完整的企业级AI系统。

觉得文章有用,欢迎点赞、在看、转发给身边的同事,大家一起少踩坑。