iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0

documents 表只有 13 個欄位,但其中幾個是踩過雷才長出來的,還有一個是為了補洞而存在。

第一條原則:正文不進資料庫

CREATE TABLE IF NOT EXISTS documents (
  id                INTEGER PRIMARY KEY AUTOINCREMENT,
  exchange          TEXT NOT NULL,            -- 交易所: sse/szse/bj/ksc/koe/knx/jpx/twse/tpex
  stock_code        TEXT NOT NULL,
  stock_name        TEXT,                     -- 反正規化
  announcement_id   TEXT UNIQUE NOT NULL,     -- 冪等的衝突鍵, 不是 id
  doc_type          TEXT NOT NULL,
  report_period     TEXT,                     -- 講哪一期, 不是什麼時候發的
  title             TEXT NOT NULL,
  adjunct_url       TEXT,                     -- 原始 PDF
  content_path      TEXT NOT NULL,            -- 指向 R2, 正文不進資料庫
  word_count        INTEGER DEFAULT 0,        -- >0 正常 / 0 掃描件 / -1 失敗
  announcement_time INTEGER,                  -- 整數 timestamp
  parser_version    INTEGER DEFAULT 0,        -- 決定這篇要不要重轉
  created_at        TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

content_path 存的是路徑,不是內容,markdown 全文另外寫成檔案(雲端那份在 R2)。正文是「一次寫、多次讀、幾乎不改」的大物件——塞進關聯式資料庫,會讓備份、同步、查詢全部變重。資料庫只留索引:查「有哪幾篇」很快,要正文再去拿。

announcement_id:衝突鍵不是 id

id 是 AUTOINCREMENT 主鍵,但冪等的關鍵是 announcement_id 上的 UNIQUE 約束。寫入用 INSERT ... ON CONFLICT(announcement_id) DO UPDATE,所以同一篇公告重跑一百次,資料庫裡還是一列;中斷後重跑也只要問「這篇存在嗎」。

為什麼不用複合自然鍵(交易所+代碼+期間+文件類型)?因為同一家公司同一期可能發不只一次——更正、重編、補充——複合鍵會讓更正版跟原版撞在一起。來源給的公告 ID,才是那把唯一的鑰匙。

word_count 的三態語意

整張表最不直覺的地方:> 0 正常有內文、0 是掃描件(空的 markdown)、-1 是轉換失敗。用一個數值欄位編碼三種狀態,而不是多加一個 status 欄位——好處是少一個欄位、判斷邏輯集中在一支函式;壞處是可讀性很差,所以我把它寫進 docstring。

為什麼 0 不當成「失敗」?因為掃描件是已知且合理的結果:那篇就是沒有文字層,不是轉換壞掉。把它當失敗,它每一輪都會被重試,永遠試不完。

parser_version:什麼時候要重轉

解析器每次改版就遞增版本號,判斷「這篇要不要重轉」的規則是三條:沒有記錄、版本落後、或是有記錄但檔案不見了。但有個例外寫在 mark_parsed() 裡:如果只是換了演算法、內容其實沒變,就只把版本號提上去,不動內文也不動字數。 另外,word_count < 0 的記錄會被跳過,即使檔案不存在也不再重拉——因為那篇本來就沒有檔案。

symbol:為什麼要對齊外面的慣例

to_symbol() 把「交易所+代碼」轉成外面世界在用的寫法:上海 600519.SS、深圳 000001.SZ;韓國 KOSPI 用 .KS、KOSDAQ 用 .KQ、KONEX 用 .KN;日本用 .T;台灣上市 .TW、上櫃 .TWO

我自己發明一套更整齊的寫法(例如 sse:600519)對使用者沒有好處。 他們手上已經有一堆用這種後綴寫好的程式碼,我的 API 不該逼他們改。一致性要對齊「外面已經存在的東西」,不是對齊「我覺得漂亮的東西」。

刪除:兩份資料的必然代價

回到 Day 4 那個「雲端那份不會自己出現」的問題。它在刪除上最痛:同步程式原本只做 upsert。 我在本地清掉一份壞掉的正文,雲端那份永遠留著,網站繼續把它發給客戶,清理等於沒做。

補法是加一張墓碑表 deleted_docs:刪記錄時順手把 announcement_idcontent_path 寫進墓碑;同步程式每次推送前先「回放」墓碑,D1 按 ID 批次刪、R2 按精確路徑刪,刪完才清墓碑。墓碑是刪除當下就記下來的,精確而且零成本——不用把幾十萬筆 ID 從 D1 拉回來做全量比對。

刪除不會自己傳播,你得自己造一個傳播機制。 這個機制後來還有第二個用途,留到 Day 25 講合規下架時再說。

明天

表定完了,但這張表上有兩個欄位不像技術決定:api_keys 裡的 quotarate_limit。明天(Day 6)把 $0 和 $31 的推導過程攤開來。


上一篇
為什麼我選 D1(SQLite)而不是 PostgreSQL
系列文
30 天打造東亞財報全文 API:PDF 解析、亂碼修復、章節切分到 MCP 上線實錄5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言