Doc Extractor 文档解析
Doc Extractor 是 Agents-Flex 提供的统一文档内容提取能力。它可以从本地文件、HTTP URL、输入流或字节数组中识别文档类型,并将正文、表格和图片转换为适合大模型、RAG、全文检索和内容审查使用的 Markdown 风格文本。
无论输入是 Office 文档、PDF、网页、邮件、电子书、OpenDocument、代码文件,还是包含多个文档的压缩包,调用方都可以使用同一套 API。
Maven 依赖
Doc Extractor 已从 agents-flex-core 中拆分为独立模块,需要显式引入:
<dependency>
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-doc-extractor</artifactId>
<version>2.2.7</version>
</dependency>公共 API 位于 com.agentsflex.doc 包中。
核心能力
- 统一输入接口:支持文件、HTTP、
InputStream、byte[]和自定义数据源。 - 丰富的格式覆盖:覆盖 Microsoft Office、OpenDocument、PDF、RTF、HTML、邮件、EPUB、Apple iWork、压缩包、代码和配置文件。
- Markdown 风格输出:保留标题、段落、表格、分页和幻灯片结构,内嵌图片可输出为 Base64 Data URI 或自定义存储 URL。
- 自动类型识别:综合文件名、扩展名和 MIME 类型选择解析器,并支持候选解析器降级重试。
- 复杂文档支持:可处理 Office 宏格式、模板格式、旧版二进制 Office 文件以及压缩包中的嵌套文档。
- 面向大文件设计:HTTP 大文件自动落盘,PDF 使用内存与临时文件混合模式。
- 可扩展架构:可以注册自定义
DocumentExtractor,也可以实现新的DocumentSource。
快速开始
使用便捷工具类
String text = DocumentExtractors.extract(new File("/path/to/report.docx"));
System.out.println(text);从 HTTP 地址读取:
String text = DocumentExtractors.extractFromUrl(
"https://example.com/files/report.pdf"
);当 URL 中没有可靠的文件名时,建议显式传入文件名和 MIME:
String text = DocumentExtractors.extractFromUrl(
"https://example.com/download?id=1001",
"report.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);使用服务实例
需要注册自定义解析器或控制解析器集合时,使用 DocumentExtractionService:
DocumentExtractionService service = new DocumentExtractionService();
String fromFile = service.extract(new File("/path/to/report.pdf"));
String fromUrl = service.extractFromUrl("https://example.com/report.pdf");从流和字节数组读取
try (InputStream input = new FileInputStream("/path/to/data.tsv")) {
String text = service.extract(
input,
"data.tsv",
"text/tab-separated-values"
);
}byte[] bytes = Files.readAllBytes(Paths.get("/path/to/slides.pptx"));
String text = service.extract(
bytes,
"slides.pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
);文件名和 MIME 会参与解析器选择。处理流、字节数组或无扩展名下载地址时,应尽量提供准确的 fileName 和 mimeType。
支持格式
Microsoft Word
| 类型 | 扩展名 | 解析能力 |
|---|---|---|
| Word 97-2003 文档 | .doc | 段落、图片、Markdown 表格 |
| Word 97-2003 模板 | .dot | 段落、图片、Markdown 表格 |
| Word OOXML 文档 | .docx | 段落、图片、Markdown 表格 |
| Word OOXML 模板 | .dotx | 段落、图片、Markdown 表格 |
| Word 宏文档 | .docm | 与 DOCX 相同,宏不会执行 |
| Word 宏模板 | .dotm | 与 DOCX 相同,宏不会执行 |
Microsoft Excel
| 类型 | 扩展名 | 解析能力 |
|---|---|---|
| Excel 97-2003 工作簿 | .xls | 多 Sheet、公式计算、Markdown 表格 |
| Excel 97-2003 模板 | .xlt | 多 Sheet、公式计算、Markdown 表格 |
| Excel OOXML 工作簿 | .xlsx | 多 Sheet、公式计算、Markdown 表格 |
| Excel 宏工作簿 | .xlsm | 与 XLSX 相同,宏不会执行 |
| Excel OOXML 模板 | .xltx | 多 Sheet、公式计算、Markdown 表格 |
| Excel 宏模板 | .xltm | 与 XLSX 相同,宏不会执行 |
| Excel 二进制工作簿 | .xlsb | 通过 Apache Tika 提取可读内容 |
Microsoft PowerPoint
| 类型 | 扩展名 | 解析能力 |
|---|---|---|
| PowerPoint 97-2003 演示文稿 | .ppt | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint 97-2003 放映文件 | .pps | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint 97-2003 模板 | .pot | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint OOXML 演示文稿 | .pptx | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint OOXML 放映文件 | .ppsx | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint OOXML 模板 | .potx | 幻灯片文本、图片、Markdown 表格 |
| PowerPoint 宏演示文稿 | .pptm | 与 PPTX 相同,宏不会执行 |
| PowerPoint 宏放映文件 | .ppsm | 与 PPTX 相同,宏不会执行 |
| PowerPoint 宏模板 | .potm | 与 PPTX 相同,宏不会执行 |
PDF
| 类型 | 扩展名 | 解析能力 |
|---|---|---|
| PDF 文档 | .pdf | 分页文本、实际绘制的图片、同页图片去重 |
PDF 输出包含 --- Page N --- 分页标记。图片统一转换为 PNG 后交给 ExtractedImageHandler 处理。纯扫描 PDF 可以提取页面中的图片,但当前不包含 OCR 文字识别。
OpenDocument
| 分类 | 扩展名 |
|---|---|
| 文本文档 | .odt、.fodt |
| 电子表格 | .ods、.fods |
| 演示文稿 | .odp、.fodp |
| 绘图文档 | .odg、.fodg |
| 文本模板 | .ott |
| 表格模板 | .ots |
| 演示模板 | .otp |
| 绘图模板 | .otg |
OpenDocument 通过 Apache Tika 提取为 XHTML,再统一转换为 Markdown 风格文本。
富文本、网页和电子书
| 分类 | 扩展名 | 说明 |
|---|---|---|
| Rich Text Format | .rtf | 提取富文本正文 |
| HTML | .html、.htm、.xhtml | 提取标题、段落、列表、链接和表格,并过滤常见网页噪音 |
| MHTML | .mhtml | 支持常见 HTML 内容,复杂 multipart 资源取决于文档结构 |
| EPUB | .epub | 按电子书内容顺序提取章节正文 |
邮件
| 类型 | 扩展名 | 说明 |
|---|---|---|
| RFC 822 邮件 | .eml | 提取主题、正文及可解析的嵌入内容 |
| Outlook 邮件 | .msg | 提取 Outlook 消息正文和可解析内容 |
Apple iWork
| 类型 | 扩展名 |
|---|---|
| Pages 文档 | .pages |
| Numbers 表格 | .numbers |
| Keynote 演示文稿 | .key |
iWork 通过 Apache Tika 的 Apple parser 处理。Apple 多次调整 iWork 内部 IWA 格式,因此不同应用版本的提取结果可能不同;部分现代文件可能只能获得元数据和包内资源信息。
压缩包
| 类型 | 扩展名 |
|---|---|
| ZIP | .zip |
| TAR | .tar |
| GZip/TGZ | .gz、.tgz |
| BZip2 | .bz2 |
| XZ | .xz |
| 7-Zip | .7z |
| RAR | .rar |
压缩包会递归解析其中受支持的文档,并在输出中保留条目文件名。为防止压缩炸弹,默认安全限制如下:
| 限制项 | 默认值 |
|---|---|
| 最大嵌套深度 | 3 层 |
| 最大条目数 | 100 |
| 单个解压条目 | 20 MB |
| 累计解压内容 | 100 MB |
超过限制的条目会被跳过。加密压缩包需要外部提供密码,目前不会自动解密。
基础文本和结构化文本
| 分类 | 扩展名 |
|---|---|
| 基础文本 | .txt、.text、.log |
| Markdown | .md、.markdown |
| 表格文本 | .csv、.tsv |
| 数据交换 | .json、.xml、.yml、.yaml |
| Java 配置 | .properties |
| 通用配置 | .conf、.cfg、.config、.ini、.toml、.env |
| 工程配置 | .editorconfig、.gitignore、.gitattributes |
纯文本解析器支持 UTF-8、GBK、GB2312 等常见编码自动检测,并接受通用的 text/* MIME。
代码文件
| 生态 | 扩展名 |
|---|---|
| JVM | .java、.kt、.kts、.groovy、.scala、.gradle |
| Python/Ruby/PHP | .py、.rb、.php |
| JavaScript/TypeScript | .js、.mjs、.cjs、.jsx、.ts、.tsx |
| Web 框架 | .vue、.svelte |
| C/C++ | .c、.h、.cc、.cpp、.hpp |
| 其他编译语言 | .cs、.go、.rs、.swift、.dart |
| 脚本 | .sh、.bash、.zsh、.fish、.bat、.cmd、.ps1 |
| 数据库 | .sql |
| 样式 | .css、.scss、.sass、.less |
| 其他 | .lua、.r |
同时支持常见的无扩展名工程文件:Dockerfile、Makefile、Jenkinsfile。
输出格式
Doc Extractor 输出的是便于模型理解和后续切分的 Markdown 风格文本。
表格
| Name | Score |
| --- | --- |
| Alice | 95 |Word、PowerPoint、Excel、HTML 和部分 Tika 文档中的表格统一使用第一行作为表头,并输出 | --- | 分隔行。不同长度的行会补充空单元格,单元格内的反斜杠、管道符和换行会统一转义。
图片
DOC、DOCX、PPT、PPTX、PDF,以及 Tika 支持的长尾文档中可识别的内嵌图片会转换为 Markdown 图片。默认使用 Data URI;生产环境通常通过 ExtractedImageHandler 替换为对象存储 URL。完整配置和扩展方式见下文“ExtractedImageHandler 图片处理”章节。
页面和幻灯片
--- Page 1 ---
PDF page content
--- Slide 2 ---
Presentation content分页和幻灯片边界可以直接用于 RAG 分块、引用定位和上下文展示。
Excel Sheet
### Sheet1
| Product | Amount |
| :--- | :--- |
| Service | 1000 |ExtractedImageHandler 图片处理
ExtractedImageHandler 是内嵌图片从“文档二进制数据”转换为“Markdown 可引用地址”的扩展点。解析器负责发现和读取图片,Handler 负责决定图片如何保存以及最终返回什么地址。
文档解析器 -> imageBytes + mimeType + fileName
-> ExtractedImageHandler.handle(...)
-> URL / Data URI / null
-> 适用场景
| 场景 | 建议处理方式 |
|---|---|
| 本地开发、小文档预览 | 使用默认 Base64ExtractedImageHandler,无需管理外部文件 |
| RAG 入库、生产文档解析 | 上传到 OSS、S3、COS 等对象存储并返回稳定 URL |
| 不需要图片的纯文本检索 | 返回 null,跳过所有图片,降低存储和 Token 开销 |
| 图片需要统一规范 | 在 Handler 中压缩、转码、生成缩略图或清理元数据 |
| 受控内容平台 | 执行格式、大小、安全检查后再写入内部文件服务 |
默认构造的 DocumentExtractionService 使用 Base64ExtractedImageHandler。它将图片编码为 data:<mimeType>;base64,...,适合直接预览,但会明显增大结果字符串和后续模型上下文。
接口契约
@FunctionalInterface
public interface ExtractedImageHandler {
String handle(byte[] imageBytes, String mimeType, String fileName)
throws IOException;
}| 参数或返回值 | 说明 |
|---|---|
imageBytes | 从文档中提取的图片二进制数据 |
mimeType | 图片 MIME 类型;无法识别时为 application/octet-stream |
fileName | 文档内的图片文件名;原格式不提供文件名时为 embedded-image |
| 返回 URL 或 Data URI | 解析器将其输出为  |
返回 null 或空字符串 | 跳过该图片,不输出 Markdown 图片标记 |
抛出 IOException | 当前候选提取器失败,服务记录日志并尝试下一个候选提取器 |
Handler 只决定 Markdown 中的图片地址,不负责生成图片说明或 OCR 文本。
上传到对象存储
ExtractedImageHandler imageHandler = (imageBytes, mimeType, fileName) -> {
String objectKey = imageKeyGenerator.fromContent(imageBytes, fileName);
return objectStorage.upload(objectKey, imageBytes, mimeType);
};
DocumentExtractionService service = new DocumentExtractionService(imageHandler);
String markdown = service.extract(new File("/path/to/report.docx"));当 Handler 返回 https://cdn.example.com/files/image-1.png 时,结果中会写入:
建议根据图片内容摘要生成对象 Key。候选提取器降级、调用方重试或同一图片重复出现时,Handler 可能处理相同图片多次;使用内容摘要可以让上传操作保持幂等。
跳过图片
只需要正文和表格时,可以显式忽略图片:
DocumentExtractionService service = new DocumentExtractionService(
(imageBytes, mimeType, fileName) -> null
);配置范围
构造服务实例时配置,适合不同租户或不同解析任务采用不同存储策略:
DocumentExtractionService service = new DocumentExtractionService(imageHandler);也可以在服务创建后替换:
service.setExtractedImageHandler(imageHandler);自定义注册中心和 Handler 可以同时传入:
DocumentExtractionService service =
new DocumentExtractionService(registry, imageHandler);静态入口支持进程级默认配置:
DocumentExtractors.setExtractedImageHandler(imageHandler);
String markdown = DocumentExtractors.extract(file);DocumentExtractors 的配置由整个进程共享,不适合保存租户级或请求级状态。存在多种图片策略时,应创建独立的 DocumentExtractionService。
自定义提取器接入
自定义 DocumentExtractor 只有覆盖包含 Handler 的重载,才能使用服务中配置的图片策略。图片应通过 MarkdownFormatter 处理,以获得一致的空值、默认 MIME、默认文件名和 Markdown 格式:
public class CustomExtractor implements DocumentExtractor {
@Override
public boolean supports(DocumentSource source) {
return "custom".equalsIgnoreCase(getExtension(source.getFileName()));
}
@Override
public String extractText(DocumentSource source) throws IOException {
return extractText(source, new Base64ExtractedImageHandler());
}
@Override
public String extractText(DocumentSource source,
ExtractedImageHandler imageHandler) throws IOException {
byte[] image = readEmbeddedImage(source);
StringBuilder output = new StringBuilder("Document content");
MarkdownFormatter.appendImage(
output, imageHandler, image, "image/png", "figure-1.png"
);
return output.toString();
}
}不处理图片的自定义提取器只需实现单参数的 extractText(DocumentSource)。
生产环境注意事项
- Handler 可能被多个解析请求并发调用,实现必须是线程安全的,或只使用方法内局部状态。
fileName和mimeType来自文档元数据,不能直接作为可信文件路径或安全校验依据;应清理文件名并校验实际文件签名。- 图片以完整
byte[]传入,应限制原始文档和单张图片大小,避免超大图片造成内存压力。 - 返回值会直接进入 Markdown 图片地址,应返回经过正确编码的 URL 或合法 Data URI。
- 上传失败可以抛出
IOException。由于服务随后可能尝试其他候选提取器,上传、计费和审计操作应设计为幂等。 - Handler 不会自动执行 OCR;扫描件文字识别需要单独的 OCR 流程。
工作原理
一次解析请求会经过以下流程:
DocumentSource提供文件名、MIME 和可重复打开的输入流。ExtractorRegistry根据文件名和 MIME 筛选候选解析器。- 候选解析器按
getOrder()从小到大依次执行。 - 解析器发现内嵌图片时,将图片数据交给
ExtractedImageHandler,并把返回地址写入 Markdown。 - 第一个返回非空内容的解析器结束本次处理。
- 所有解析器均失败或返回空内容时,服务返回
null。 - 请求结束后自动调用
DocumentSource.cleanup()清理临时资源。
默认注册的解析器包括:
| 解析器 | 主要职责 |
|---|---|
PdfTextExtractor | PDF 文字、分页和图片 |
DocExtractor | Word 97-2003 |
DocxExtractor | Word OOXML、宏格式和模板 |
PptExtractor | PowerPoint 97-2003 |
PptxExtractor | PowerPoint OOXML、宏格式和模板 |
ExcelExtractor | XLS/XLSX、宏格式和模板 |
TikaDocumentExtractor | RTF、OpenDocument、邮件、EPUB、XLSB、iWork 和压缩包 |
HtmlExtractor | HTML 内容清洗和 Markdown 转换 |
PlainTextExtractor | 文本、代码、配置和结构化文本 |
DocumentSource
内置数据源
| 数据源 | 用途 |
|---|---|
FileDocumentSource | 本地文件 |
HttpDocumentSource | HTTP/HTTPS 文件,支持缓存和临时文件 |
ByteArrayDocumentSource | 已加载到内存的字节数组 |
ByteStreamDocumentSource | 输入流,内部缓存后允许解析器重复打开 |
TemporaryFileStreamDocumentSource | 将大输入流保存为临时文件,默认最大 100 MB |
HTTP 下载配置
HttpDocumentSource 默认连接超时为 20 秒、读取超时为 60 秒。已知长度且不超过 10 MB 的文件使用内存缓存,其他文件使用临时文件,并在解析结束后清理。
HttpDocumentSource source = new HttpDocumentSource(
"https://example.com/private/report.pdf",
"report.pdf",
"application/pdf",
10_000,
60_000,
connection -> connection.setRequestProperty(
"Authorization",
"Bearer " + accessToken
)
);
String text = service.extract(source);获取已缓存的数据大小:
long size = source.getCachedSize();自定义数据源
可以通过实现 DocumentSource 接入对象存储、数据库、内部文件系统或其他数据平台:
public class ObjectStorageDocumentSource implements DocumentSource {
private final String objectKey;
public ObjectStorageDocumentSource(String objectKey) {
this.objectKey = objectKey;
}
@Override
public String getFileName() {
return objectKey;
}
@Override
public String getMimeType() {
return "application/pdf";
}
@Override
public InputStream openStream() {
return objectStorageClient.openStream(objectKey);
}
@Override
public void cleanup() {
// 按需释放临时文件、连接或其他资源
}
}openStream() 可能被多个候选解析器调用,因此自定义数据源应返回新的、可独立读取的流。
自定义解析器
实现 DocumentExtractor 并注册到 ExtractorRegistry:
public class CustomExtractor implements DocumentExtractor {
@Override
public boolean supports(DocumentSource source) {
return "custom".equalsIgnoreCase(getExtension(source.getFileName()));
}
@Override
public String extractText(DocumentSource source) throws IOException {
try (InputStream input = source.openStream()) {
byte[] bytes = IOUtils.toByteArray(input, 10 * 1024 * 1024);
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new IOException("Failed to parse custom document", e);
}
}
@Override
public int getOrder() {
return 20;
}
}注册并创建服务:
ExtractorRegistry registry = new ExtractorRegistry();
registry.register(new CustomExtractor());
DocumentExtractionService service = new DocumentExtractionService(registry);getOrder() 数值越小,尝试顺序越靠前。自定义解析器不应执行文档中的宏、脚本或外部命令。
自定义解析器需要输出表格或处理内嵌图片时,应复用 MarkdownFormatter.appendTable(...)、MarkdownFormatter.handleImage(...) 和 MarkdownFormatter.appendImage(...),以保持与内置解析器一致的 Markdown、空值和元数据处理规则。
错误处理
DocumentExtractionService 会记录候选解析器及失败原因,并尝试下一个候选解析器。
| 场景 | 结果 |
|---|---|
DocumentSource 为 null | 抛出 IllegalArgumentException |
| 没有支持该格式的解析器 | 返回 null |
| 所有候选解析器失败 | 返回 null,并记录 WARN 日志 |
| 解析器成功但没有可读内容 | 继续尝试其他候选,最终可能返回 null |
生产环境应检查返回值:
String text = service.extract(file);
if (text == null || text.trim().isEmpty()) {
// 记录失败、进入人工处理或 OCR 流程
}安全与性能
- Office 宏格式只读取文档内容,不会执行宏。
- PDF 使用 32 MB 内存阈值和临时文件混合加载,降低大文件堆内存压力。
- PDF 单张图片默认限制为 5000 万像素,异常超大图片会被跳过。
- 压缩包限制嵌套深度、条目数量和解压大小,降低 ZIP bomb 风险。
- HTTP 内容仅下载一次;小文件缓存在内存,大文件写入临时文件。
- 处理不可信文件时,仍建议在隔离进程中设置请求超时、堆内存上限和文件大小上限。
ByteStreamDocumentSource会将整个流缓存到内存;大流优先使用TemporaryFileStreamDocumentSource。
已知边界
- 扫描 PDF 和普通图片尚未集成 OCR;PDF 中没有文本层时主要返回图片。
- PDF 通常没有语义化表格结构,基础解析器不会对任意坐标文本强行推断表格。
- 加密 Office、PDF、EPUB 或压缩包需要密码时,默认解析可能失败。
- Apple iWork 内部格式随应用版本变化,部分现代文件只能提取元数据和资源列表。
- MHTML 的复杂 multipart、内嵌脚本和动态网页行为不会完整还原。
- Tika 长尾格式以内容提取为目标,不保证完全保留源文档视觉布局。
- 默认的 Base64 图片会显著增加输出长度;生产环境建议配置
ExtractedImageHandler,在解析过程中直接替换为对象存储 URL。
推荐实践
- 对流和 HTTP 下载尽量提供准确的文件名及 MIME。
- 大文件使用临时文件数据源,并在入口处限制原始文件大小。
- 将返回的分页、幻灯片或 Sheet 标记作为 RAG 分块边界。
- 使用
ExtractedImageHandler在解析图片时直接上传到对象存储,避免在最终文本中保留 Base64 数据。 - 对扫描件配置独立 OCR 流程,并与 Doc Extractor 的文本结果合并。
- 对
null结果保留原文件和解析日志,便于降级处理与格式补充。
从 File2Text 迁移
Doc Extractor 是原 agents-flex-core 内 File2Text 功能的独立模块。升级时需要同时修改 Maven 依赖、包名和入口 API:
| 旧 API | 新 API |
|---|---|
com.agentsflex.core.file2text.File2TextService | com.agentsflex.doc.DocumentExtractionService |
com.agentsflex.core.file2text.File2TextUtil | com.agentsflex.doc.DocumentExtractors |
FileExtractor | DocumentExtractor |
readFromFile(...) / extractTextFromFile(...) | extract(...) |
readFromHttpUrl(...) / extractTextFromHttpUrl(...) | extractFromUrl(...) |
extractTextFromSource(...) | extract(...) |
旧包不再由 agents-flex-core 提供。只使用 Chat、Prompt、Tool、Memory 等核心能力的项目不再传递引入 POI、PDFBox、Tika、JSoup 和 ICU4J。