上場企業の IR ページは、銘柄ごとに構造がまったく異なる。ある自動車メーカーは {FY}_{QNUM}q_summary_jp.pdf というパターンで URL を組み立てられるが、電機メーカーは毎年 URL 構造が変わり、ページをスクレイピングしてから PDF リンクを拾わなければならない。さらに会計基準が IFRS か JGAAP かによってパーサが参照すべき項目名も変わる。
汎用パーサをひとつ書けばいい、という発想は最初の数銘柄で通用する。しかし 50 銘柄、100 銘柄、最終的に 3,800 銘柄という規模を目指したとき、「銘柄ごとの個別仕様をコードで持つ」設計は確実に破綻する。
課題:銘柄別コードは線形に増殖する
最初はシンプルだ。パーサに銘柄コードの if 分岐を1本追加するだけで動く。しかしこの手法を続けると、コードは次のような構造に向かっていく。
if ticker == "XXXX":
url = f"https://example.com/ir/{year}_q{quarter}.pdf"
elif ticker == "YYYY":
# 毎年 URL が変わるのでページをスクレイプ
url = scrape_ir_page("https://example.com/ir/")
elif ticker == "ZZZZ":
# US-GAAP なので項目名が違う
profit = row["income_before_tax"]
...
分岐が 10 銘柄を超えた時点で、コードは銘柄仕様書の代替物になっている。新しいエンジニアがコードベースを読んでも「なぜこの銘柄だけここをスクレイプするのか」がわからない。仕様変更のたびにテストもなくコードを触ることになる。
アプローチ:銘柄別 YAML に「ここを見る」を書く
解決策は、銘柄ごとの個別仕様をコードの外に追い出すことだ。config/companies/ 配下に銘柄コードをファイル名とした YAML を置き、スクリプトは実行時にこれを読み込む。
# config/companies/6758.yaml(ある電機メーカーの例)
ticker: "6758"
company: "(ある電機メーカー)"
fiscal_month: 3
accounting_std: "IFRS"
scraper_strategy: "page_scrape"
ir_pages:
- url: "https://example.com/ir/library/"
pdf_link_pattern: "決算短信"
- url: "https://example.com/ir/library/archive/"
pdf_link_pattern: "決算短信"
notes: "IFRS 連結。IR サイト構造が複雑で URL パターン化困難。page_scrape で複数ページをクロール。"
一方、URL パターンが素直な銘柄は別の戦略を使う。
# config/companies/7203.yaml(ある自動車メーカーの例)
ticker: "7203"
company: "(ある自動車メーカー)"
fiscal_month: 3
accounting_std: "IFRS"
ir_url: "https://example.com/ir/financial-results/"
short_term_template: "https://example.com/ir/{FY}_{QNUM}q_summary_jp.pdf"
scraper_strategy: "url_template"
notes: "IFRS 連結。短信 PDF は {FY}_{QNUM}q パターン。過去年は EDINET 有報でカバー。"
scraper_strategy フィールドがスクリプトに「どのロジックを使うか」を伝える。コードは戦略名を見てルーティングするだけで、銘柄固有の知識は持たない。
共通スクリプト+個別 YAML の組み合わせ
スクリプト側の構造はシンプルになる。
def fetch_ir(ticker: str) -> str | None:
cfg = load_yaml(f"config/companies/{ticker}.yaml")
strategy = cfg.get("scraper_strategy", "edinet_only")
if strategy == "url_template":
return build_url_from_template(cfg)
elif strategy == "page_scrape":
return scrape_ir_page(cfg)
elif strategy == "edinet_only":
return None # EDINET フォールバックを使う
YAML を追加するだけで新銘柄に対応できる。コードの改変は不要だ。新しい戦略が必要になったとき(たとえば js_render のような SPA 対応)だけ、スクリプト側に1つの elif ブランチを追加する。
信頼度が低い YAML には自動生成コメントを残しておくことで、手動確認が必要な銘柄を一目で識別できる。
accounting_std: null # 要確認: JGAAP / IFRS / US-GAAP
scraper_strategy: "edinet_only"
notes: "自動生成 (信頼度=low). 動作確認後にこのコメントを削除すること。"
「端の挙動」を YAML 側で吸収するパターン
規模が増えると、スクリプトが想定しない「端の挙動」が必ず出てくる。たとえば「会計基準は IFRS だが営業利益の行が有価証券報告書にない」ケースや、「複数ページの IR アーカイブをクロールしなければならない」ケースなどだ。
これらを notes フィールドと追加フィールドの組み合わせで YAML 側に吸収する。
# ある自動車メーカーの例(簡略)
accounting_std: "IFRS"
notes: "IFRS 連結。営業利益は有報に記載なし(税引前利益のみ)。"
パーサは notes をログに出力するだけで、この情報を処理に使わない。「なぜこの銘柄だけ結果が違うのか」を調査するときに notes が手がかりになる。コードに「この銘柄はこうする」と書くのではなく、「この銘柄はこうなっている」を YAML に書く、という思想の差だ。
スクレイパー戦略レベルで対応できないケースは、ir_page_selector や pdf_link_pattern など、より細粒度のオーバーライドフィールドを追加して吸収する。フィールドを増やすのは許容するが、分岐をコードに戻さない、という原則を守る。
運用してわかった効果と落とし穴
効果。銘柄を追加するとき、YAML を1ファイル書くだけで済む。スクリプトのリグレッションが発生しない。Claude Code に「この銘柄の YAML を書いて」と頼めば、既存の YAML を参照して正しい構造で生成してくれる。IR ページの URL が変わったとき、担当者が YAML を1行直すだけで対応が完結する。コードレビューが不要だ。
落とし穴その1。YAML に何でも書きすぎると、スクリプトが「設定を全部読んで条件分岐する」という別の複雑さを持つようになる。フィールドを増やすときは「このフィールドをスクリプトが使う必要があるか、それとも notes に書けば十分か」を必ず問う。
落とし穴その2。自動生成した YAML の信頼度は低い。IR ページのスクレイピングで URL パターンを推測した場合、実際に動くかどうかは手動確認が必要だ。信頼度コメントを残しておかないと、未検証の YAML が「正しい仕様」として扱われるリスクがある。
落とし穴その3。YAML の構造に破壊的変更を加えると、過去の全ファイルが無効になる。フィールドを廃止するときは古いフィールド名も一定期間サポートする互換レイヤーを挟むか、一括マイグレーションスクリプトを書く。スキーマバージョン管理の重要性は、銘柄数が増えるほど大きくなる。
まとめ
銘柄別 YAML 駆動の設計は、「コードが知っていい情報」と「設定ファイルが知っていい情報」の境界を引くことで成立する。コードはロジックを持ち、YAML は仕様を持つ。この分離が銘柄数が増えても維持可能な収集基盤の土台になっている。
この設計思想は XBRL パーサの構造とも連動している。XBRL パーサの設計については 決算短信 XBRL パーサの設計 で詳述している。銘柄別設定が効いてくる背景にある収集基盤全体の設計原則は データ収集と利用を分離する で扱っている。また、コードに設定値を持たせないという思想は ハードコード禁止と config 駆動設計 と根を同じくする。