最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot集成Docling文檔解析服務的完全指南

 更新時間:2026年05月17日 14:55:08   作者:奮斗的老史  
本文從入門到精通,詳細介紹如何在 SpringBoot項目中集成Docling文檔解析服務,包含環(huán)境搭建、配置詳解、661行核心代碼逐行解析、請求參數(shù)詳解、返回數(shù)據處理、向量存儲集成、生產級優(yōu)化方案和完整測試用例,看完本文即可獨立實現(xiàn)企業(yè)級文檔解析功能

一、背景介紹

1.1 企業(yè)級 AI 應用場景

在企業(yè)級 AI 應用中,RAG(檢索增強生成) 已經成為標配功能。而 RAG 的第一步,就是把企業(yè)的 PDF、Word、Excel 等文檔解析成機器可讀的結構化數(shù)據。

典型場景

  • 用戶上傳 100 頁的 PDF 產品手冊,包含文字、表格、圖片、公式
  • 財務部門的 Excel 報表,需要提取關鍵數(shù)據
  • 合同文檔中的條款需要結構化存儲
  • 技術手冊中的流程圖需要 OCR 識別

1.2 傳統(tǒng)方案的痛點

方案優(yōu)點缺點
Apache Tika支持格式多,社區(qū)成熟只能提取純文本,表格和圖片丟失結構
PDFBox純 Java,無依賴需要手寫大量解析邏輯,表格識別效果差
商業(yè) OCR 服務準確率高成本高(每頁 0.1-0.5 元),數(shù)據隱私風險
自建 OCR數(shù)據可控開發(fā)周期長(3-6 個月),維護成本高

1.3 本文解決方案

使用 IBM 開源的 Docling 文檔解析引擎,配合 Spring Boot 實現(xiàn)企業(yè)級文檔解析服務。

核心優(yōu)勢

  • ? 開箱即用:Docker 一鍵部署,5 分鐘上手
  • ? 格式支持全:PDF/DOCX/XLSX/PPTX/Markdown/HTML
  • ? 結構化提取:文字、表格、圖片、公式、標題層級
  • ? 內置 OCR:支持 30+ 語言,準確率 85%~90%
  • ? 免費開源:Apache 2.0 協(xié)議,無商業(yè)限制
  • ? 生產級:并發(fā)控制、超時處理、錯誤恢復

二、Docling 是什么

2.1 官方介紹

Docling 是 IBM 研究院開源的文檔解析工具,專門用于將 PDF、Word、Excel 等文檔轉換為結構化 Markdown 和 JSON 格式。

技術報告Docling Technical Report (arXiv:2408.09869)

GitHub 倉庫:https://github.com/DS4SD/docling

核心特性

  • ? 支持 PDF、DOCX、XLSX、PPTX、Markdown、HTML 等多種格式
  • ? 內置 OCR 能力,識別圖片中的文字(30+ 語言)
  • ? 智能表格識別,還原表格結構(行列關系、合并單元格)
  • ? 圖片提取,支持 base64 嵌入或外部引用
  • ? 保持文檔層次結構(標題層級、段落順序)
  • ? 公式識別(LaTeX 格式)

2.2 解析效果對比

工具文字提取表格識別圖片提取OCR 能力結構保持
Apache Tika?????
PDFBox??? 一般?? 一般??? 一般
商業(yè) OCR?????? 一般
Docling?? 優(yōu)秀? base64? 內置? 完美

2.3 適用場景

推薦使用 Docling

  • ? 企業(yè)知識庫構建(RAG)
  • ? 合同/文檔結構化存儲
  • ? 技術手冊數(shù)字化
  • ? 財務報表提取
  • ? 學術論文解析

不推薦使用

  • ? 掃描件質量極差(建議專業(yè) OCR)
  • ? 手寫體文檔(Docling 主要識別印刷體)
  • ? 超大幅面圖紙(如 CAD 圖紙)

三、整體架構設計

3.1 系統(tǒng)架構圖

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│ 用戶上傳 PDF │ ──? │ DoclingClient│ ──? │ Docling-Serve│
└─────────────┘     └──────────────┘     └─────────────┘
                           │                      │
                           │ 異步任務              │ 解析文檔
                           │ (提交→輪詢→獲取)       │ (OCR+ 表格+ 圖片)
                           │                      │
                           ▼                      ▼
                    ┌──────────────┐     ┌─────────────┐
                    │ DoclingResult│ ?──│ JSON 結果   │
                    └──────────────┘     └─────────────┘
                           │
                           │ Markdown + 結構化元素 + 圖片
                           ▼
                    ┌──────────────┐     ┌─────────────┐
                    │ 分塊處理      │ ──? │ Embedding   │
                    └──────────────┘     └─────────────┘
                                                 │
                                                 ▼
                                          ┌─────────────┐
                                          │ 向量數(shù)據庫   │
                                          │ (Milvus/ES) │
                                          └─────────────┘

3.2 核心類圖

┌─────────────────────────────────────────┐
│           DoclingClient                 │
├─────────────────────────────────────────┤
│ - aiProp: AIProp                        │
│ - parseSemaphore: Semaphore             │
│ + convert(File): DoclingResult          │
│ + convert(File, Config): Result         │
│ - submitAsyncTask(): String             │
│ - pollTaskStatus(): String              │
│ - fetchTaskResult(): DoclingResult      │
│ - parseResponse(): DoclingResult        │
│ - resolveChildren(): void               │
│ - buildTableText(): String              │
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│           DoclingResult                 │
├─────────────────────────────────────────┤
│ - success: boolean                      │
│ - mdContent: String (Markdown 全文)     │
│ - elements: List<DoclingElement>       │
│ - images: List<DoclingImage>           │
│ - pageCount: Integer                    │
│ - tableCount: Integer                   │
│ - parseTime: Integer (毫秒)             │
└─────────────────────────────────────────┘

