know-engine

项目中多种格式的文档处理?

PDF ✅MinerU文档处理功能实现 前面我们介绍了如何使用MinerU的网页端进行文件的处理。但是我们总不能每次文档都去网页上处理吧,所以我们需要有工程化的手段来实现。介绍两种方案: 使用API服务 这种方式适合小团队,要处理的…

TL;DR

PDF ✅MinerU文档处理功能实现 前面我们介绍了如何使用MinerU的网页端进行文件的处理。但是我们总不能每次文档都去网页上处理吧,所以我们需要有工程化的手段来实现。介绍两种方案: 使用API服务 这种方式适合小团队,要处理的…

PDF

✅MinerU文档处理功能实现

前面我们介绍了如何使用MinerU的网页端进行文件的处理。但是我们总不能每次文档都去网页上处理吧,所以我们需要有工程化的手段来实现。介绍两种方案: 使用API服务 这种方式适合小团队,要处理的pdf不多,对处理速度要求不高的情况。个人实验也 LLMentor

✅在Java代码中把MinerU解析文档功能集成进来

(MinerU相关的内容,虽然之前公司也在用,但是部署这块是算法同学搞的,这次从部署开始弄,我花了很多时间,前前后后搞了差不多三四个通宵吧,把一些过程中遇到的问题都整理出来了。网上这块的资料比较少,踩了很多坑,希望能给大家点帮助) 我们前面 LLMentor

WORD

因为我们使用MinerU做了pdf的处理,那MinerU是否支持doc和docx的word文档呢,尝试着调用fast api,发现报错如下: HTTP 400, {"error":"Unsupported file type: docx"} 于是我去github上查了一下,官方说不支持。。。。 https://github.com/opendatalab/MinerU/discussions/3746 但是,不要慌,他只是旧版本(我之前用的2.7.6)不支持,把你的mineru升级到3.0以后的版本就支持了。升级方式如下: 卸载后重新安装最新的3.0.x版本: 然后就和pdf一模一样的用法了。也是转成zip,然后解压上传处理。

EXCEL

✅Excel文件如何处理?

一个优秀的rag系统,肯定要支持很多种类型的文件的处理,那么有一种常见的格式,即excel,我们要如何处理他呢? 我们前面介绍过pdf的处理,使用了mineru这个工具,然后市面上也有一些处理excel的方案,是这样的。会先通过LibreO LLMentor

CSV

同 excel

Markdown

markdown没啥要讲的,因为pdf、word都是转成markdown再处理的,所以markdown的处理就是直接读取内容然后分段就好了。 markdown的格式,分段上不需要做特别处理,但是图片是需要处理的,即把文档中的图片描述通过LLM转换出来,并且替换文档中的原来的描述。 代码实现如下:

package cn.hollis.llm.mentor.know.engine.document.service.impl;

import cn.hollis.llm.mentor.know.engine.document.constant.ContentType;
import cn.hollis.llm.mentor.know.engine.document.constant.DocumentStatus;
import cn.hollis.llm.mentor.know.engine.document.constant.FileType;
import cn.hollis.llm.mentor.know.engine.document.constant.KnowledgeBaseType;
import cn.hollis.llm.mentor.know.engine.document.entity.KnowledgeDocument;
import cn.hollis.llm.mentor.know.engine.document.service.FileStorageService;
import cn.hollis.llm.mentor.know.engine.document.service.KnowledgeDocumentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Markdown 文档处理服务实现类
 * <p>
 * 与 MinerU 流程不同,此处的输入流本身就是一个 Markdown 文件,
 * 文件中的图片地址通常已经是公网可访问的 URL(如 MinerU 输出的 cdn 地址)。
 * 该类的职责:
 * 1. 直接读取 Markdown 内容
 * 2. 解析其中的图片标签 
> 🖼️ 原文引用的图片资源未包含在导出文件中:`alt`

 * 3. 调用视觉大模型为每张图片生成描述,替换 alt 文本
 * 4. 将处理后的 Markdown 上传到 MinIO,并更新文档状态为 CONVERTED
 */
@Slf4j
@Service
public class MarkdownProcessServiceImpl extends MinerUProcessBaseServiceImpl {

    private static final String CONVERTED_FILE_DIR = "converted/";

    /**
     * 匹配 Markdown 图片标签:
> 🖼️ 原文引用的图片资源未包含在导出文件中:`alt`

     */
    private static final Pattern IMAGE_PATTERN = Pattern.compile("!\\[(.*?)\\]\\(([^)]+)\\)");

