跳到主要内容

Doc Extractor 文档解析

Doc Extractor 是 Agents-Flex 提供的统一文档内容提取能力。它可以从本地文件、HTTP URL、输入流或字节数组中识别文档类型,并将正文、表格和图片转换为适合大模型、RAG、全文检索和内容审查使用的 Markdown 风格文本。

无论输入是 Office 文档、PDF、网页、邮件、电子书、OpenDocument、代码文件,还是包含多个文档的压缩包,调用方都可以使用同一套 API。

Maven 依赖

Doc Extractor 已从 agents-flex-core 中拆分为独立模块,需要显式引入:

xml
<dependency>
    <groupId>com.agentsflex</groupId>
    <artifactId>agents-flex-doc-extractor</artifactId>
    <version>2.2.7</version>
</dependency>

公共 API 位于 com.agentsflex.doc 包中。

核心能力

  • 统一输入接口:支持文件、HTTP、InputStreambyte[] 和自定义数据源。
  • 丰富的格式覆盖:覆盖 Microsoft Office、OpenDocument、PDF、RTF、HTML、邮件、EPUB、Apple iWork、压缩包、代码和配置文件。
  • Markdown 风格输出:保留标题、段落、表格、分页和幻灯片结构,内嵌图片可输出为 Base64 Data URI 或自定义存储 URL。
  • 自动类型识别:综合文件名、扩展名和 MIME 类型选择解析器,并支持候选解析器降级重试。
  • 复杂文档支持:可处理 Office 宏格式、模板格式、旧版二进制 Office 文件以及压缩包中的嵌套文档。
  • 面向大文件设计:HTTP 大文件自动落盘,PDF 使用内存与临时文件混合模式。
  • 可扩展架构:可以注册自定义 DocumentExtractor,也可以实现新的 DocumentSource

快速开始

使用便捷工具类

java
String text = DocumentExtractors.extract(new File("/path/to/report.docx"));
System.out.println(text);

从 HTTP 地址读取:

java
String text = DocumentExtractors.extractFromUrl(
    "https://example.com/files/report.pdf"
);

当 URL 中没有可靠的文件名时,建议显式传入文件名和 MIME:

java
String text = DocumentExtractors.extractFromUrl(
    "https://example.com/download?id=1001",
    "report.docx",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);

使用服务实例

需要注册自定义解析器或控制解析器集合时,使用 DocumentExtractionService

java
DocumentExtractionService service = new DocumentExtractionService();

String fromFile = service.extract(new File("/path/to/report.pdf"));
String fromUrl = service.extractFromUrl("https://example.com/report.pdf");

从流和字节数组读取

java
try (InputStream input = new FileInputStream("/path/to/data.tsv")) {
    String text = service.extract(
        input,
        "data.tsv",
        "text/tab-separated-values"
    );
}
java
byte[] bytes = Files.readAllBytes(Paths.get("/path/to/slides.pptx"));
String text = service.extract(
    bytes,
    "slides.pptx",
    "application/vnd.openxmlformats-officedocument.presentationml.presentation"
);

文件名和 MIME 会参与解析器选择。处理流、字节数组或无扩展名下载地址时,应尽量提供准确的 fileNamemimeType

支持格式

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

同时支持常见的无扩展名工程文件:DockerfileMakefileJenkinsfile

输出格式

Doc Extractor 输出的是便于模型理解和后续切分的 Markdown 风格文本。

表格

markdown
| Name | Score |
| --- | --- |
| Alice | 95 |

Word、PowerPoint、Excel、HTML 和部分 Tika 文档中的表格统一使用第一行作为表头,并输出 | --- | 分隔行。不同长度的行会补充空单元格,单元格内的反斜杠、管道符和换行会统一转义。

图片

markdown
![Image](data:image/png;base64,iVBORw0KGgo...)

DOC、DOCX、PPT、PPTX、PDF,以及 Tika 支持的长尾文档中可识别的内嵌图片会转换为 Markdown 图片。默认使用 Data URI;生产环境通常通过 ExtractedImageHandler 替换为对象存储 URL。完整配置和扩展方式见下文“ExtractedImageHandler 图片处理”章节。

页面和幻灯片

markdown
--- Page 1 ---
PDF page content

