構(gòu)化文檔智能解析接口選型與工程化落地)
近幾年做票據(jù)、合同、表單這類非結(jié)構(gòu)化文檔入庫(kù)我前后對(duì)接過三四套文檔智能解析接口其中一套用得最久、踩坑也最密。這類接口的核心價(jià)值很直白把掃描件、拍照件、PDF 里那些人眼一看就懂、程序一碰就廢的內(nèi)容轉(zhuǎn)成帶坐標(biāo)、帶層級(jí)、帶語義的結(jié)構(gòu)化數(shù)據(jù)。它解決的不是能不能認(rèn)字的問題而是認(rèn)出來的字能不能直接喂給業(yè)務(wù)系統(tǒng)的問題——閱讀順序、表格結(jié)構(gòu)、段落歸屬、印章位置這些才是真正卡住自動(dòng)化的地方。這篇內(nèi)容適合三類人看正在做 OCR 選型的技術(shù)負(fù)責(zé)人、要寫對(duì)接代碼的后端或算法同學(xué)、以及被識(shí)別率上不去折磨過的產(chǎn)品同學(xué)。我會(huì)把選型邏輯、接入細(xì)節(jié)、預(yù)處理參數(shù)、排查經(jīng)驗(yàn)一次講透能直接抄的部分我盡量寫成可復(fù)現(xiàn)的形式。1. 為什么我把文檔解析這件事交給外部接口1.1 非結(jié)構(gòu)化文檔進(jìn)系統(tǒng)的第一道坎從來不是認(rèn)字很多人對(duì)文檔解析的想象停留在圖片轉(zhuǎn)文字真做過就知道這是最淺的一層。一張掃描的采購(gòu)合同丟進(jìn)來業(yè)務(wù)系統(tǒng)真正想要的是合同編號(hào)在哪、甲乙方名稱是什么、金額是多少、簽署日期、有沒有騎縫章、附件表格里每一行對(duì)應(yīng)什么。這些信息的提取依賴的是版面結(jié)構(gòu)而不只是字符。字符識(shí)別錯(cuò)一個(gè)字可能還能靠上下文猜版面結(jié)構(gòu)錯(cuò)了整段數(shù)據(jù)的歸屬就全亂了下游再怎么處理都是錯(cuò)的。我最早一版是自己在服務(wù)器上搭開源 OCR跑單張圖效果看著還行但一上批量就露餡雙欄排版的 PDF 會(huì)被從左到右串成一句頁(yè)眉頁(yè)腳混進(jìn)正文表格直接化成一行一行的字金額和科目對(duì)不上號(hào)。后來我意識(shí)到純文字識(shí)別解決不了閱讀順序問題而閱讀順序是版面分析的活兒。這中間的差距就是自建和采購(gòu)之間的第一道分水嶺。1.2 自建和采購(gòu)我列了一張盡量誠(chéng)實(shí)的對(duì)照表每次有人問我為什么不自己搭我都會(huì)把這張表拿出來。不是說自建不行而是要看你的業(yè)務(wù)量和團(tuán)隊(duì)結(jié)構(gòu)。表里的結(jié)論是基于我遇到過的幾種典型場(chǎng)景總結(jié)的具體到你的情況還得自己稱一稱。維度自建開源方案采購(gòu)文檔解析 API起步成本低一臺(tái)帶顯卡的機(jī)器就能跑低注冊(cè)拿 Key 就能調(diào)達(dá)到可用識(shí)別率的時(shí)間數(shù)周到數(shù)月取決于場(chǎng)景復(fù)雜度基本當(dāng)天復(fù)雜版面一到兩周調(diào)優(yōu)復(fù)雜版面多欄、表格、公式需要自己訓(xùn)練或拼裝多個(gè)模型通常在同一個(gè)接口里返回結(jié)構(gòu)化結(jié)果運(yùn)維負(fù)擔(dān)模型版本、顯存、并發(fā)擴(kuò)縮容全自己扛對(duì)方承擔(dān)你只管調(diào)用單頁(yè)成本前期固定投入高量大后邊際成本低按量計(jì)費(fèi)量小的時(shí)候非常劃算數(shù)據(jù)合規(guī)數(shù)據(jù)不出內(nèi)網(wǎng)可控性最強(qiáng)需要確認(rèn)傳輸與留存策略定制空間想怎么改怎么改受限于接口能力邊界我最后的做法是混合涉密程度高的走內(nèi)網(wǎng)自建量大但對(duì)結(jié)構(gòu)要求一般的走外部接口兩邊用同一套結(jié)果 schema 兜住業(yè)務(wù)層無感知。這個(gè)思路我覺得比二選一更實(shí)際。1.3 選型時(shí)我重點(diǎn)看的五個(gè)指標(biāo)不看宣傳頁(yè)宣傳頁(yè)上的識(shí)別準(zhǔn)確率 99%基本沒有參考價(jià)值因?yàn)槟悴恢浪臏y(cè)試集長(zhǎng)什么樣。我實(shí)際評(píng)估只看這幾個(gè)字段級(jí)準(zhǔn)確率不是字符級(jí)。字符準(zhǔn)確率 98% 聽起來很美但如果金額字段錯(cuò)一位業(yè)務(wù)上就是 100% 錯(cuò)誤。版面還原能力。給一張三欄帶跨頁(yè)表格的 PDF看它返回的閱讀順序?qū)Σ粚?duì)、表格有沒有斷。長(zhǎng)尾文檔的穩(wěn)定性。蓋章遮擋、手機(jī)拍照傾斜、復(fù)印件發(fā)灰、手寫批注這些才是真實(shí)數(shù)據(jù)的主流。接口形態(tài)是否工程友好。同步還是異步、有沒有回調(diào)、單次能傳多大、返回體里有沒有頁(yè)碼和坐標(biāo)。失敗時(shí)的信息量。報(bào)錯(cuò)只給一個(gè) 500和給出圖片分辨率過低或頁(yè)數(shù)超限排查效率差一個(gè)量級(jí)。我建議做選型的時(shí)候直接從自己歷史數(shù)據(jù)里抽 50 到 100 份最有代表性的文件做成一個(gè)小測(cè)試集跑完按字段統(tǒng)計(jì)準(zhǔn)確率。這件事花半天能省掉后面幾個(gè)月的返工。2. 能力拆解這類接口到底把什么交到你手上2.1 從字符識(shí)別到版面分析是三個(gè)層級(jí)的事我習(xí)慣把這類接口的能力分成三層來看理解了分層調(diào)參和排查才有方向。第一層是字符識(shí)別輸出是一堆文字 每個(gè)字的坐標(biāo)框。這一層現(xiàn)在大家差距不大清晰印刷體都能做到很高。第二層是版面分析輸出是這塊是標(biāo)題、這塊是正文、這塊是表格、這塊是頁(yè)眉、這塊是圖片并且給出閱讀順序。第三層是結(jié)構(gòu)化抽取直接輸出 JSON 字段比如{合同編號(hào): ..., 金額: ...}。層級(jí)越高省事越多但同時(shí)你對(duì)錯(cuò)誤的可干預(yù)空間也越小。我實(shí)際用下來第二層是最有性價(jià)比的。因?yàn)闃I(yè)務(wù)側(cè)真正需要的是段落級(jí)、表格級(jí)的文本而不是字段級(jí)。字段抽取我可以自己寫規(guī)則或者接小模型但版面結(jié)構(gòu)我自己做不動(dòng)。2.2 表格識(shí)別為什么是公認(rèn)最難啃的一塊表格識(shí)別難在它不是純視覺問題也不是純語言問題。一張無框線表格視覺上只有一堆對(duì)齊的文字模型得靠間距推斷列邊界一張有合并單元格的表格跨行跨列的關(guān)系得還原成結(jié)構(gòu)化表達(dá)跨頁(yè)表格更麻煩第 3 頁(yè)底部和第 4 頁(yè)頂部是同一張表得靠表頭重復(fù)特征或者語義連續(xù)性去拼。我踩過最典型的坑是金額列錯(cuò)位。一份三列的費(fèi)用清單識(shí)別出來每一行的科目和金額整體左移了一列。原因不是識(shí)別錯(cuò)而是中間有一行的第二個(gè)單元格是空的解析器把空格當(dāng)成了列分隔失效。這種問題在返回結(jié)果里表現(xiàn)為數(shù)據(jù)看著都對(duì)就是錯(cuò)位特別隱蔽一定要在入庫(kù)前做列數(shù)一致性校驗(yàn)。我的建議是表格結(jié)果拿到手后先做一次列數(shù)分布統(tǒng)計(jì)如果某張表的列數(shù)在行間波動(dòng)直接把這張表標(biāo)記為待人工復(fù)核別硬往下走。2.3 印章、公式、手寫批注這些長(zhǎng)尾能力關(guān)鍵時(shí)刻能救命印章檢測(cè)看著像錦上添花實(shí)際在合同場(chǎng)景是剛需。業(yè)務(wù)上需要知道這份文件有沒有蓋章章壓在哪個(gè)位置是不是壓在簽字欄上。我遇到過一批文件正文識(shí)別完全沒問題但實(shí)際是未蓋章的草稿版如果沒有印章位置信息這批文件會(huì)被當(dāng)成正式合同入庫(kù)。公式識(shí)別主要是給技術(shù)文檔和試卷場(chǎng)景用的輸出 LaTeX。這塊的準(zhǔn)確性對(duì)復(fù)雜公式還是有挑戰(zhàn)分式嵌套、上下標(biāo)、矩陣錯(cuò)一處整條公式就廢了。我的處理方式是公式區(qū)域單獨(dú)標(biāo)記出來識(shí)別結(jié)果不直接入庫(kù)先呈現(xiàn)給用戶確認(rèn)。手寫體是另一個(gè)極端。工整的手寫簽名類識(shí)別率可以接受潦草的批注基本別指望。我在產(chǎn)品設(shè)計(jì)上干脆把這部分做成輔助高亮——把疑似手寫的區(qū)域框出來讓人看而不是硬轉(zhuǎn)文字。這個(gè)取舍省了大量投訴。3. 接入實(shí)操?gòu)哪玫綉{證到落庫(kù)的完整鏈路3.1 鑒權(quán)與請(qǐng)求頭第一處容易翻車的地方絕大多數(shù)這類接口走的是憑證換令牌或者固定 Header 帶 Key兩種模式??雌饋砗?jiǎn)單實(shí)際最容易出問題的是這三件事一是密鑰寫死在代碼里提交到了倉(cāng)庫(kù)二是時(shí)間戳和簽名算法對(duì)不上導(dǎo)致 401三是不同接口的 Header 名稱不一致復(fù)制粘貼的時(shí)候漏改。我現(xiàn)在的固定做法是密鑰全部走環(huán)境變量或者配置中心代碼里只讀不寫簽名邏輯單獨(dú)封裝成一個(gè)函數(shù)并寫單測(cè)用固定輸入驗(yàn)證輸出每個(gè)接口的 Header 用常量定義不允許在業(yè)務(wù)代碼里手寫字符串。import os import time import hmac import hashlib import base64 import requests API_BASE https://api.example-doc.com APP_ID os.environ[DOC_APP_ID] APP_SECRET os.environ[DOC_APP_SECRET].encode() def build_headers(path: str, body: bytes) - dict: ts str(int(time.time())) # 待簽名串方法 路徑 時(shí)間戳 body 摘要順序必須與服務(wù)端一致 digest hashlib.sha256(body).hexdigest() raw fPOST\n{path}\n{ts}\n{digest}.encode() sign base64.b64encode( hmac.new(APP_SECRET, raw, hashlib.sha256).digest() ).decode() return { X-App-Id: APP_ID, X-Timestamp: ts, X-Signature: sign, Content-Type: application/json, }注意簽名串里的換行符、參數(shù)順序、大小寫任何一處和服務(wù)端不一致都會(huì)返回鑒權(quán)失敗而錯(cuò)誤信息往往只說簽名錯(cuò)誤。建議第一次對(duì)接時(shí)先用最簡(jiǎn)單的固定 body 打通再逐步加字段。3.2 三種傳圖方式別圖省事一律用 URL這類接口一般支持三種輸入公網(wǎng)可訪問的 URL、Base64 內(nèi)聯(lián)、multipart 文件流。三者適用場(chǎng)景差別很大混用會(huì)出問題。URL 方式最省流量但要求你的文件必須公網(wǎng)可達(dá)且穩(wěn)定。我們內(nèi)網(wǎng)的文件服務(wù)走不了這條路而且對(duì)方服務(wù)器拉取超時(shí)的時(shí)候報(bào)錯(cuò)信息會(huì)很含糊。Base64 方式最通用缺點(diǎn)是體積膨脹約 33%一張 2MB 的圖變成 2.7MB 的請(qǐng)求體很容易超過請(qǐng)求體上限。multipart 方式最適合大文件但要注意部分網(wǎng)關(guān)對(duì) multipart 的支持不完整。我現(xiàn)在的默認(rèn)策略是小于 1MB 用 Base64 圖省事大于 1MB 走 multipart需要批量異步處理的先傳到對(duì)象存儲(chǔ)再用 URL。這個(gè)分界線不是拍腦袋是按照請(qǐng)求體不超過 3MB、單請(qǐng)求耗時(shí)不超過 15 秒這兩條經(jīng)驗(yàn)線倒推的。3.3 異步任務(wù)模式輪詢、回調(diào)與超時(shí)控制超過一定頁(yè)數(shù)或者走結(jié)構(gòu)化抽取接口通常會(huì)切成異步提交任務(wù)拿task_id然后輪詢結(jié)果或者等回調(diào)。這塊是工程上最容易寫出事故的地方。輪詢寫法上我見過太多while True sleep(1)的實(shí)現(xiàn)任務(wù)一多就把對(duì)方打掛自己也跟著被限流。正確姿勢(shì)是指數(shù)退避加最大重試次數(shù)并且把單次任務(wù)的超時(shí)時(shí)間設(shè)死。import time import requests def poll_result(task_id: str, timeout: int 180) - dict: deadline time.time() timeout interval 1.0 while time.time() deadline: r requests.get( f{API_BASE}/v1/tasks/{task_id}, headersbuild_headers(f/v1/tasks/{task_id}, b), timeout10, ) data r.json() status data.get(status) if status done: return data[result] if status failed: raise RuntimeError(ftask failed: {data.get(message)}) time.sleep(interval (0.2 * interval * (0.5 - __import__(random).random()))) interval min(interval * 2, 8.0) # 上限 8 秒避免長(zhǎng)尾任務(wù)把間隔拉太大 raise TimeoutError(ftask {task_id} not finished in {timeout}s)退避上限卡在 8 秒是有原因的多數(shù)文檔解析任務(wù)在 20 秒內(nèi)完成超過 2 分鐘基本就是卡住了這時(shí)候應(yīng)該走失敗重提而不是繼續(xù)等。加了隨機(jī)抖動(dòng)是為了避免批量提交時(shí)所有客戶端在同一秒集體撞上來。回調(diào)模式更省資源但要求你的回調(diào)地址公網(wǎng)可達(dá)、能冪等處理重復(fù)通知。我一般兩個(gè)都開回調(diào)作為主路徑輪詢作為兜底兩邊都做冪等靠task_id去重。3.4 返回結(jié)果的解析與落庫(kù)schema 一定要自己定不同接口返回的字段命名、層級(jí)深度都不一樣。最忌諱的做法是業(yè)務(wù)代碼直接讀接口返回的原始 JSON一旦接口升版本或者你換供應(yīng)商整個(gè)系統(tǒng)跟著改。我的做法是中間加一層適配定義一套自己的 schema包含頁(yè)碼、塊類型、文本、坐標(biāo)、置信度、層級(jí)關(guān)系然后為每個(gè)供應(yīng)商寫一個(gè)轉(zhuǎn)換器。這樣業(yè)務(wù)層只認(rèn)自己的 schema換供應(yīng)商時(shí)只改轉(zhuǎn)換器。def normalize(raw: dict) - list[dict]: 把供應(yīng)商返回結(jié)構(gòu)統(tǒng)一成本地 schema blocks [] for page in raw.get(pages, []): pno page[page_no] for item in page.get(items, []): blocks.append({ page: pno, type: item.get(type, text), # text/table/image/title text: item.get(content, ), bbox: item.get(box, []), # [x1,y1,x2,y2] conf: round(float(item.get(score, 0)), 4), seq: item.get(reading_order, 0), }) return sorted(blocks, keylambda b: (b[page], b[seq]))落庫(kù)時(shí)我一般存兩份一份原始返回整體存進(jìn)對(duì)象存儲(chǔ)或者 JSON 字段留檔便于回溯一份結(jié)構(gòu)化塊存進(jìn)業(yè)務(wù)表供查詢和檢索。前者幾乎不占查詢性能但排查問題是救命的。4. 識(shí)別率的真正來源預(yù)處理和參數(shù)才是分水嶺4.1 分辨率與 DPI一個(gè)必須算清楚的基礎(chǔ)參數(shù)很多人把手機(jī)拍的圖直接丟給接口然后抱怨識(shí)別率低。問題往往出在分辨率上。印刷體識(shí)別對(duì)字符高度有隱性要求字符太小時(shí)抗鋸齒和壓縮噪聲會(huì)把筆畫糊掉模型無從下手。我習(xí)慣按字符高度不低于 20 像素來倒推。A4 紙 210 毫米寬正文小四號(hào)字大約 3.5 毫米高。如果按 300 DPI 掃描210 毫米等于 8.27 英寸橫向約 2480 像素3.5 毫米的字符大約占 41 像素非常充裕。同樣是 A4150 DPI 時(shí)字符約 21 像素勉強(qiáng)夠用100 DPI 就只有 14 像素小字開始出錯(cuò)。紙張規(guī)格掃描 DPI像素尺寸正文估算字符高度我的判斷A4 210×297mm100827×1169約 14 px偏低小字易錯(cuò)A4 210×297mm1501240×1754約 21 px可用適合快速預(yù)覽A4 210×297mm2001654×2339約 27 px平衡點(diǎn)我常用的檔位A4 210×297mm3002481×3508約 41 px最穩(wěn)但文件體積大A4 210×297mm6004961×7016約 82 px收益遞減不建議實(shí)際項(xiàng)目里我默認(rèn)用 200 DPI既保證準(zhǔn)確率又把單頁(yè)文件控制在合理體積。票據(jù)類因?yàn)樽痔?hào)更小、字段更密我會(huì)提到 300 DPI。4.2 傾斜校正、去噪、二值化什么時(shí)候該做什么時(shí)候別碰預(yù)處理不是越多越好這是我交過學(xué)費(fèi)的結(jié)論。傾斜校正幾乎是必做項(xiàng)。手機(jī)拍照哪怕偏 2 度長(zhǎng)表格的行線就會(huì)斜著穿過文字表格結(jié)構(gòu)直接崩。我用 OpenCV 的霍夫變換或者最小外接矩形估角度超過 1 度才旋轉(zhuǎn)閾值太小會(huì)因?yàn)槎秳?dòng)導(dǎo)致反向影響。去噪要分情況。椒鹽噪聲、掃描斑點(diǎn)中值濾波效果不錯(cuò)。但如果是復(fù)印件上那種淡淡的底紋粗暴去噪會(huì)把細(xì)筆畫一起干掉。我一般只用輕度處理寧可留著噪點(diǎn)也不傷筆畫。二值化是最容易幫倒忙的一步。現(xiàn)在主流模型都是在灰度或彩色圖上訓(xùn)練的你自己先二值化相當(dāng)于把信息壓掉了反而掉點(diǎn)。我的原則是只有在圖像對(duì)比度極低、肉眼都看不清的情況下才考慮自適應(yīng)二值化其余情況保持原圖灰度甚至彩色。import cv2 import numpy as np def preprocess(path: str) - np.ndarray: img cv2.imread(path, cv2.IMREAD_COLOR) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 1) 輕度去噪別用大核 gray cv2.medianBlur(gray, 3) # 2) 傾斜角度估計(jì)超過 1 度才校正 edges cv2.Canny(gray, 50, 150) coords np.column_stack(np.where(edges 0)) if len(coords) 100: angle cv2.minAreaRect(coords)[-1] angle -(90 angle) if angle -45 else -angle if abs(angle) 1.0: h, w gray.shape m cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) gray cv2.warpAffine( gray, m, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE, ) return gray注意旋轉(zhuǎn)后一定要用BORDER_REPLICATE而不是默認(rèn)的黑色填充否則邊緣會(huì)出現(xiàn)一大片黑邊被模型當(dāng)成圖像內(nèi)容影響版面判斷。4.3 多欄排版和跨頁(yè)內(nèi)容靠切分和解構(gòu)來解決雙欄、三欄的 PDF 是閱讀順序出錯(cuò)的重災(zāi)區(qū)。如果接口支持指定版面類型或者單雙欄一定要傳對(duì)。如果接口不提供這個(gè)開關(guān)那就得自己先按列切圖逐列送識(shí)別再按列順序拼接。判斷欄數(shù)有個(gè)土辦法對(duì)整頁(yè)做垂直投影統(tǒng)計(jì)每列像素密度的波谷位置。兩欄文檔在中間會(huì)有一條明顯的空白帶。這個(gè)方法的魯棒性一般但對(duì)規(guī)整的學(xué)術(shù)論文和期刊掃描件足夠用??珥?yè)內(nèi)容我一般按頁(yè)眉頁(yè)腳剔除 相鄰頁(yè)尾首拼接來處理。頁(yè)眉頁(yè)腳的特征是位置固定、內(nèi)容重復(fù)可以在多頁(yè)之間做文本相似度比對(duì)重復(fù)出現(xiàn)三次以上的短文本直接過濾掉??珥?yè)表格則要看表頭是否在下頁(yè)重復(fù)出現(xiàn)是的話就把兩頁(yè)的表合并。4.4 參數(shù)組合的實(shí)測(cè)對(duì)照我留了一份小表下面這組數(shù)據(jù)是我在同一批 200 份混合文檔掃描件、拍照件、PDF 各占約三分之一上對(duì)比出來的相對(duì)趨勢(shì)不是絕對(duì)值但趨勢(shì)穩(wěn)定預(yù)處理組合正文通順度表格還原平均耗時(shí)備注原圖直傳一般差1.0x傾斜件基本全廢僅傾斜校正較好中1.1x性價(jià)比最高傾斜 輕去噪好較好1.2x我現(xiàn)在的默認(rèn)檔傾斜 輕去噪 二值化一般中1.2x細(xì)筆畫丟失明顯傾斜 強(qiáng)去噪 銳化差差1.3x過度處理反效果這張表給我的最大教訓(xùn)是預(yù)處理的目標(biāo)是減少干擾不是改變圖像。任何讓圖像看起來更干凈但丟失了原始信息的操作都要謹(jǐn)慎。5. 常見問題與排查實(shí)錄5.1 識(shí)別結(jié)果亂序十有八九是閱讀順序沒取對(duì)文字都識(shí)別出來了但順序是亂的——這個(gè)問題的排查順序應(yīng)該是先看返回里有沒有reading_order或者段落序號(hào)字段有就直接用字段排序別自己按坐標(biāo)排序如果沒有再考慮按先分欄、再按 y 坐標(biāo)、后按 x 坐標(biāo)排序。自己按坐標(biāo)排是最容易出錯(cuò)的因?yàn)橥恍形淖值?y 坐標(biāo)會(huì)有幾像素的浮動(dòng)簡(jiǎn)單按 y 排序會(huì)把一行拆成兩行。我用的容差是半個(gè)字符高度y 差值在這個(gè)范圍內(nèi)視為同一行。這個(gè)方法對(duì)付規(guī)整版面沒問題對(duì)付圖文混排就力不從心了所以還是盡量用接口給的順序字段。還有一種情況是結(jié)果本身沒錯(cuò)是你在展示的時(shí)候把 JSON 的字典順序當(dāng)成了閱讀順序。字典序和閱讀順序毫無關(guān)系這個(gè)坑我見過不止一個(gè)人踩。5.2 表格錯(cuò)列與合并單元格丟失的應(yīng)對(duì)表格問題我整理了一套固定的自檢流程每次上線新場(chǎng)景都跑一遍統(tǒng)計(jì)該表的行數(shù)分布如果某一行明顯偏短標(biāo)記異常。統(tǒng)計(jì)每行列數(shù)列數(shù)波動(dòng)超過 20% 的表標(biāo)記為待復(fù)核。檢查是否存在空單元格占位空單元格容易被解析器吞掉導(dǎo)致左移。對(duì)于跨頁(yè)表檢查第二頁(yè)首行是否與第一頁(yè)表頭一致。合并單元格是另一個(gè)維度的問題。有些接口會(huì)返回rowspan和colspan有些只返回展平后的值。展平的后果是總計(jì)那一行的數(shù)值被塞進(jìn)第一個(gè)單元格看著像數(shù)據(jù)錯(cuò)位。如果接口不支持合并信息我的做法是把表格結(jié)果按行展示給用戶同時(shí)把原始圖片的表格區(qū)域裁出來一起給讓人工快速核對(duì)。5.3 限流、超時(shí)、大文件三個(gè)必須提前設(shè)計(jì)的邊界限流先確認(rèn)接口的 QPS 上限和并發(fā)上限是兩個(gè)獨(dú)立指標(biāo)很多接口只限并發(fā)不限 QPS或者反過來。我一般在本地下游放一個(gè)令牌桶把并發(fā)壓到上限的 70%留出余量給重試。超時(shí)同步接口的客戶端超時(shí)要設(shè)得比服務(wù)端處理時(shí)間長(zhǎng)否則會(huì)出現(xiàn)客戶端放棄了但服務(wù)端還在算的浪費(fèi)。我設(shè)的是服務(wù)端超時(shí) 30 秒、客戶端 35 秒。大文件超過頁(yè)數(shù)上限的文檔要先拆分。拆分的時(shí)候注意別把一張表拆斷在兩頁(yè)之間我一般按章節(jié)標(biāo)題前或空白頁(yè)后作為切分點(diǎn)。5.4 問題速查表出問題先對(duì)著這張表看一遍現(xiàn)象最可能原因排查動(dòng)作返回 401 / 簽名錯(cuò)誤時(shí)間戳偏差、簽名串順序不對(duì)校驗(yàn)服務(wù)器時(shí)間打印簽名原文比對(duì)返回 413請(qǐng)求體超限改用 multipart或壓縮圖片到 2MB 內(nèi)返回 429觸發(fā)限流檢查本地令牌桶加退避重試任務(wù)一直 pending隊(duì)列積壓或參數(shù)不合法看任務(wù)詳情里的入?yún)⒒仫@確認(rèn)頁(yè)數(shù)是否超限正文缺字分辨率過低、二值化過度提到 200 DPI 以上關(guān)閉二值化表格左移一列空單元格被吞校驗(yàn)列數(shù)一致性標(biāo)記異常表印章檢測(cè)不到章體顏色偏淡、被壓縮提分辨率用彩色原圖而非灰度公式亂碼復(fù)雜嵌套結(jié)構(gòu)單獨(dú)標(biāo)記區(qū)域人工確認(rèn)中文標(biāo)點(diǎn)變英文后處理規(guī)則缺失加一層標(biāo)點(diǎn)規(guī)范化長(zhǎng)文檔后半段質(zhì)量驟降單頁(yè)耗時(shí)累計(jì)超時(shí)拆分成多個(gè)任務(wù)提交6. 工程化落地把它用成穩(wěn)定基礎(chǔ)設(shè)施而不是一次性腳本6.1 緩存與冪等能砍掉三成以上的重復(fù)開銷實(shí)際業(yè)務(wù)里同一份文件被重復(fù)提交的概率高得驚人用戶刷新頁(yè)面、任務(wù)重試、多個(gè)業(yè)務(wù)線各調(diào)一次。我做的第一件事就是加緩存緩存的 key 用文件內(nèi)容哈希 關(guān)鍵參數(shù)指紋注意是內(nèi)容哈希不是文件名文件名重復(fù)的情況太多了。參數(shù)指紋要包含所有影響結(jié)果的參數(shù)是否開啟表格、是否開啟印章、語言、版面模式。少了任何一個(gè)都可能出現(xiàn)參數(shù)不同卻命中同一份緩存的錯(cuò)誤。緩存命中率我實(shí)測(cè)下來在票據(jù)場(chǎng)景能到 35% 左右在合同場(chǎng)景接近 45%。這一塊省下來的成本和時(shí)間非常可觀。6.2 質(zhì)量監(jiān)控和人工復(fù)核閉環(huán)別指望一步到位再好的接口也有失手的時(shí)候關(guān)鍵是要能發(fā)現(xiàn)。我在落庫(kù)之后加了一層自動(dòng)質(zhì)檢關(guān)鍵字段的格式校驗(yàn)比如金額必須能轉(zhuǎn)成數(shù)值、日期必須符合格式。字段間的邏輯校驗(yàn)比如合計(jì)是否等于明細(xì)之和、簽署日期是否早于生效日期。置信度閾值低于閾值的塊自動(dòng)進(jìn)入待復(fù)核隊(duì)列。待復(fù)核的界面設(shè)計(jì)上我堅(jiān)持原圖 識(shí)別結(jié)果左右對(duì)照并且把低置信度的字符高亮出來。這個(gè)設(shè)計(jì)讓復(fù)核效率提升非常明顯平均每份文件的復(fù)核時(shí)間從兩分多鐘壓到四十秒左右。復(fù)核的結(jié)果還能反哺把人工修正后的數(shù)據(jù)定期抽出來做回歸測(cè)試集每次接口升級(jí)或者參數(shù)調(diào)整前跑一遍避免改了一個(gè)場(chǎng)景崩了另一個(gè)場(chǎng)景。6.3 成本控制的幾個(gè)杠桿我按收益排了序成本這件事光看單價(jià)意義不大得看你浪費(fèi)了多少。我按投入產(chǎn)出比排了個(gè)序第一是前置篩選。清晰度不達(dá)標(biāo)的圖先退回重拍別送去識(shí)別。一張糊圖不僅浪費(fèi)一次調(diào)用還會(huì)帶來人工復(fù)核成本。第二是按需開啟能力。表格、印章、公式這些增強(qiáng)能力通常單獨(dú)計(jì)費(fèi)業(yè)務(wù)不需要就別開。我見過有人在合同場(chǎng)景把公式識(shí)別也開著純屬浪費(fèi)。第三是壓縮策略。同一頁(yè)圖像從 300 DPI 彩色降到 200 DPI 灰度體積能降六七成識(shí)別率損失很小。這個(gè)平衡點(diǎn)在不同文檔類型上不一樣得自己試。第四是批量與異步。有些服務(wù)對(duì)批量任務(wù)有折扣量大的時(shí)候值得改造成批量提交。第五才是比價(jià)。前面四項(xiàng)做完了再比價(jià)否則就是拿優(yōu)化空間換幾分錢的差價(jià)。最后分享一個(gè)我自己的體會(huì)這類文檔解析接口的價(jià)值不在它識(shí)別得多準(zhǔn)而在它把版面結(jié)構(gòu)這件極其費(fèi)人力的事情標(biāo)準(zhǔn)化了。我早年在項(xiàng)目里為了讓一份雙欄 PDF 的閱讀順序正確寫過幾百行坐標(biāo)排序的代碼最后還是繞不過跨欄表格的坑?,F(xiàn)在把這塊交給成熟接口我自己專注在字段抽取和業(yè)務(wù)校驗(yàn)上整體交付質(zhì)量反而更穩(wěn)。如果你也在做類似的事我的建議是別急著追求識(shí)別率 99%先把失敗路徑設(shè)計(jì)清楚——哪些情況降級(jí)、哪些情況轉(zhuǎn)人工、哪些情況直接拒絕。把這套兜底邏輯想明白比調(diào)參更能決定項(xiàng)目能不能上線。