    @Autowired
    private FileStorageService fileStorageService;

    @Autowired
    private KnowledgeDocumentService knowledgeDocumentService;

    @Override
    public void processDocument(KnowledgeDocument document, InputStream inputStream) {
        log.info("开始处理 Markdown 文档图片描述生成,documentId: {}", document.getDocTitle());

        // 更新状态为转换中
        document.setStatus(DocumentStatus.CONVERTING);
        boolean result = knowledgeDocumentService.updateById(document);
        Assert.isTrue(result, "文件CONVERTING状态更新失败");

        try {
            // 1. 读取 Markdown 文件内容
            String mdContent = readInputStreamAsString(inputStream);

            // 2. 为图片生成描述并替换 alt 文本
            String processedMdContent = enrichImageDescriptions(mdContent);

            // 3. 上传处理后的 Markdown 到 MinIO
            String docTitle = document.getDocTitle();
            String baseName = docTitle.contains(".") ? docTitle.substring(0, docTitle.lastIndexOf(".")) : docTitle;
            String convertedObjectName = CONVERTED_FILE_DIR + baseName + ".md";
            String convertedUrl = fileStorageService.uploadFile(
                    convertedObjectName,
                    processedMdContent.getBytes(StandardCharsets.UTF_8),
                    ContentType.TEXT_MARKDOWN);

            // 4. 更新文档状态为已转换
            document.setStatus(DocumentStatus.CONVERTED);
            document.setConvertedDocUrl(convertedUrl);
            result = knowledgeDocumentService.updateById(document);
            Assert.isTrue(result, "文件CONVERTED状态更新失败");

            log.info("Markdown 文档处理完成,documentId: {}, convertedUrl: {}", document.getDocTitle(), convertedUrl);
        } catch (Exception e) {
            log.error("Markdown 文档处理失败,documentId: {}", document.getDocTitle(), e);
            // 处理失败,状态回滚为 UPLOADED
            document.setStatus(DocumentStatus.UPLOADED);
            result = knowledgeDocumentService.updateById(document);
            Assert.isTrue(result, "文件UPLOADED状态更新失败");
            throw new RuntimeException("Markdown 文档处理失败: " + e.getMessage(), e);
        } finally {
            closeQuietly(inputStream);
        }
    }

    /**
     * 遍历 Markdown 中的图片标签,为每张图片调用视觉模型生成描述并替换 alt 文本
     * 例如:![image](https://xxx/foo.jpg) -> ![这是一张xxx图片,xxx描述](https://xxx/foo.jpg)
     */
    private String enrichImageDescriptions(String mdContent) {
        Matcher matcher = IMAGE_PATTERN.matcher(mdContent);
        StringBuffer result = new StringBuffer();

        while (matcher.find()) {
            String originAlt = matcher.group(1);
            String imageUrl = matcher.group(2);

            String description;
            try {
                // 调用基类提供的视觉模型生成描述
                description = generateImageDescription(imageUrl);
                if (description == null || description.isBlank()) {
                    log.warn("图片描述为空,保留原 alt 文本,url: {}", imageUrl);
                    description = originAlt;
                } else {
                    // 去除可能存在的换行,保证 Markdown 标签单行
                    description = description.replaceAll("[\\r\\n]+", " ").trim();
                }
                log.info("图片描述已生成: {} -> {}", imageUrl, description);
            } catch (Exception e) {
                log.warn("生成图片描述失败,保留原 alt 文本,url: {}", imageUrl, e);
                description = originAlt;
            }

            String newImageTag = "
> 🖼️ 原文引用的图片资源未包含在导出文件中:`" + description + "`
";
            matcher.appendReplacement(result, Matcher.quoteReplacement(newImageTag));
        }
        matcher.appendTail(result);

        return result.toString();
    }

    /**
     * 将 InputStream 全量读取为字符串
     */
    private String readInputStreamAsString(InputStream inputStream) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[8192];
            int n;
            while ((n = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, n);
            }
            return baos.toString(StandardCharsets.UTF_8);
        }
    }

    /**
     * 安静关闭输入流,忽略异常
     */
    private void closeQuietly(InputStream inputStream) {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (Exception ignored) {
                // 忽略关闭异常
            }
        }
    }

    @Override
    public boolean supports(FileType fileType, KnowledgeBaseType knowledgeBaseType) {
        return fileType == FileType.MARKDOWN;
    }
}

TXT

其实markdown本质上也是txt的纯文本文件,只不过是有一些markdown语法的内容而已,所以txt的处理方式和markdown一样。只不过不太适合做标题分段。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。