PaddleOCR 實戰:83 秒提取港股年報財務數據 解決了「機器能不能把年報上的字讀出來」;這篇要解決下一個問題:讀出來的一堆文字,怎麼變成可查詢、可校驗、可回溯的結構化數據。 這兩個問題之間的距離,比多數人預期的遠——OCR 給你的是一頁頁帶座標的字串,而估值模型要的是「FY2025 收入,港幣千元,出自第 96 頁,已人工核對」這樣一個完整的數據點。
本文給出一條五階段管線:定位、抽取、對應、schema、勾稽。每個階段都有明確的輸入輸出與失效形態。讀完你應該能設計自己的年報結構化流程,並且知道每一步哪裡會出錯、錯了怎麼抓。
全景:管線的五個階段
| 階段 | 輸入 | 輸出 | 主要風險 |
|---|---|---|---|
| 1. 定位 | 整份 PDF | 三表與附註的頁碼範圍 | 定位到錯誤的「類似頁面」 |
| 2. 抽取 | 目標頁面(文字層或圖像) | 帶座標的原始儲存格 | 跨頁表格接錯、行列錯位 |
| 3. 對應 | 原始儲存格 | 標準欄位+統一單位 | 科目映射錯、口徑混淆 |
| 4. Schema | 標準欄位 | 結構化 JSON(帶元資料) | 丟失來源座標與原始值 |
| 5. 勾稽 | 結構化 JSON | 通過校驗的 JSON+失敗清單 | 容差設計不當、校驗被繞過 |
設計原則先講在前面:每個階段的輸出都保留上一階段的痕跡。 JSON 裡要能找到原始抽取值與頁碼,對應表要能查到為什麼這個科目映射到那個欄位。管線出錯時,你要能定位是哪一階段錯的,而不是整段重跑然後祈禱。
階段一:定位財報頁
300 頁的年報裡,你要的目標分散在幾個固定區域:合併損益表、合併資產負債表(財務狀況表)、合併現金流量表、合併權益變動表,以及附註裡的分部資訊。定位的常用信號:
| 信號 | 用法 |
|---|---|
| 目錄頁 | 年報開頭通常有目錄,直接給出各節頁碼——但那是「印刷頁碼」,不是 PDF 頁碼 |
| 章節結構 | 經審計財務報表本體通常緊接在獨立核數師報告之後、附註之前 |
| 關鍵詞掃描 | 對每頁文字搜「綜合損益表」「財務狀況表」「現金流量表」「分部」等標題詞 |
| 頁眉頁腳 | 財報章節的頁眉常與前後的董事會報告不同 |
三個必踩的坑:
- PDF 頁碼與印刷頁碼的偏移。 封面、目錄、公司資料佔掉前幾頁,之後兩個頁碼系統就差出一個常數。管線內部統一用 PDF 頁碼,輸出給人看的引用再換算成印刷頁碼,並把換算偏移量記錄在元資料裡。
- 同一張表在年報裡出現多次。 董事會報告會引用財務摘要、五年財務摘要會再列一遍主要數字、業績公告與年報也高度重疊。只認「經審計的財務報表本體」那一份——判斷依據是它處於核數師報告與附註之間的結構位置,而不是標題像不像。
- 分部資訊不在報表本體裡。 分部收入、分部利潤通常藏在附註(搜索「分部」或「segment」),而且往往跨頁。定位階段要把它單獨列為一個目標區域。
補充一點:中期報告的結構與年報類似,定位邏輯可以整套復用,但中期財務數據通常未經全年審計(部分公司會安排審閱)。管線必須在元資料裡標注審計狀態——經審計與未經審計的數據,在下游分析裡的權重不應該相同,混在一起存是日後口徑災難的源頭。
階段二:抽取與跨頁表格處理
抽取本身分兩條路徑——文字層 PDF 直接解析、掃描檔走 OCR 加版面分析——原理與工具選擇見 機器怎麼讀懂財報:從 PDF 到結構化數據,PaddleOCR 的實作細節見 PaddleOCR 實戰:83 秒提取港股年報財務數據。這裡只講財報抽取特有的兩個難題。
跨頁表格的接續。 附註裡的長表(分部資訊、金融工具、關連方)經常被切成兩頁以上。處理規則:
| 步驟 | 動作 |
|---|---|
| 1 | 識別接續標誌:頁首的「(續)」字樣、與上頁相同的表頭、行標籤序列的自然延續 |
| 2 | 接續頁繼承上頁的列定義與行標籤結構,不重新推斷 |
| 3 | 檢查斷點:上頁最後一行與續頁第一行是「接續」還是「重複」——部分排版會在續頁重複最後一行,直接合併會多算一次 |
| 4 | 合併後的表格記錄完整來源頁範圍(例如「第 130–132 頁」) |
合併儲存格的層級。 財報常見「大類+其中項」的縮排結構:一個大類標籤縱跨多行,子項用縮排表示。抽取階段不要急著展平——先把座標與縮排原樣存下來,層級的解釋留到對應階段做。原因:展平是有損操作,一旦在抽取階段就猜錯層級,後面沒有機會回頭。
抽取階段的標準輸出形態,是「每張表一組結構化記錄」:每一行的文字、所屬欄位、是否為小計或合計行的標記,加上來源頁碼與座標。座標看起來冗餘,但它是日後覆核行列錯位的唯一依據——當某個數字被質疑配錯科目時,你只能靠回到座標、重看原始版面來裁定是哪一環出了錯。
階段三:欄位對應與單位統一
這一階段把「報表語言」翻譯成「你的數據語言」,是三階段裡最需要人工設計的一步。
科目映射表。 同一個會計概念,不同公司的報表項目名不同。維護一張映射表,從標準欄位出發,遇到新名稱就增補,跑過幾份年報之後這張表會穩定下來:
| 標準欄位 | 報告中常見的名稱變體(示例) |
|---|---|
| revenue | 收入、營業額、銷售額 |
| costOfSales | 銷售成本、營業成本 |
| grossProfit | 毛利(部分公司不單獨列示,需以收入減銷售成本推得,此時標注「推得」) |
| profitAttributableToOwners | 本公司擁有人應佔溢利、股東應佔利潤 |
| totalAssets | 資產總值、資產總計 |
| netCashFromOperating | 經營活動所得現金淨額、營運現金流 |
映射的紀律:推得的欄位必須標注「推得」與推得公式,與報告原文直接列示的數字區分開。上面 grossProfit 的例子就是典型——它不是抽取值,是計算值,出錯責任在你而不在報告。
單位與幣別統一。 年報表頭可能寫「人民幣千元」「港幣百萬元」,管線必須:
- 把單位與幣別從表頭抽出來,寫進每張表的元資料,不靠人記憶
- 原始值(raw)按報告單位保存;換算後的值(normalized)另存一個欄位。兩者都留,換算錯誤才能被發現
- 財年期間用確切的起訖日期表示(港股公司以 12 月 31 日為財年年結日者居多,但也有其他年結日),不要只存「2025」
四捨五入的現實。 年報數字是捨入後的(千元位、百萬位),報表內部的加總可能差一兩個最小單位——這是正常現象,不是錯誤。它直接決定了下一階段的勾稽容差設計:容差為零的校驗會天天誤報。
階段四:JSON schema 設計
Schema 是管線的合約。設計原則四條:
- 每個數值是一個物件,不是一個裸數字——至少包含原始值、來源頁碼、校對狀態三樣。
- raw 與 normalized 分離——原始抽取值永不被覆蓋,換算與修正都是新增欄位。
- 元資料完整——實體、財年期間、幣別、單位、來源文件(檔名與擷取日期)、管線版本。
- 校驗狀態顯式——
humanVerified布林值加信心度,未校對的數據在下游必須可識別。
以下為教學用假設數字與假設公司,非任何真實公司;欄位設計可直接改編使用:
{
"meta": {
"entity": "教學用假設公司",
"stockCode": "00000",
"fiscalPeriod": { "start": "2025-01-01", "end": "2025-12-31" },
"reportingCurrency": "HKD",
"unit": "thousand",
"pageOffset": 4,
"sourceDocument": { "file": "ar2025.pdf", "retrievedAt": "2026-09-30" },
"pipelineVersion": "1.0"
},
"incomeStatement": {
"revenue": {
"raw": { "label": "收入", "value": 888000, "page": 96 },
"normalized": { "value": 888000000, "unit": "HKD" },
"derived": false,
"ocrConfidence": 0.99,
"humanVerified": true
},
"costOfSales": {
"raw": { "label": "銷售成本", "value": -600000, "page": 96 },
"normalized": { "value": -600000000, "unit": "HKD" },
"derived": false,
"ocrConfidence": 0.98,
"humanVerified": true
},
"grossProfit": {
"raw": { "label": null, "value": 288000, "page": 96 },
"derived": true,
"derivedFrom": "revenue + costOfSales",
"humanVerified": true
}
},
"balanceSheet": {
"totalAssets": { "raw": { "label": "資產總值", "value": 2000000, "page": 98 }, "humanVerified": true },
"totalLiabilities": { "raw": { "value": 800000, "page": 98 }, "humanVerified": true },
"totalEquity": { "raw": { "value": 1200000, "page": 98 }, "humanVerified": true }
},
"cashFlowStatement": {
"netCashFromOperating": { "raw": { "value": 100000, "page": 100 }, "humanVerified": false },
"cashAtEndOfPeriod": { "raw": { "value": 150000, "page": 100 }, "humanVerified": false }
},
"segments": [
{ "name": "假設分部A", "revenue": { "raw": { "value": 500000, "page": 130 }, "humanVerified": false } },
{ "name": "假設分部B", "revenue": { "raw": { "value": 388000, "page": 130 }, "humanVerified": false } }
],
"segmentReconciliation": {
"interSegmentElimination": { "raw": { "value": 0, "page": 130 } }
}
}
幾個值得注意的設計細節:pageOffset 記錄 PDF 頁碼與印刷頁碼的換算;derived 與 derivedFrom 把計算值與抽取值分開;成本以負數保存,符合報表呈現習慣,也讓 revenue + costOfSales 的推得公式成立。
還有兩個經常被忽略的設計點。第一是多年合併:按「一個財年一個檔案」組織,跨年的合併視圖用程式生成,不要手工維護一份大表;欄位名必須跨年穩定,否則每年微調的 schema 會讓所有下游比較代碼悄悄壞掉。第二是修訂記錄:年報可能出更正版,抽取結果也可能被人工修正——JSON 裡要能回答「這個數字的初值是什麼、何時被誰改成了什麼、為什麼改」,修改一律以新增修訂記錄的方式留痕,而不是原地覆蓋。
要支持勾稽檢查 4(留存收益滾動),schema 裡還需要權益變動的一小段,接在上例的 balanceSheet 之後(同為假設數字):
{
"equityChanges": {
"retainedEarnings": {
"opening": { "raw": { "value": 950000, "page": 99 }, "humanVerified": true },
"profitForYear": { "raw": { "value": 180000, "page": 99 }, "humanVerified": true },
"dividends": { "raw": { "value": -30000, "page": 99 }, "humanVerified": false },
"closing": { "raw": { "value": 1100000, "page": 99 }, "humanVerified": true }
}
}
}
階段五:勾稽校對
結構化數據最大的風險是「安靜地錯」:每個數字看起來都合理,錯的只在綁定關係。勾稽校對用會計恆等式當探測器,把錯誤逼出來。核心檢查表:
| # | 檢查項 | 關係式 | 容差 | 不通過的可能含義 |
|---|---|---|---|---|
| 1 | 資產負債表平衡 | totalAssets = totalLiabilities + totalEquity | 1–2 個最小單位(捨入) | 抽取漏項、映射錯欄,或平衡項本身讀錯 |
| 2 | 損益表小計 | revenue + costOfSales = grossProfit | 捨入容差 | 小計行與明細行錯位 |
| 3 | 現金勾稽 | 現金流量表期末現金 ≈ 資產負債表現金及現金等價物 | 較寬:注意外幣折算差與受限制現金 | 不一定是錯——先讀附註的調節表,調節不上才是錯 |
| 4 | 留存收益滾動 | 期末 = 期初 + 本期歸屬利潤 − 股息 ± 其他調整 | 需權益變動表與附註支持 | 權益變動表抽取錯誤,或調整項遺漏 |
| 5 | 分部對帳 | 各分部收入合計 − 分部間抵銷 = 合併收入 | 捨入容差 | 分部資訊跨頁接續錯誤(階段二失效) |
| 6 | EPS 勾稽 | 基本每股盈利 × 加權平均股數 ≈ 歸屬母公司利潤 | 較寬:EPS 通常只列到小數點後兩三位 | 期間或實體錯位——例如混入了上期欄位 |
拿上面的假設 JSON 演練一遍:檢查 1,2,000,000 = 800,000 + 1,200,000,通過;檢查 2,888,000 − 600,000 = 288,000,通過;檢查 4,950,000 + 180,000 − 30,000 = 1,100,000,通過;檢查 5,500,000 + 388,000 − 0 = 888,000,通過。全部通過不代表數據全對——勾稽抓不到「兩個數字一起錯」的情形(例如整張表串了一個欄位但內部自洽)——但任何一項不過,都保證有問題。
工程實現要點:
- 勾稽用 raw 值,不用 normalized 值。 換算錯誤(例如千元當百萬)會同時縮放等式兩邊,用換算值校驗等於沒校驗。
- 勾稽腳本獨立於抽取過程。 必須是「另一雙眼睛」——絕不讓執行抽取的同一個模型自己宣告校驗通過。「獨立」要落在架構上而不是自覺上:腳本在獨立進程運行、只讀產出的 JSON 檔案、與抽取模型不共享任何上下文,讓「改數讓檢查通過」在物理上做不到,而不是「約定不做」。
- 容差顯式寫在配置裡。 每項檢查的容差與理由記錄下來;容差調整需要人批準,防止「調大容差讓檢查通過」的靜默腐化。
- 失敗不是異常,是產出。 勾稽結果(通過清單+失敗清單+每項的實際差額)與 JSON 一起存檔,作為人工複核的工作清單。
勾稽本身只需要幾十行代碼。示意骨架(沿用上文的假設 JSON):
TOL = 2 # 捨入容差,單位與報表一致(千元)
def reconcile(data):
failures = []
bs = data["balanceSheet"]
lhs = bs["totalAssets"]["raw"]["value"]
rhs = bs["totalLiabilities"]["raw"]["value"] + bs["totalEquity"]["raw"]["value"]
if abs(lhs - rhs) > TOL:
failures.append(("資產=負債+權益", lhs, rhs))
seg_sum = sum(s["revenue"]["raw"]["value"] for s in data["segments"])
elim = data["segmentReconciliation"]["interSegmentElimination"]["raw"]["value"]
rev = data["incomeStatement"]["revenue"]["raw"]["value"]
if abs(seg_sum - elim - rev) > TOL:
failures.append(("分部對帳", seg_sum - elim, rev))
return failures # 空列表=全過;非空=人工裁定佇列
注意兩個細節:全程用 raw 值;輸出是「失敗清單」而不是布林值——失敗項要帶著實際差額,因為差額的大小本身就是診斷資訊(差一兩個單位多半是捨入、差整千倍是單位錯、差額無規律是行列錯位)。
AI 會在這裡出錯,以及人工複核節點
這條管線裡 AI 的典型失效形態,按危險程度排序:
| 失效形態 | 說明 | 防法 |
|---|---|---|
| 為通過校驗而改數 | 最危險的一種:模型發現勾稽不平,不去報告失敗,而是「順手」調整某個數字讓等式成立 | 勾稽腳本與抽取過程物理隔離;raw 值不可變,任何修改都是新增欄位並留痕 |
| 科目映射自創 | 把「其他收入」映射進 revenue、把租賃負債漏出 totalLiabilities | 映射表是白名單:不在表裡的科目一律進「待人工分類」佇列 |
| 跨頁表格接錯但數字合理 | 續頁行配到錯誤標籤,勾稽未必抓得到 | 跨頁合併處強制列入人工抽查範圍 |
| 單位換算錯一千倍 | raw 對、normalized 錯 | 雙值並存+勾稽用 raw(上文已述) |
| 虛報 humanVerified | 模型把自己「看過了」當成「人工核對了」 | 該欄位只能由校對工具在人工確認動作後寫入,模型輸出中一律預設 false |
人工複核的最小配置,按投入產出排序:
- 錨點全核:三表的合計與小計(每份報告約十幾個數字)逐項回看原頁——這是性價比最高的一步
- 勾稽失敗逐條裁定:每條失敗由人判斷是抽取錯、口徑差還是報表本身的特殊情形,裁定結果寫回記錄
- 明細抽樣:非錨點欄位按比例抽樣回查(例如一成),抽樣發現錯誤時擴大範圍
- Schema 變更審批:新增欄位、修改映射、調整容差,都要人批準並記錄版本
把這四條固定下來之後,管線就具備了本站反覆強調的那個性質:每個數字要嘛被人工核過,要嘛明確標著「未核」,不存在第三種狀態。 下游的現金流品質分析(三表聯動與現金流品質檢查:識別賬面利潤陷阱)與估值建模(DCF 假設與敏感度分析:讓估值可被挑戰)都應該只消費帶狀態標籤的數據。
下一步
- PaddleOCR 實戰:83 秒提取港股年報財務數據:本管線階段二的工具實作——PaddleOCR 環境、表格解析與效能實測。
- 機器怎麼讀懂財報:從 PDF 到結構化數據:PDF 讀取的三條路徑與 OCR 校對策略,本管線的概念地基。
- 三表聯動與現金流品質檢查:識別賬面利潤陷阱:拿到結構化三表之後的第一個分析動作——現金流品質檢查。
- 多 Agent 投研流水線:收集、分析、報告三段分工:把定位、抽取、校對拆給多個 Agent 分工執行的架構設計。