3.3 核心流程時序圖

用戶            Controller        Service        DoclingClient      Docling-Serve
 │                  │               │                 │                  │
 │──上傳 PDF──────?│               │                 │                  │
 │                  │               │                 │                  │
 │                  │──調用──────?│                 │                  │
 │                  │               │                 │                  │
 │                  │               │──convert()───?│                  │
 │                  │               │                 │                  │
 │                  │               │                 │──POST /async──?│ 提交任務
 │                  │               │                 │?───────────────│ 返回 taskId
 │                  │               │                 │                  │
 │                  │               │                 │──輪詢狀態(tài)────?│ 查詢進度
 │                  │               │                 │?───────────────│ pending/running
 │                  │               │                 │                  │
 │                  │               │                 │──輪詢狀態(tài)────?│ 查詢進度
 │                  │               │                 │?───────────────│ success
 │                  │               │                 │                  │
 │                  │               │                 │──GET /result──?│ 獲取結果
 │                  │               │                 │?───────────────│ JSON 數(shù)據
 │                  │               │                 │                  │
 │                  │               │?────────────────│ 返回 DoclingResult
 │                  │               │                 │                  │
 │                  │               │──分塊→向量化──?│                  │
 │                  │               │                 │                  │
 │?───────完成─────────────────────────────────────────────────────────│

四、快速開始(5 分鐘上手)

4.1 第一步:啟動 Docling 服務

# 拉取 Docker 鏡像
docker pull ds4sd/docling-serve:latest
# 啟動服務
docker run -d \
  -p 50080:50080 \
  --name docling \
  ds4sd/docling-serve:latest
# 驗證服務
curl http://localhost:50080/health
# 返回 {"status":"healthy"} 表示成功

4.2 第二步:添加 Maven 依賴

<dependencies>
    <!-- Spring Boot 核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    <!-- Hutool 工具類 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.32</version>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.36</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

4.3 第三步:配置 application.yml

docling:
  url: http://localhost:50080
  timeout: 300
  concurrency: 4

4.4 第四步:復制 DoclingClient 代碼

將本文第六部分的 DoclingClient.java 完整代碼復制到項目中。

4.5 第五步:調用解析

@Resource
private DoclingClient doclingClient;

// 解析 PDF
File pdfFile = new File("/path/to/document.pdf");
DoclingResult result = doclingClient.convert(pdfFile);

