✅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 文本
* 例如: -> 
*/
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一样。只不过不太适合做标题分段。