--- Slide 2 ---
Presentation content

分页和幻灯片边界可以直接用于 RAG 分块、引用定位和上下文展示。

Excel Sheet

markdown
### Sheet1

| Product | Amount |
| :--- | :--- |
| Service | 1000 |

ExtractedImageHandler 图片处理

ExtractedImageHandler 是内嵌图片从“文档二进制数据”转换为“Markdown 可引用地址”的扩展点。解析器负责发现和读取图片,Handler 负责决定图片如何保存以及最终返回什么地址。

text
文档解析器 -> imageBytes + mimeType + fileName
          -> ExtractedImageHandler.handle(...)
          -> URL / Data URI / null
          -> ![Image](返回值)

适用场景

场景建议处理方式
本地开发、小文档预览使用默认 Base64ExtractedImageHandler,无需管理外部文件
RAG 入库、生产文档解析上传到 OSS、S3、COS 等对象存储并返回稳定 URL
不需要图片的纯文本检索返回 null,跳过所有图片,降低存储和 Token 开销
图片需要统一规范在 Handler 中压缩、转码、生成缩略图或清理元数据
受控内容平台执行格式、大小、安全检查后再写入内部文件服务

默认构造的 DocumentExtractionService 使用 Base64ExtractedImageHandler。它将图片编码为 data:<mimeType>;base64,...,适合直接预览,但会明显增大结果字符串和后续模型上下文。

接口契约

java
@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解析器将其输出为 ![Image](...)
返回 null 或空字符串跳过该图片,不输出 Markdown 图片标记
抛出 IOException当前候选提取器失败,服务记录日志并尝试下一个候选提取器

Handler 只决定 Markdown 中的图片地址,不负责生成图片说明或 OCR 文本。

上传到对象存储

java
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 时,结果中会写入:

markdown
![Image](https://cdn.example.com/files/image-1.png)

建议根据图片内容摘要生成对象 Key。候选提取器降级、调用方重试或同一图片重复出现时,Handler 可能处理相同图片多次;使用内容摘要可以让上传操作保持幂等。

跳过图片

只需要正文和表格时,可以显式忽略图片:

java
DocumentExtractionService service = new DocumentExtractionService(
    (imageBytes, mimeType, fileName) -> null
);

配置范围

构造服务实例时配置,适合不同租户或不同解析任务采用不同存储策略:

java
DocumentExtractionService service = new DocumentExtractionService(imageHandler);

也可以在服务创建后替换:

java
service.setExtractedImageHandler(imageHandler);

自定义注册中心和 Handler 可以同时传入:

java
DocumentExtractionService service =
    new DocumentExtractionService(registry, imageHandler);

静态入口支持进程级默认配置:

java
DocumentExtractors.setExtractedImageHandler(imageHandler);
String markdown = DocumentExtractors.extract(file);

DocumentExtractors 的配置由整个进程共享,不适合保存租户级或请求级状态。存在多种图片策略时,应创建独立的 DocumentExtractionService

自定义提取器接入

自定义 DocumentExtractor 只有覆盖包含 Handler 的重载,才能使用服务中配置的图片策略。图片应通过 MarkdownFormatter 处理,以获得一致的空值、默认 MIME、默认文件名和 Markdown 格式:

java
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 可能被多个解析请求并发调用,实现必须是线程安全的,或只使用方法内局部状态。
  • fileNamemimeType 来自文档元数据,不能直接作为可信文件路径或安全校验依据;应清理文件名并校验实际文件签名。
  • 图片以完整 byte[] 传入,应限制原始文档和单张图片大小,避免超大图片造成内存压力。
  • 返回值会直接进入 Markdown 图片地址,应返回经过正确编码的 URL 或合法 Data URI。
  • 上传失败可以抛出 IOException。由于服务随后可能尝试其他候选提取器,上传、计费和审计操作应设计为幂等。
  • Handler 不会自动执行 OCR;扫描件文字识别需要单独的 OCR 流程。

工作原理

一次解析请求会经过以下流程:

  1. DocumentSource 提供文件名、MIME 和可重复打开的输入流。
  2. ExtractorRegistry 根据文件名和 MIME 筛选候选解析器。
  3. 候选解析器按 getOrder() 从小到大依次执行。
  4. 解析器发现内嵌图片时,将图片数据交给 ExtractedImageHandler,并把返回地址写入 Markdown。
  5. 第一个返回非空内容的解析器结束本次处理。
  6. 所有解析器均失败或返回空内容时,服务返回 null
  7. 请求结束后自动调用 DocumentSource.cleanup() 清理临时资源。

默认注册的解析器包括:

解析器主要职责
PdfTextExtractorPDF 文字、分页和图片
DocExtractorWord 97-2003
DocxExtractorWord OOXML、宏格式和模板
PptExtractorPowerPoint 97-2003
PptxExtractorPowerPoint OOXML、宏格式和模板
ExcelExtractorXLS/XLSX、宏格式和模板
TikaDocumentExtractorRTF、OpenDocument、邮件、EPUB、XLSB、iWork 和压缩包
HtmlExtractorHTML 内容清洗和 Markdown 转换
PlainTextExtractor文本、代码、配置和结构化文本

DocumentSource

内置数据源

数据源用途
FileDocumentSource本地文件
HttpDocumentSourceHTTP/HTTPS 文件,支持缓存和临时文件
ByteArrayDocumentSource已加载到内存的字节数组
ByteStreamDocumentSource输入流,内部缓存后允许解析器重复打开
TemporaryFileStreamDocumentSource将大输入流保存为临时文件,默认最大 100 MB

HTTP 下载配置

HttpDocumentSource 默认连接超时为 20 秒、读取超时为 60 秒。已知长度且不超过 10 MB 的文件使用内存缓存,其他文件使用临时文件,并在解析结束后清理。

java
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);

