核心命題: 港股併購盡職調查最耗時的環節是什麼?不是分析,是手動從年報 PDF 抄寫財務數字。PaddleOCR 能否自動化這個過程?
數據: 81K+ Stars · 102 文字區域 · 96.5% 平均信心度 · 83 秒三表提取 · 0 誤差關鍵數字
方法論: Clone → venv → Transformers 引擎 → PP-OCRv5 測試 → PP-StructureV3 KMeans Bug 分析 → 自建 DBSCAN Parser → 端到端 Pipeline → 封裝為 AK-OCR 技能
前言:年報數據提取 — 併購盡調的瓶頸
在港股併購前盡職調查中,我們需要從目標公司的年報中提取大量財務數據:損益表(P&L)、資產負債表(BS)、現金流量表(CF)。傳統做法是:
- 從港交所披露易(hkexnews.hk)下載年報 PDF
- 人手翻到財務報表頁面
- 逐行抄寫數字到 Excel
- 反覆核對確保無誤
一份 200 頁的年報,單是提取三張主表就需要 15-20 分鐘,而且容易出現人為抄寫錯誤。
PaddleOCR — 百度出品的開源文檔 AI 引擎(81K+ Stars,Apache 2.0)— 提供了三種核心能力:
| 模組 | 功能 | 規模 |
|---|---|---|
| PP-OCRv5 | 100+ 語言純文字識別 | CPU 即可運行 |
| PaddleOCR-VL-1.6 | 0.9B 參數 VLM,OmniDocBench 96.33% SOTA | 多模態文檔理解 |
| PP-StructureV3 | 表格 / 公式 / 印章 / 圖表全要素結構化 | 完整版面分析 |
我們的目標:用 PaddleOCR 自動化港股年報的財務數據提取。
一、環境搭建:macOS 上的 Transformers 引擎
PaddleOCR 3.x 支援兩種推理引擎:PaddlePaddle 和 Transformers(HuggingFace)。
macOS(Apple Silicon)沒有官方的 PaddlePaddle wheel,但 Transformers 引擎完美支援。只需:
git clone https://github.com/PaddlePaddle/PaddleOCR.git
cd PaddleOCR
python3 -m venv venv && source venv/bin/activate
pip install "paddleocr[all]" transformers torch torchvision
全部依賴安裝完成後,環境僅需約 1.5GB 磁盤空間。
二、PP-OCRv5 首戰:00928 帝王國際投資 FY2025 P&L 頁
測試目標:00928.HK(帝王國際投資)FY2025 年報第 90 頁 — 綜合損益及其他全面收益表。
from paddleocr import PaddleOCR
ocr = PaddleOCR(
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=False,
engine="transformers",
lang="ch", # 中文 + 英文混合
)
result = ocr.predict("page_90.png")
for res in result:
res.save_to_json("output") # 保存結構化結果
結果
| 指標 | 數值 |
|---|---|
| 文字區域檢測 | 102 項 |
| 平均信心度 | 96.5% |
| 低信心 (<85%) | 5/102 (4.9%) |
| 首次推理耗時 | ~30 秒(含模型下載) |
| 後續推理耗時 | ~27 秒/頁 |
關鍵財務數字全部準確:
| 項目 | 年報原件 | OCR 擷取 | 匹配 |
|---|---|---|---|
| Revenue | 40,765 | 40,765 | ✅ |
| Cost of Sales | (28,033) | (28,033) | ✅ |
| Gross Profit | 12,732 | 12,732 | ✅ |
| Loss Before Tax | (44,906) | (44,906) | ✅ |
| Loss for Year | (47,454) | (47,454) | ✅ |
5 個低信心項目 主要來自單一字元或黏連英文字:
|(0.650) — 表格分隔符\|I(0.328) — 應為11(附註編號)I,895(0.904) — 應為1,895
這些錯誤完全可以通過正則修正表自動處理。
三、PP-StructureV3 的 KMeans Bug:技術根因分析
PP-OCRv5 只能提取文字,不能理解表格結構(行列關係)。PP-StructureV3 是專門為表格結構化設計的 pipeline。
但我們在 Transformers 引擎上運行 PP-StructureV3 時遇到了致命錯誤:
sklearn.utils._param_validation.InvalidParameterError:
The 'n_clusters' parameter of KMeans must be an int in the range [1, inf).
Got 0 instead.
根因追蹤
錯誤發生在 paddlex/inference/pipelines/table_recognition/pipeline_v2.py 第 478 行:
# 呼叫鏈
cells_det_results_reprocessing()
→ combine_rectangles(cells + ocr_miss_boxes, html_pred_boxes_nums)
→ KMeans(n_clusters=N) # N = html_pred_boxes_nums
SLANeXt(表格結構預測模型)在 Transformers 引擎下,預測出的 HTML 表格結構有 html_pred_boxes_nums = 0——即一個 cell 都沒有檢測到。
傳入 KMeans 的 N=0 → scikit-learn 直接拋出 InvalidParameterError。
為什麼會這樣?
PaddleOCR 官方文檔明確寫明:「目前部分模型尚在支持中」—— SLANeXt 表格結構模型對 Transformers 引擎(PyTorch backend)的適配尚未完成。原生設計是給 PaddlePaddle 引擎使用的。macOS 沒有 PaddlePaddle → 這條路暫時不通。
四、自建 Table Parser:DBSCAN 行列分群
既然 PP-StructureV3 不可用,我們需要一個替代方案。核心思路:利用 OCR 返回的 bounding boxes 坐標,用 DBSCAN 聚類重組行列關係。
算法設計
Step 1: Y 軸 DBSCAN 分群 → 「行」
→ 所有 Y 座標中點相近的文字盒歸為同一行
Step 2: 每行內 X 軸排序 → 「列」
→ 同一行內按 X 座標排序
Step 3: 中英雙語合併
→ 港股年報特點:同一單元格有中英文兩行
→ X 軸距離 <120px 的非數字配對合併
Step 4: Column Grid Detection
→ 全局 X 軸 DBSCAN 建立統一的 column boundary
完整源碼(table_parser_v2.py):
import numpy as np
from sklearn.cluster import DBSCAN
from collections import defaultdict
def parse_financial_table(ocr_json, eps_y=14, eps_x=60):
texts = [fix_ocr_text(t) for t in ocr_json['rec_texts']]
boxes = ocr_json['rec_boxes']
# Step 1: Y-axis clustering → rows
y_centers = np.array([(b[1]+b[3])/2 for b in boxes]).reshape(-1,1)
labels = DBSCAN(eps=eps_y, min_samples=1).fit(y_centers).labels_
rows = defaultdict(list)
for i, label in enumerate(labels):
rows[label].append({
'text': texts[i], 'score': scores[i],
'x_center': (boxes[i][0]+boxes[i][2])/2,
'y_center': (boxes[i][1]+boxes[i][3])/2,
})
# Step 2: Sort rows by Y, sort items within row by X
sorted_rows = []
for label, items in rows.items():
items_sorted = sorted(items, key=lambda it: it['x_center'])
items_sorted = merge_bilingual(items_sorted, eps_x=120)
sorted_rows.append({
'row_y': np.mean([it['y_center'] for it in items_sorted]),
'cols': items_sorted,
})
sorted_rows.sort(key=lambda r: r['row_y'])
# Step 3: Global column grid
all_x = np.array([it['x_center'] for r in sorted_rows
for it in r['cols']]).reshape(-1,1)
x_labels = DBSCAN(eps=eps_x, min_samples=2).fit(all_x).labels_
# Step 4: Assign each item to its column
# ... (full code in repo)
效果
00928.HK 年報 P&L 頁:34 行 × 6 列,完全匹配原表格結構。
五、AK-OCR Pipeline:端到端自動化
將上述組件封裝為完整的 Pipeline:
PDF → PyMuPDF 拆頁(200dpi) → PP-OCRv5 → Table Parser → JSON/MD 輸出
端到端測試結果(00928 18M FY2025 年報)
| 階段 | 耗時 |
|---|---|
| Phase 1: PDF → 圖片 (3頁) | 0.8s |
| Phase 2: PP-OCRv5 文字識別 | 123.2s |
| Phase 3: Table Parser 結構化 | ~1s |
| Phase 4: JSON + MD 輸出 | <1s |
| 總計 | ~125s |
| 報表 | 文字區域 | 平均信心 | 結構化 |
|---|---|---|---|
| P&L (p.91) | 102 | 96.5% | 34行×6列 |
| BS (p.93) | 100 | 98.3% | 32行×6列 |
| CF (p.96) | 114 | 96.5% | 39行×5列 |
六、OCR 修正表:常見錯誤自動修正
PP-OCRv5 對中英文混合文檔的常見誤認模式:
OCR_FIXES = [
(r'\b3I\b', '31'), # "3I March" → "31 March"
(r'\bI,(\d)', r'1,\1'), # "I,895" → "1,895"
(r'\(2I\)', '(21)'), # "(2I)" → "(21)"
(r'\b\|I\b', '11'), # "|I" → "11"
(r'15,1\|4', '15,114'), # "15,1|4" → "15,114"
(r'diferencesarising', 'differences arising'),
(r'subsequentl y\b', 'subsequently'),
]
這些修正表可以隨著新的年報格式不斷擴充。
七、結論與下一步
核心價值
| 指標 | 手動提取 | AK-OCR Pipeline |
|---|---|---|
| 一份年報 P&L 提取 | ~15 min | ~30 sec |
| 準確率 | 人為誤差風險 | 96.5%+ |
| 可複用性 | 每次重做 | 一鍵批量 |
| 成本 | 人力 | 0(本地 CPU) |
已知限制
- PP-StructureV3 不可用:需等官方修復 Transformers 引擎適配,或改用 Docker PaddlePaddle 版
- OCR 字元誤認:數字
1↔I、黏連字 — 可通過修正表持續改善 - 頁碼偏移:年報頁碼 ≠ PDF index(差 ~1-2 頁),已內建自動 TOC 掃描
源碼位置
- Pipeline:
~/workspace/PaddleOCR/ak_ocr_pipeline.py - Table Parser:
~/workspace/PaddleOCR/table_parser_v2.py - 技能文檔:
~/workspace/skills/ak-ocr/SKILL.md
本文基於真實港股年報(00928.HK 帝王國際投資 FY2025)的實戰測試數據。所有 OCR 結果均可在本地復現。