Agentic Research
首頁/模板/300 頁年報轉結構化 JSON:三表與分部數據

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

2026/09/3022 分鐘君澤智庫最後更新 2026/09/30
這篇屬於模板主題財報解析OCRJSON港股

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 假設與敏感度分析:讓估值可被挑戰)都應該只消費帶狀態標籤的數據。

下一步