获取已缓存的数据大小:

java
long size = source.getCachedSize();

自定义数据源

可以通过实现 DocumentSource 接入对象存储、数据库、内部文件系统或其他数据平台:

java
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

java
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;
    }
}

注册并创建服务:

java
ExtractorRegistry registry = new ExtractorRegistry();
registry.register(new CustomExtractor());

DocumentExtractionService service = new DocumentExtractionService(registry);

getOrder() 数值越小,尝试顺序越靠前。自定义解析器不应执行文档中的宏、脚本或外部命令。

自定义解析器需要输出表格或处理内嵌图片时,应复用 MarkdownFormatter.appendTable(...)MarkdownFormatter.handleImage(...)MarkdownFormatter.appendImage(...),以保持与内置解析器一致的 Markdown、空值和元数据处理规则。

错误处理

DocumentExtractionService 会记录候选解析器及失败原因,并尝试下一个候选解析器。

场景结果
DocumentSourcenull抛出 IllegalArgumentException
没有支持该格式的解析器返回 null
所有候选解析器失败返回 null,并记录 WARN 日志
解析器成功但没有可读内容继续尝试其他候选,最终可能返回 null

生产环境应检查返回值:

java
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。

推荐实践

  1. 对流和 HTTP 下载尽量提供准确的文件名及 MIME。
  2. 大文件使用临时文件数据源,并在入口处限制原始文件大小。
  3. 将返回的分页、幻灯片或 Sheet 标记作为 RAG 分块边界。
  4. 使用 ExtractedImageHandler 在解析图片时直接上传到对象存储,避免在最终文本中保留 Base64 数据。
  5. 对扫描件配置独立 OCR 流程,并与 Doc Extractor 的文本结果合并。
  6. null 结果保留原文件和解析日志,便于降级处理与格式补充。

从 File2Text 迁移

Doc Extractor 是原 agents-flex-core 内 File2Text 功能的独立模块。升级时需要同时修改 Maven 依赖、包名和入口 API:

旧 API新 API
com.agentsflex.core.file2text.File2TextServicecom.agentsflex.doc.DocumentExtractionService
com.agentsflex.core.file2text.File2TextUtilcom.agentsflex.doc.DocumentExtractors
FileExtractorDocumentExtractor
readFromFile(...) / extractTextFromFile(...)extract(...)
readFromHttpUrl(...) / extractTextFromHttpUrl(...)extractFromUrl(...)
extractTextFromSource(...)extract(...)

旧包不再由 agents-flex-core 提供。只使用 Chat、Prompt、Tool、Memory 等核心能力的项目不再传递引入 POI、PDFBox、Tika、JSoup 和 ICU4J。