Agentic Research

This article is not yet available in English. You are reading the Traditional Chinese original. The English edition will appear here once it is translated.

Browse articles that do have an English edition

300 頁年報轉結構化 JSON:三表與分部數據

2026/09/3022 min readBryan Chan閱讀中文原文
Topics財報解析OCRJSON港股AI Agent

PaddleOCR 實戰:83 秒提取港股年報財務數據 解決了「機器能不能把年報上的字讀出來」;這篇要解決下一個問題:讀出來的一堆文字,怎麼變成可查詢、可校驗、可回溯的結構化數據。 這兩個問題之間的距離,比多數人預期的遠——OCR 給你的是一頁頁帶座標的字串,而估值模型要的是「FY2025 收入,港幣千元,出自第 96 頁,已人工核對」這樣一個完整的數據點。

本文給出一條五階段管線:定位、抽取、對應、schema、勾稽。每個階段都有明確的輸入輸出與失效形態。讀完你應該能設計自己的年報結構化流程,並且知道每一步哪裡會出錯、錯了怎麼抓。

全景:管線的五個階段

階段輸入輸出主要風險
1. 定位整份 PDF三表與附註的頁碼範圍定位到錯誤的「類似頁面」
2. 抽取目標頁面(文字層或圖像)帶座標的原始儲存格跨頁表格接錯、行列錯位
3. 對應原始儲存格標準欄位+統一單位科目映射錯、口徑混淆
4. Schema標準欄位結構化 JSON(帶元資料)丟失來源座標與原始值
5. 勾稽結構化 JSON通過校驗的 JSON+失敗清單容差設計不當、校驗被繞過

設計原則先講在前面:每個階段的輸出都保留上一階段的痕跡。 JSON 裡要能找到原始抽取值與頁碼,對應表要能查到為什麼這個科目映射到那個欄位。管線出錯時,你要能定位是哪一階段錯的,而不是整段重跑然後祈禱。

階段一:定位財報頁

300 頁的年報裡,你要的目標分散在幾個固定區域:合併損益表、合併資產負債表(財務狀況表)、合併現金流量表、合併權益變動表,以及附註裡的分部資訊。定位的常用信號:

信號用法
目錄頁年報開頭通常有目錄,直接給出各節頁碼——但那是「印刷頁碼」,不是 PDF 頁碼
章節結構經審計財務報表本體通常緊接在獨立核數師報告之後、附註之前
關鍵詞掃描對每頁文字搜「綜合損益表」「財務狀況表」「現金流量表」「分部」等標題詞
頁眉頁腳財報章節的頁眉常與前後的董事會報告不同

三個必踩的坑:

  1. PDF 頁碼與印刷頁碼的偏移。 封面、目錄、公司資料佔掉前幾頁,之後兩個頁碼系統就差出一個常數。管線內部統一用 PDF 頁碼,輸出給人看的引用再換算成印刷頁碼,並把換算偏移量記錄在元資料裡。
  2. 同一張表在年報裡出現多次。 董事會報告會引用財務摘要、五年財務摘要會再列一遍主要數字、業績公告與年報也高度重疊。只認「經審計的財務報表本體」那一份——判斷依據是它處於核數師報告與附註之間的結構位置,而不是標題像不像。
  3. 分部資訊不在報表本體裡。 分部收入、分部利潤通常藏在附註(搜索「分部」或「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 是管線的合約。設計原則四條:

  1. 每個數值是一個物件,不是一個裸數字——至少包含原始值、來源頁碼、校對狀態三樣。
  2. raw 與 normalized 分離——原始抽取值永不被覆蓋,換算與修正都是新增欄位。
  3. 元資料完整——實體、財年期間、幣別、單位、來源文件(檔名與擷取日期)、管線版本。
  4. 校驗狀態顯式——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 + totalEquity1–2 個最小單位(捨入)抽取漏項、映射錯欄,或平衡項本身讀錯
2損益表小計revenue + costOfSales = grossProfit捨入容差小計行與明細行錯位
3現金勾稽現金流量表期末現金 ≈ 資產負債表現金及現金等價物較寬:注意外幣折算差與受限制現金不一定是錯——先讀附註的調節表,調節不上才是錯
4留存收益滾動期末 = 期初 + 本期歸屬利潤 − 股息 ± 其他調整需權益變動表與附註支持權益變動表抽取錯誤,或調整項遺漏
5分部對帳各分部收入合計 − 分部間抵銷 = 合併收入捨入容差分部資訊跨頁接續錯誤(階段二失效)
6EPS 勾稽基本每股盈利 × 加權平均股數 ≈ 歸屬母公司利潤較寬: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 假設與敏感度分析:讓估值可被挑戰)都應該只消費帶狀態標籤的數據。

下一步