// 檢查結果
if (result.isSuccess()) {
    System.out.println("Markdown 內容:" + result.getMdContent());
    System.out.println("元素數(shù)量:" + result.getElementCount());
    System.out.println("表格數(shù)量:" + result.getTableCount());
    System.out.println("圖片數(shù)量:" + result.getImageCount());
} else {
    System.err.println("解析失?。? + result.getErrorMessage());
}

完成! 

五、環(huán)境搭建與配置

5.1 環(huán)境要求

軟件版本要求說明
JDK17+推薦 JDK 21(支持 Switch 表達式)
Spring Boot3.2+需要 Jakarta 注解支持
Hutool5.8+HTTP 客戶端和 JSON 處理
Lombok1.18+簡化 Getter/Setter
Docker20.10+部署 Docling 服務
Docling-Serve1.12.0+文檔解析服務

5.2 完整 Maven 依賴

<project>
    <dependencies>
        <!-- Spring Boot 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!-- Spring Boot Web(可選,用于 Controller) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!-- Hutool 工具類(HTTP 客戶端 + JSON 處理) -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.32</version>
        </dependency>
        <!-- Lombok 簡化代碼 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.36</version>
            <scope>provided</scope>
        </dependency>
        <!-- SLF4J 日志(Spring Boot 默認包含) -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <!-- 單元測試(可選) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

5.3 配置文件(application.yml)

# ==================== Docling 配置 ====================
docling:
  # Docling-Serve 服務地址
  # Docker 部署:http://localhost:50080
  # 遠程服務器:http://192.168.1.100:50080
  url: http://localhost:50080
  # 解析超時時間(秒)
  # 建議:
  # - 普通文檔(<50 頁):120 秒
  # - 中等文檔(50-200 頁):300 秒
  # - 大文檔(>200 頁):600 秒
  timeout: 300
  # 最大并發(fā)解析數(shù)
  # 說明:每個并發(fā)會占用一個 CPU 核心
  # 建議:
  # - 4 核 CPU:2-4
  # - 8 核 CPU:4-8
  # - 16 核 CPU:8-12
  # 注意:過高會導致 CPU/GPU 過載
  concurrency: 4
  # OCR 配置
  ocr:
    # 是否啟用 OCR
    enabled: true
    # OCR 語言列表
    # 支持:en, zh, zh-tw, ja, ko, de, fr, es, it, ru 等 30+ 語言
    languages:
      - zh
      - en
  # 表格識別配置
  table:
    # 是否啟用表格結構識別
    enabled: true
  # 圖片導出配置
  image:
    # 導出模式
    # - embedded:base64 嵌入 JSON(推薦)
    # - referenced:外部文件引用
    export-mode: embedded
  # PDF 解析后端配置
  pdf:
    # 后端選擇
    # - fitz:PyMuPDF(推薦,速度快,表格識別好)
    # - pypdf:純 Python 實現(xiàn)(兼容性好)
    backend: fitz
# ==================== 日志配置 ====================
logging:
  level:
    # DoclingClient 調試日志
    com.wt.admin.service.docling: DEBUG
    # Hutool HTTP 調試日志
    cn.hutool.http: DEBUG

5.4 配置屬性類(AIProp.java)

package com.wt.admin.config.prop;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * AI 相關配置屬性
 */
@Data
@Component
@ConfigurationProperties(prefix = "docling")
public class AIProp {
    
    /** Docling 服務配置 */
    private DoclingConfig docling = new DoclingConfig();
    
    @Data
    public static class DoclingConfig {
        /** 服務地址 */
        private String url = "http://localhost:50080";
        
        /** 超時時間(秒) */
        private int timeout = 300;
        
        /** 最大并發(fā)數(shù) */
        private int concurrency = 4;
        
        /** OCR 配置 */
        private OcrConfig ocr = new OcrConfig();
        
        /** 表格配置 */
        private TableConfig table = new TableConfig();
        
        /** 圖片配置 */
        private ImageConfig image = new ImageConfig();
        
        /** PDF 配置 */
        private PdfConfig pdf = new PdfConfig();
    }
    
    @Data
    public static class OcrConfig {
        private boolean enabled = true;
        private java.util.List<String> languages = java.util.List.of("zh", "en");
    }
    
    @Data
    public static class TableConfig {
        private boolean enabled = true;
    }
    
    @Data
    public static class ImageConfig {
        private String exportMode = "embedded";
    }
    
    @Data
    public static class PdfConfig {
        private String backend = "fitz";
    }
}

5.5 解析配置 DTO(ParseConfigDO.java)

package com.wt.admin.domain.model.ai;

import lombok.Data;
import java.util.List;

/**
 * 文檔解析配置對象
 */
@Data
public class ParseConfigDO {
    
    /**
     * Docling 解析參數(shù)配置
     */
    @Data
    public static class DoclingConfig {
        
        /**
         * 是否啟用 OCR 識別
         * 默認:true
         */
        private Boolean doOcr = true;
        
        /**
         * 是否識別表格結構
         * 默認:true
         */
        private Boolean doTableStructure = true;
        
        /**
         * 圖片導出模式
         * 可選值:embedded(base64 嵌入)、referenced(外部引用)
         * 默認:embedded
         */
        private String imageExportMode = "embedded";
        
        /**
         * PDF 解析后端
         * 可選值:fitz(PyMuPDF)、pypdf
         * 默認:fitz
         */
        private String pdfBackend = "fitz";
        
        /**
         * OCR 識別語言列表
         * 示例:["zh", "en"]
         */
        private List<String> ocrLang;
    }
}

六、DoclingClient 核心代碼逐行解析

6.1 完整代碼

package com.wt.admin.service.docling;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.wt.admin.config.prop.AIProp;
import com.wt.admin.domain.model.ai.ParseConfigDO;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;

/**
 * Docling 文檔解析客戶端
 * 負責與本地 Docling-Serve 進程通信,將文檔轉換為結構化數(shù)據
 * Docling 論文:Docling Technical Report, arXiv:2408.09869
 * 
 * @author MonkeyCode-AI
 * @version 1.0
 * @since 2026-05-15
 */
@Slf4j           // Lombok 注解:自動生成 log 日志對象
@Component       // Spring 注解:標記為 Bean,自動注入到其他類
public class DoclingClient {

    @Resource      // Spring 注解:自動注入配置屬性
    private AIProp aiProp;

    /** 
     * 輪詢間隔(毫秒)
     * 說明:每 3 秒向 Docling 服務查詢一次任務狀態(tài)
     */
    private static final int POLL_INTERVAL_MS = 3000;

    /** 
     * 控制 Docling 服務最大并發(fā)解析數(shù),防止 CPU/GPU 過載
     */
    private Semaphore parseSemaphore;

    /**
     * 初始化方法
     * Spring Bean 創(chuàng)建完成后自動調用
     */
    @PostConstruct
    public void init() {
        int concurrency = aiProp.getDocling().getConcurrency();
        parseSemaphore = new Semaphore(concurrency);
        log.debug("[Docling] 并發(fā)控制初始化,最大并發(fā)數(shù)={}", concurrency);
    }

    /**
     * 異步解析文檔(使用默認參數(shù))
     */
    public DoclingResult convert(File file) {
        return convert(file, null);
    }

    /**
     * 異步解析文檔,適配 Docling-Serve v1.12.0 的異步 API
     * 流程:提交異步任務 → 輪詢狀態(tài) → 獲取結果
     */
    public DoclingResult convert(File file, ParseConfigDO.DoclingConfig doclingConfig) {
        String baseUrl = aiProp.getDocling().getUrl();
        int timeoutSeconds = aiProp.getDocling().getTimeout();

        // 獲取信號量許可,控制并發(fā)度
        try {
            parseSemaphore.acquire();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return failResult("等待 Docling 解析許可時被中斷");
        }

        try {
            long start = System.currentTimeMillis();
            
            // 1. 提交異步任務
            String taskId = submitAsyncTask(baseUrl, file, timeoutSeconds, doclingConfig);
            if (taskId == null) {
                return failResult("提交 Docling 異步任務失敗");
            }
            log.info("[Docling] 開始解析,taskId={}, 排隊數(shù)={}", 
                     taskId, parseSemaphore.getQueueLength());

            // 2. 輪詢等待任務完成
            String taskStatus = pollTaskStatus(baseUrl, taskId, timeoutSeconds);
            if (!"success".equals(taskStatus)) {
                return failResult("Docling 任務執(zhí)行失敗,taskId=" + taskId + ",status=" + taskStatus);
            }

            // 3. 獲取任務結果
            return fetchTaskResult(baseUrl, taskId, System.currentTimeMillis() - start);
        } finally {
            parseSemaphore.release();
        }
    }

    /**
     * 提交異步轉換任務到 /v1/convert/file/async
     */
    private String submitAsyncTask(String baseUrl, File file, int timeoutSeconds, 
                                   ParseConfigDO.DoclingConfig config) {
        String url = baseUrl + "/v1/convert/file/async";
        try {
            HttpRequest request = HttpRequest.post(url)
                    .form("files", file)
                    .form("to_formats", "md")
                    .form("to_formats", "json")
                    .timeout(timeoutSeconds * 1000);
            applyDoclingConfig(request, config);
            HttpResponse response = request.execute();
            if (!response.isOk()) {
                log.error("Docling 異步任務提交失敗,HTTP 狀態(tài)碼:{},響應:{}", 
                         response.getStatus(), response.body());
                return null;
            }
            JSONObject body = JSONUtil.parseObj(response.body());
            return body.getStr("task_id");
        } catch (Exception e) {
            log.error("Docling 異步任務提交異常:{}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * 將 DoclingConfig 參數(shù)應用到 HTTP 請求
     */
    private void applyDoclingConfig(HttpRequest request, ParseConfigDO.DoclingConfig config) {
        boolean doOcr = config != null ? Boolean.TRUE.equals(config.getDoOcr()) : true;
        boolean doTable = config != null ? Boolean.TRUE.equals(config.getDoTableStructure()) : true;
        String imageMode = config != null && StrUtil.isNotBlank(config.getImageExportMode()) 
                         ? config.getImageExportMode() 
                         : "embedded";
        request.form("do_ocr", String.valueOf(doOcr));
        request.form("do_table_structure", String.valueOf(doTable));
        request.form("image_export_mode", imageMode);
        applyOptionalDoclingParams(request, config);
    }

    private void applyOptionalDoclingParams(HttpRequest request, 
                                           ParseConfigDO.DoclingConfig config) {
        if (config == null) {
            return;
        }
        if (StrUtil.isNotBlank(config.getPdfBackend())) {
            request.form("pdf_backend", config.getPdfBackend());
        }
        if (CollUtil.isNotEmpty(config.getOcrLang())) {
            config.getOcrLang().forEach(lang -> request.form("ocr_lang", lang));
        }
    }

    /**
     * 輪詢任務狀態(tài) /v1/status/poll/{taskId}
     */
    private String pollTaskStatus(String baseUrl, String taskId, int timeoutSeconds) {
        String url = baseUrl + "/v1/status/poll/" + taskId;
        long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

        while (System.currentTimeMillis() < deadline) {
            try {
                Thread.sleep(POLL_INTERVAL_MS);
                HttpResponse response = HttpRequest.get(url).timeout(10000).execute();
                if (!response.isOk()) {
                    log.warn("Docling 輪詢狀態(tài)異常,HTTP 狀態(tài)碼:{}", response.getStatus());
                    continue;
                }
                JSONObject body = JSONUtil.parseObj(response.body());
                String status = body.getStr("task_status");
                log.debug("Docling 任務輪詢,taskId={},status={}", taskId, status);

                if ("success".equals(status) || "failure".equals(status)) {
                    return status;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "interrupted";
            } catch (Exception e) {
                log.warn("Docling 輪詢異常:{}", e.getMessage());
            }
        }
        log.error("Docling 任務超時,taskId={},超時時間={}秒", taskId, timeoutSeconds);
        return "timeout";
    }

    /**
     * 獲取已完成任務的結果 /v1/result/{taskId}
     */
    private DoclingResult fetchTaskResult(String baseUrl, String taskId, long costMs) {
        String url = baseUrl + "/v1/result/" + taskId;
        try {
            HttpResponse response = HttpRequest.get(url).timeout(30000).execute();
            if (!response.isOk()) {
                log.error("Docling 獲取結果失敗,HTTP 狀態(tài)碼:{},響應:{}", 
                         response.getStatus(), response.body());
                return failResult("Docling 獲取結果失?。? + response.getStatus());
            }
            return parseResponse(response.body(), costMs);
        } catch (Exception e) {
            log.error("Docling 獲取結果異常:{}", e.getMessage(), e);
            return failResult("Docling 獲取結果異常:" + e.getMessage());
        }
    }

    /**
     * 構建失敗結果
     */
    private DoclingResult failResult(String message) {
        DoclingResult result = new DoclingResult();
        result.setSuccess(false);
        result.setErrorMessage(message);
        return result;
    }

    /**
     * 解析 Docling 返回的 JSON
     */
    private DoclingResult parseResponse(String json, long costMs) {
        DoclingResult result = new DoclingResult();
        result.setSuccess(true);
        result.setParseTime((int) costMs);

        JSONObject root = JSONUtil.parseObj(json);
        JSONObject document = root.getJSONObject("document");
        if (document == null) {
            result.setSuccess(false);
            result.setErrorMessage("Docling 返回數(shù)據中無 document 字段");
            return result;
        }

        result.setMdContent(document.getStr("md_content", ""));

        JSONObject jsonContent = document.getJSONObject("json_content");
        if (jsonContent != null) {
            parseJsonContent(jsonContent, result);
            return result;
        }

        parseLegacyElements(document, result);
        return result;
    }

    /**
     * 從 json_content 提取結構化元素和圖片
     */
    private void parseJsonContent(JSONObject jsonContent, DoclingResult result) {
        JSONObject pages = jsonContent.getJSONObject("pages");
        if (pages != null) {
            result.setPageCount(pages.size());
        }

        JSONArray texts = jsonContent.getJSONArray("texts");
        JSONArray pictures = jsonContent.getJSONArray("pictures");
        JSONArray tables = jsonContent.getJSONArray("tables");
        JSONArray groups = jsonContent.getJSONArray("groups");

        List<DoclingElement> elementList = new ArrayList<>();
        List<DoclingImage> imageList = new ArrayList<>();
        int[] tableCount = {0};
        int[] index = {0};

        JSONObject body = jsonContent.getJSONObject("body");
        JSONArray children = body != null ? body.getJSONArray("children") : null;
        if (children == null) {
            return;
        }

        Set<Integer> pageImageCollected = new HashSet<>();
        resolveChildren(children, texts, pictures, tables, groups, pages,
                elementList, imageList, tableCount, index, pageImageCollected);

        result.setElements(elementList);
        result.setImages(imageList);
        result.setElementCount(elementList.size());
        result.setTableCount(tableCount[0]);
        result.setImageCount(imageList.size());
        
        JSONObject origin = jsonContent.getJSONObject("origin");
        result.setDocMetadata(origin != null ? origin.toString() : null);
    }

    /**
     * 遞歸解析 children 中的 $ref 引用
     */
    private void resolveChildren(JSONArray children, JSONArray texts, JSONArray pictures,
                                 JSONArray tables, JSONArray groups, JSONObject pages,
                                 List<DoclingElement> elementList, List<DoclingImage> imageList,
                                 int[] tableCount, int[] index, Set<Integer> pageImageCollected) {
        for (int i = 0; i < children.size(); i++) {
            JSONObject childRef = children.getJSONObject(i);
            String ref = childRef.getStr("$ref");
            if (StrUtil.isBlank(ref)) {
                continue;
            }

            String[] parts = ref.replace("#/", "").split("/");
            if (parts.length != 2) {
                continue;
            }
            String category = parts[0];
            int refIndex;
            try {
                refIndex = Integer.parseInt(parts[1]);
            } catch (NumberFormatException e) {
                continue;
            }

            if ("groups".equals(category) && groups != null && refIndex < groups.size()) {
                JSONObject groupObj = groups.getJSONObject(refIndex);
                JSONArray groupChildren = groupObj.getJSONArray("children");
                if (groupChildren != null) {
                    resolveChildren(groupChildren, texts, pictures, tables, groups, pages,
                            elementList, imageList, tableCount, index, pageImageCollected);
                }
                continue;
            }

            if ("texts".equals(category) && texts != null && refIndex < texts.size()) {
                JSONObject textObj = texts.getJSONObject(refIndex);
                String label = textObj.getStr("label", "text");
                String text = textObj.getStr("text", "");
                String type = mapLabelToType(label);
                DoclingElement element = new DoclingElement();
                element.setIndex(index[0]++);
                element.setType(type);
                element.setText(text);
                element.setPage(extractPage(textObj));
                element.setLevel(textObj.getInt("level", "title".equals(label) ? 1 : 0));
                elementList.add(element);
                continue;
            }

            if ("tables".equals(category) && tables != null && refIndex < tables.size()) {
                JSONObject tableObj = tables.getJSONObject(refIndex);
                String tableText = buildTableText(tableObj);
                DoclingElement element = new DoclingElement();
                element.setIndex(index[0]++);
                element.setType("table");
                element.setText(tableText);
                element.setPage(extractPage(tableObj));
                element.setLevel(0);
                elementList.add(element);
                tableCount[0]++;
                continue;
            }

            if ("pictures".equals(category) && pictures != null && refIndex < pictures.size()) {
                JSONObject picObj = pictures.getJSONObject(refIndex);
                int page = extractPage(picObj);
                String caption = extractCaption(picObj);

                DoclingElement element = new DoclingElement();
                element.setIndex(index[0]++);
                element.setType("picture");
                element.setText(StrUtil.isNotBlank(caption) ? caption : "[image]");
                element.setPage(page);
                element.setLevel(0);
                elementList.add(element);

                JSONObject picImage = picObj.getJSONObject("image");
                boolean hasSelfImage = picImage != null && StrUtil.isNotBlank(picImage.getStr("uri", ""));
                if (hasSelfImage) {
                    String base64 = stripBase64Prefix(picImage.getStr("uri", ""));
                    DoclingImage img = new DoclingImage();
                    img.setBase64(base64);
                    img.setCaption(caption);
                    img.setPage(page);
                    img.setOcrText("");
                    imageList.add(img);
                } else if (pages != null && pageImageCollected.add(page)) {
                    String base64 = extractPageImage(pages, page);
                    if (StrUtil.isNotBlank(base64)) {
                        DoclingImage img = new DoclingImage();
                        img.setBase64(base64);
                        img.setCaption("第" + page + "頁截圖");
                        img.setPage(page);
                        img.setOcrText("");
                        imageList.add(img);
                    }
                }
            }
        }
    }

    /**
     * 提取圖片的 base64 數(shù)據
     */
    private String extractPictureBase64(JSONObject picObj, JSONObject pages, int page) {
        JSONObject imageData = picObj.getJSONObject("image");
        if (imageData != null) {
            String uri = imageData.getStr("uri", "");
            if (StrUtil.isNotBlank(uri)) {
                return stripBase64Prefix(uri);
            }
        }
        if (pages != null) {
            JSONObject pageObj = pages.getJSONObject(String.valueOf(page));
            if (pageObj != null) {
                JSONObject pageImage = pageObj.getJSONObject("image");
                if (pageImage != null) {
                    String uri = pageImage.getStr("uri", "");
                    if (StrUtil.isNotBlank(uri)) {
                        return stripBase64Prefix(uri);
                    }
                }
            }
        }
        return "";
    }

    /**
     * 從 pages 提取整頁截圖的 base64
     */
    private String extractPageImage(JSONObject pages, int page) {
        JSONObject pageObj = pages.getJSONObject(String.valueOf(page));
        if (pageObj == null) {
            return "";
        }
        JSONObject pageImage = pageObj.getJSONObject("image");
        if (pageImage == null) {
            return "";
        }
        String uri = pageImage.getStr("uri", "");
        return StrUtil.isNotBlank(uri) ? stripBase64Prefix(uri) : "";
    }

    /**
     * 去掉 base64 前綴
     */
    private String stripBase64Prefix(String uri) {
        if (uri.contains(",")) {
            return uri.substring(uri.indexOf(",") + 1);
        }
        return uri;
    }

    /**
     * 將 Docling label 映射為統(tǒng)一的元素類型
     */
    private String mapLabelToType(String label) {
        return switch (label) {
            case "title", "section_header" -> "title";
            case "list_item" -> "list";
            case "caption" -> "paragraph";
            default -> "paragraph";
        };
    }

    /**
     * 從元素的 prov 字段提取頁碼
     */
    private int extractPage(JSONObject element) {
        JSONArray prov = element.getJSONArray("prov");
        if (prov != null && !prov.isEmpty()) {
            JSONObject firstProv = prov.getJSONObject(0);
            return firstProv.getInt("page_no", 1);
        }
        return 1;
    }

    /**
     * 從圖片/表格元素的 captions 字段提取標題文本
     */
    private String extractCaption(JSONObject element) {
        JSONArray captions = element.getJSONArray("captions");
        if (captions == null || captions.isEmpty()) {
            return "";
        }
        JSONObject firstCaption = captions.getJSONObject(0);
        if (firstCaption == null) {
            return "";
        }
        String text = firstCaption.getStr("text");
        if (StrUtil.isNotBlank(text)) {
            return text;
        }
        return "";
    }

    /**
     * 將表格數(shù)據轉換為 Markdown 表格文本
     */
    private String buildTableText(JSONObject tableObj) {
        JSONObject data = tableObj.getJSONObject("data");
        if (data == null) {
            return "[table]";
        }
        JSONArray cells = data.getJSONArray("table_cells");
        if (cells == null || cells.isEmpty()) {
            return "[table]";
        }
        int numCols = data.getInt("num_cols", 0);
        int numRows = data.getInt("num_rows", 0);
        if (numCols == 0 || numRows == 0) {
            return "[table]";
        }

        String[][] grid = new String[numRows][numCols];
        for (int i = 0; i < cells.size(); i++) {
            JSONObject cell = cells.getJSONObject(i);
            int row = cell.getInt("start_row_offset_idx", 0);
            int col = cell.getInt("start_col_offset_idx", 0);
            if (row < numRows && col < numCols) {
                grid[row][col] = cell.getStr("text", "");
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int r = 0; r < numRows; r++) {
            sb.append("|");
            for (int c = 0; c < numCols; c++) {
                sb.append(" ").append(grid[r][c] != null ? grid[r][c] : "").append(" |");
            }
            sb.append("\n");
            if (r == 0) {
                sb.append("|");
                for (int c = 0; c < numCols; c++) {
                    sb.append("---|");
                }
                sb.append("\n");
            }
        }
        return sb.toString().trim();
    }

    /**
     * 從舊版 Docling 的 elements 字段提取結構化數(shù)據
     */
    private void parseLegacyElements(JSONObject document, DoclingResult result) {
        JSONObject metadata = document.getJSONObject("metadata");
        if (metadata != null) {
            result.setPageCount(metadata.getInt("page_count", 0));
            result.setDocMetadata(metadata.toString());
        }

        JSONArray elements = document.getJSONArray("elements");
        if (elements == null) {
            return;
        }

        List<DoclingElement> elementList = new ArrayList<>();
        List<DoclingImage> imageList = new ArrayList<>();
        int tableCount = 0;

        for (int i = 0; i < elements.size(); i++) {
            JSONObject el = elements.getJSONObject(i);
            String type = el.getStr("type", "paragraph");
            String text = el.getStr("text", "");

            DoclingElement element = new DoclingElement();
            element.setIndex(i);
            element.setType(type);
            element.setText(text);
            element.setPage(el.getInt("page", 1));
            element.setLevel(el.getInt("level", 0));
            elementList.add(element);

            if ("table".equals(type)) {
                tableCount++;
            }

            if ("picture".equals(type) && StrUtil.isNotBlank(el.getStr("image"))) {
                DoclingImage image = new DoclingImage();
                image.setBase64(el.getStr("image"));
                image.setCaption(el.getStr("caption", ""));
                image.setPage(el.getInt("page", 1));
                image.setOcrText(el.getStr("ocr_text", ""));
                imageList.add(image);
            }
        }

        result.setElements(elementList);
        result.setImages(imageList);
        result.setElementCount(elementList.size());
        result.setTableCount(tableCount);
        result.setImageCount(imageList.size());
    }

    /**
     * 檢查 Docling 服務是否可用
     */
    public boolean isAvailable() {
        try {
            HttpResponse response = HttpRequest.get(aiProp.getDocling().getUrl() + "/health")
                    .timeout(3000)
                    .execute();
            return response.isOk();
        } catch (Exception e) {
            log.warn("Docling 服務不可用:{}", e.getMessage());
            return false;
        }
    }

    // ========== 內部數(shù)據結構 ==========

    @Data
    public static class DoclingResult {
        private boolean success;
        private String errorMessage;
        private String mdContent;
        private String docMetadata;
        private Integer pageCount;
        private Integer elementCount;
        private Integer tableCount;
        private Integer imageCount;
        private Integer parseTime;
        private List<DoclingElement> elements;
        private List<DoclingImage> images;
    }

    @Data
    public static class DoclingElement {
        private Integer index;
        /** 元素類型:title/paragraph/table/list/picture/formula */
        private String type;
        private String text;
        private Integer page;
        /** 標題層級,僅 type=title 時有效 */
        private Integer level;
    }

    @Data
    public static class DoclingImage {
        private String base64;
        private String caption;
        private Integer page;
        private String ocrText;
    }
}

七、請求參數(shù)詳解

7.1 HTTP 請求格式

POST http://localhost:50080/v1/convert/file/async
Content-Type: multipart/form-data
files: <PDF 文件>
to_formats: "md"
to_formats: "json"
do_ocr: "true"
do_table_structure: "true"
image_export_mode: "embedded"
pdf_backend: "fitz"
ocr_lang: "en"
ocr_lang: "zh"

7.2 參數(shù)說明表

參數(shù)名類型必填默認值說明
filesFile?-待解析的文件
to_formatsString?-輸出格式:md、json
do_ocrBoolean?true是否啟用 OCR
do_table_structureBoolean?true是否識別表格
image_export_modeString?"embedded"圖片導出模式
pdf_backendString?-PDF 后端:fitz/pypdf
ocr_langString[]?-OCR 語言

7.3 Java 調用示例

// 示例 1:默認參數(shù)
DoclingResult result = doclingClient.convert(new File("document.pdf"));
// 示例 2:自定義參數(shù)
ParseConfigDO.DoclingConfig config = new ParseConfigDO.DoclingConfig();
config.setDoOcr(true);
config.setDoTableStructure(true);
config.setImageExportMode("embedded");
config.setPdfBackend("fitz");
config.setOcrLang(List.of("en", "zh"));
DoclingResult result = doclingClient.convert(new File("document.pdf"), config);

八、返回數(shù)據格式詳解

8.1 DoclingResult 結構

public class DoclingResult {
    private boolean success;
    private String errorMessage;
    private String mdContent;          // Markdown 全文
    private String docMetadata;        // 文檔元數(shù)據
    private Integer pageCount;         // 頁數(shù)
    private Integer elementCount;      // 元素總數(shù)
    private Integer tableCount;        // 表格數(shù)量
    private Integer imageCount;        // 圖片數(shù)量
    private Integer parseTime;         // 解析耗時(毫秒)
    private List<DoclingElement> elements;
    private List<DoclingImage> images;
}

8.2 實際返回示例

{
  "success": true,
  "mdContent": "# 項目報告\n\n## 第一章 概述\n\n...",
  "pageCount": 5,
  "elementCount": 28,
  "tableCount": 3,
  "imageCount": 2,
  "parseTime": 4532,
  "elements": [
    {
      "index": 0,
      "type": "title",
      "text": "項目報告",
      "page": 1,
      "level": 1
    },
    {
      "index": 1,
      "type": "table",
      "text": "| 姓名 | 年齡 |\n|------|------|\n| 張三 | 25 |",
      "page": 2,
      "level": 0
    }
  ],
  "images": [
    {
      "base64": "iVBORw0KGgoAAAANSUhEUgAAAA...",
      "caption": "銷售趨勢",
      "page": 3,
      "ocrText": ""
    }
  ]
}

九、如何集成到向量存儲

9.1 完整流程

Docling 解析 → 按元素分塊 → Embedding → 向量庫存儲

9.2 代碼實現(xiàn)

@Service
public class ESVectorImpl implements VectorApi {
    
    @Resource
    private DoclingClient doclingClient;
    
    @Resource
    private ElasticsearchVectorStore vectorStore;
    
    public void add(File pdfFile, Integer modelId, String fileId) {
        // 1. 解析文檔
        DoclingResult result = doclingClient.convert(pdfFile);
        
        if (!result.isSuccess()) {
            throw new RuntimeException("解析失?。? + result.getErrorMessage());
        }
        
        // 2. 按元素分塊
        List<Document> chunks = new ArrayList<>();
        for (DoclingElement element : result.getElements()) {
            if (StrUtil.isBlank(element.getText())) {
                continue;
            }
            
            Document chunk = new Document();
            chunk.setContent(element.getText());
            chunk.getMetadata().put("modelId", modelId);
            chunk.getMetadata().put("fileId", fileId);
            chunk.getMetadata().put("page", element.getPage());
            chunk.getMetadata().put("type", element.getType());
            
            chunks.add(chunk);
        }
        
        // 3. 存儲到向量庫
        vectorStore.add(chunks);
    }
}

十、生產級優(yōu)化方案

10.1 異步任務處理

@Service
public class DocumentProcessService {
    
    @Async
    public void processAsync(File file, Integer knowledgeId) {
        try {
            DoclingResult result = doclingClient.convert(file);
            // 處理結果...
        } catch (Exception e) {
            log.error("解析失敗", e);
        }
    }
}

10.2 并發(fā)控制優(yōu)化

// 根據 CPU 核心數(shù)動態(tài)設置
int cpuCores = Runtime.getRuntime().availableProcessors();
int concurrency = Math.max(1, cpuCores / 2);
parseSemaphore = new Semaphore(concurrency);

10.3 大文件拆分

// 超過 100 頁的 PDF 拆分處理
if (totalPages > 100) {
    int batchSize = 20;
    for (int i = 0; i < totalPages; i += batchSize) {
        // 拆分并分批解析
    }
}

十一、完整調用示例

11.1 Controller 層

@RestController
@RequestMapping("/knowledge")
public class KnowledgeController {
    
    @Resource
    private KnowledgeService knowledgeService;
    
    @PostMapping("/upload")
    public Result<Integer> upload(
        @RequestParam("file") MultipartFile file,
        @RequestParam("knowledgeId") Integer knowledgeId
    ) {
        File tempFile = FileUtil.convertMultipartFileToFile(file);
        try {
            Integer fileId = knowledgeService.processDocument(tempFile, knowledgeId);
            return Result.success(fileId);
        } finally {
            FileUtil.del(tempFile);
        }
    }
}

11.2 Service 層

@Service
public class KnowledgeService {
    
    @Resource
    private DoclingClient doclingClient;
    
    @Resource
    private VectorApi vectorApi;
    
    public Integer processDocument(File file, Integer knowledgeId) {
        // 解析
        DoclingResult result = doclingClient.convert(file);
        
        // 保存記錄
        KnowledgeFileEntity entity = new KnowledgeFileEntity();
        entity.setKnowledgeId(knowledgeId);
        entity.setFileName(file.getName());
        entity.setPageCount(result.getPageCount());
        knowledgeMapper.insert(entity);
        
        // 向量化
        vectorApi.add(file, knowledgeId, entity.getId().toString());
        
        return entity.getId();
    }
}

十二、測試用例

@SpringBootTest
class DoclingClientTest {

    @Autowired
    private DoclingClient doclingClient;

    @Test
    void testConvert() {
        File pdfFile = new File("src/test/resources/sample.pdf");
        DoclingResult result = doclingClient.convert(pdfFile);
        
        assertTrue(result.isSuccess());
        assertNotNull(result.getMdContent());
        assertTrue(result.getPageCount() > 0);
    }
}

十三、常見問題 FAQ

Q1:Docling 服務如何部署?

docker pull ds4sd/docling-serve:latest
docker run -d -p 50080:50080 --name docling ds4sd/docling-serve:latest

Q2:解析大文件內存溢出怎么辦?

  1. 降低并發(fā)數(shù)
  2. 增加 JVM 堆內存 -Xmx4g
  3. 拆分大文件分批處理

Q3:表格識別效果不好?

config.setDoTableStructure(true);
config.setPdfBackend("fitz");  // 使用 PyMuPDF
config.setDoOcr(true);         // 啟用 OCR

Q4:如何監(jiān)控進度?

@GetMapping("/task/{taskId}")
public Result<TaskProgressVO> getProgress(@PathVariable String taskId) {
    String url = doclingUrl + "/v1/status/poll/" + taskId;
    // 輪詢獲取進度
}

十四、總結

14.1 核心要點

知識點關鍵內容
Docling 定位企業(yè)級文檔解析引擎
核心能力文字 + 表格 + 圖片 + OCR
請求方式異步任務(提交→輪詢→獲?。?/td>
返回數(shù)據Markdown + 結構化元素 + 圖片
并發(fā)控制Semaphore 信號量
向量集成解析→分塊→Embedding→存儲

14.2 最佳實踐

推薦

  1. 異步處理大文檔
  2. 合理設置并發(fā)數(shù)
  3. 按元素分塊
  4. 添加元數(shù)據
  5. 錯誤重試機制

避免

  1. 同步處理大文檔
  2. 并發(fā)數(shù)過高
  3. 整個文檔一個 chunk
  4. 忽略圖片數(shù)據

以上就是SpringBoot集成Docling文檔解析服務的完全指南的詳細內容,更多關于SpringBoot Docling文檔解析服務的資料請關注腳本之家其它相關文章!

相關文章

  • maven異常Invalid?bound?statement(not?found)的問題解決

    maven異常Invalid?bound?statement(not?found)的問題解決

    本文詳細介紹了Maven項目中常見的Invalidboundstatement異常及其解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-12-12
  • Java如何使用Optional與Stream取代if判空邏輯(JDK8以上)

    Java如何使用Optional與Stream取代if判空邏輯(JDK8以上)

    這篇文章主要給大家介紹了關于Java如何使用Optional與Stream取代if判空邏輯(JDK8以上)的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Java具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-09-09
  • Java如何獲取當前年份、月份和日期字符串

    Java如何獲取當前年份、月份和日期字符串

    Java獲取當前年份、月份和日期是通過Calendar類的實例對象來獲取的,本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧
    2024-03-03
  • springboot在idea下debug調試熱部署問題

    springboot在idea下debug調試熱部署問題

    這篇文章主要介紹了springboot在idea下debug調試熱部署問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • java實現(xiàn)的2048游戲完整實例

    java實現(xiàn)的2048游戲完整實例

    這篇文章主要介紹了java實現(xiàn)的2048游戲,結合完整實例形式分析了java實現(xiàn)2048游戲功能的相關數(shù)值運算、swing組件布局、事件響應等相關操作技巧,需要的朋友可以參考下
    2018-01-01
  • java中的 toString()方法實例代碼

    java中的 toString()方法實例代碼

    toString()方法 相信大家都用到過,一般用于以字符串的形式返回對象的相關數(shù)據。這篇文章主要介紹了java中的 toString()方法,需要的朋友可以參考下
    2017-05-05
  • Java線程池隊列LinkedTransferQueue示例詳解

    Java線程池隊列LinkedTransferQueue示例詳解

    這篇文章主要為大家介紹了Java線程池隊列LinkedTransferQueue示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • IDEA中JetBrains Mono字體的正確安裝姿勢

    IDEA中JetBrains Mono字體的正確安裝姿勢

    在 JetBrains Mono 的設計階段,它就充分考慮到了長時間工作可能導致的眼睛疲勞問題,比如字母的大小和形狀、空間量、自然等寬平衡、不必要的細節(jié)、連字、以及難以區(qū)分的符號等,從而最終設計出了這么一款字體
    2021-06-06
  • Sharding-Jdbc如何配置主從讀寫分離模式

    Sharding-Jdbc如何配置主從讀寫分離模式

    文章介紹了如何使用Sharding-JDBC實現(xiàn)MySQL的讀寫分離,通過配置主從數(shù)據源和讀寫分離策略,可以在項目中實現(xiàn)高效的數(shù)據庫讀寫操作,同時,文章還提到解決MySQL連接異常的一個常見方法
    2025-03-03
  • Java實現(xiàn)多線程輪流打印1-100的數(shù)字操作

    Java實現(xiàn)多線程輪流打印1-100的數(shù)字操作

    這篇文章主要介紹了Java實現(xiàn)多線程輪流打印1-100的數(shù)字操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-08-08

最新評論

稷山县| 通化市| 格尔木市| 伊金霍洛旗| 垫江县| 衡水市| 桦南县| 双桥区| 高阳县| 福清市| 韩城市| 通州区| 鹿邑县| 凤凰县| 寿光市| 眉山市| 合江县| 美姑县| 图们市| 临邑县| 盖州市| 琼中| 渭源县| 汤原县| 海盐县| 乌兰浩特市| 三门县| 温泉县| 赞皇县| 苍山县| 阜宁县| 彭州市| 定西市| 固原市| 淮安市| 呼玛县| 茌平县| 贵州省| 喜德县| 阜城县| 柘城县|