PDF-OCR文件识别篇(四):百度 OCR 模块文档解析

本章是 baiduocr 模块的全部。它做两件事:先鉴权拿 Access Token,再用 Token 调文档解析接口 。对外只暴露一个同步 parse(bytes, name),把鉴权和轮询全藏在里面。

4.1 鉴权:用 API Key / Secret 换 Access Token

百度文档解析走 OAuth2 client_credentials 模式,参考官方《鉴权认证机制》。

配置类 BaiduOcrProperties(前缀 pdf.baidu.ocr

java 复制代码
@Data @Component
@ConfigurationProperties(prefix = "pdf.baidu.ocr")
public class BaiduOcrProperties {
    private String apiKey;          // 鉴权 client_id
    private String secretKey;       // 鉴权 client_secret
    private String tokenUrl = "https://aip.baidubce.com/oauth/2.0/token";
    private String docParseTaskUrl  = ".../rest/2.0/brain/online/v2/parser/task";
    private String docParseQueryUrl = ".../rest/2.0/brain/online/v2/parser/task/query";
    private long docParsePollIntervalMs = 5000L;  // 轮询间隔
    private int  docParseMaxAttempts    = 40;     // 轮询上限
    private long expireBuffer = 86400L;           // Token 提前刷新安全间隔(秒)
}

所有接口地址、轮询参数都外置成配置,切环境不改代码。

响应体 BaiduTokenResponse

用 FastJSON2 @JSONField 做下划线映射,一个类同时容纳成功字段与错误字段:

java 复制代码
@JSONField(name = "access_token") private String accessToken;
@JSONField(name = "expires_in")   private Long   expiresIn;   // 约 2592000 秒(30天)
private String error;                                          // 失败时返回
@JSONField(name = "error_description") private String errorDescription;

鉴权服务 BaiduOcrTokenServiceImpl

接口暴露两个方法:getAccessToken()(优先读缓存)、refreshAccessToken()(强制刷新)。

java 复制代码
@Override
public String getAccessToken() {
    String cached = redisCache.getCacheObject(ACCESS_TOKEN_CACHE_KEY);
    if (StringUtils.isNotEmpty(cached)) return cached;   // 命中缓存直接返回
    return refreshAccessToken();
}

@Override
public String refreshAccessToken() {
    BaiduTokenResponse response = requestAccessToken();
    String accessToken = response.getAccessToken();
    // 缓存有效期 = 百度返回有效期 - 安全间隔,避免临界使用到失效 Token
    long expire = response.getExpiresIn() - baiduOcrProperties.getExpireBuffer();
    if (expire <= 0) expire = response.getExpiresIn();
    redisCache.setCacheObject(ACCESS_TOKEN_CACHE_KEY, accessToken, (int) expire, TimeUnit.SECONDS);
    return accessToken;
}

requestAccessToken() 用 Hutool POST 表单换 Token,并做三层校验:

java 复制代码
Map<String,Object> params = Map.of(
    "grant_type", "client_credentials", "client_id", apiKey, "client_secret", secretKey);
String body = HttpRequest.post(tokenUrl).form(params).timeout(10000).execute().body();
BaiduTokenResponse response = JSONObject.parseObject(body, BaiduTokenResponse.class);
if (response == null)     throw ServiceException("响应为空");
if (StringUtils.isNotEmpty(response.getError()))   throw ServiceException("鉴权失败:" + errorDescription);
if (无 access_token 或 expiresIn 为空)      throw ServiceException("未返回有效 token");

设计精华------「提前过期」缓存策略。 百度 Token 有效期约 30 天,缓存有效期设为 30天 - expireBuffer(默认1天)。缓存先于真实 Token 失效,下一次取用自然触发刷新,永远不会把「马上失效」的 Token 用到接口上。它和下文「失效自动重试」组成双保险。

4.2 文档解析:异步任务式接口

百度「文档解析」是异步任务:提交拿 task_id → 轮询查状态 → 成功后下载结果 URL 。封装在 BaiduDocParseClientImpl

提交任务 submitParse

文件转 Base64 作为表单字段提交:

java 复制代码
Map<String,Object> form = new HashMap<>();
form.put("file_data", Base64.encode(fileBytes));
form.put("file_name", fileName);
JSONObject json = callApi(docParseTaskUrl, form, SUBMIT_TIMEOUT, "提交文档解析任务");
checkError(json, "提交文档解析任务");
String taskId = extractTaskId(json);   // 兼容 task_id 在顶层或 result 节点下

同步解析 parse:提交 + 轮询 + 下载

java 复制代码
String taskId = submitParse(fileBytes, fileName);
for (int i = 1; i <= maxAttempts; i++) {
    Thread.sleep(interval);                       // 官方建议 5~10s
    JSONObject result = queryResult(taskId);
    String status = lower(str(result, "status", "task_status"));
    if (status ∈ {success, succeeded, finished}) {
        ret.setJsonText(download(str(result, "parse_result_url", "result_url", "json_url")));
        String mdUrl = str(result, "markdown_url", "md_url");
        if (mdUrl != null) ret.setMarkdownText(download(mdUrl));
        return ret;                               // 同时拿到 JSON + Markdown 两份
    }
    if (status ∈ {failed, fail, error}) throw ServiceException("任务失败:" + ...);
    // pending / running → 继续轮询
}
throw ServiceException("文档解析超时");

返回的 BaiduDocParseResultjsonText(结构化)与 markdownText(人读友好)两份,taskIdduration 备查。

4.3 健壮性三件套

1)Token 失效自动刷新重试。 所有请求经 callApi 包一层,命中 error_code == 110/111(Token 过期/无效)时刷新 Token 重发一次:

java 复制代码
private JSONObject callApi(String baseUrl, Map<String,Object> form, int timeout, String action) {
    JSONObject json = doPost(baseUrl, form, timeout, action);
    Integer code = json.getInteger("error_code");
    if (code != null && (code == 110 || code == 111)) {
        baiduOcrTokenService.refreshAccessToken();     // 强刷
        json = doPost(baseUrl, form, timeout, action);  // 重试一次
    }
    return json;
}

2)字段多别名兼容。 str(obj, "a", "b", "c") 依次取首个非空键,吸收百度不同接口版本的字段差异(parse_result_url / result_url / json_url...)。

3)分级超时。 提交 60s(上传大文件慢)、查询 30s、下载 60s,互不影响。

4.4 小结

baiduocr 把「鉴权 + 异步任务轮询 + 容错」全封装在两个 Service 里,对上层只给一个 parse(bytes, name)。上层(第 7 章的 ocrParse)只管把 PDF 字节丢进去,拿回 JSON/Markdown 文本,完全不感知 Token 与轮询。

关键文件

  • config/BaiduOcrProperties.java --- 配置
  • constant/BaiduOcrConstants.java --- 缓存 key / grant_type
  • domain/BaiduTokenResponse.javadomain/BaiduDocParseResult.java --- 响应体
  • service/impl/BaiduOcrTokenServiceImpl.java --- Access Token 鉴权与缓存
  • service/impl/BaiduDocParseClientImpl.java --- 文档解析(提交/轮询/下载/容错)