XBRLはXMLの方言なので、lxmlでパースすれば1日で終わると思っていた。
5日後、自分が何を知らなかったかを理解した。
この記事はEDINETの有報XBRLを全社分パースするシステムを作った記録。うまくいかなかった部分も全部書く。同じことをやろうとしている人が同じ穴に落ちないように。
Day 1: XMLでしょ?簡単でしょ?
EDINETから有報のZIPをダウンロードして展開すると、中にXBRLファイルがある。XMLだ。lxmlでパースして売上高のタグを探せばいい。30分で書ける:
from lxml import etree
tree = etree.parse("xbrl_file.xbrl")
# 名前空間を指定して売上高を探す
ns = tree.getroot().nsmap
revenue = tree.find(".//{*}NetSales")
print(f"売上高: ¥{int(revenue.text):,}")
# 売上高: ¥1,164,922,000,000
動いた。1社目は。
2社目を試す:
revenue = tree.find(".//{*}NetSales")
print(revenue)
# None
None。
1社目はJP-GAAP。2社目はIFRS。売上高のタグ名が違う。JP-GAAPの売上高タグはIFRS企業には存在しない。
Day 2: 名前空間地獄
日本の上場企業は3つの会計基準で有報を提出する:
- JP-GAAP(日本基準) — 約70%の企業
- IFRS(国際会計基準) — 約25%
- US-GAAP(米国基準) — 約5%
同じ「売上高」なのに、会計基準ごとにタグ名が違う。名前空間も違う。
しかも名前空間のURLにタクソノミーのバージョン年が含まれている。2023年提出と2024年提出でURLが変わる。ハードコーディングした名前空間はタクソノミー更新のたびに壊れる。
じゃあif文で3基準に対応すればいい?
# こうなっていく...
revenue = tree.find(".//{*}NetSales") # JP-GAAP?
if revenue is None:
revenue = tree.find(".//{*}Revenue") # IFRS?
if revenue is None:
revenue = tree.find(".//{*}Revenues") # US-GAAP?
if revenue is None:
# まだ他のパターンがある...
pass
3パターンでは足りない。製造業、サービス業、銀行、保険会社でタグ名が違う。売上高だけで6パターン以上。これが52フィールドある。
動的に読み取る必要がある。だが話はまだ始まったばかり。
Day 3: 同じ概念、違うタグ、3会計基準
売上高だけではない。営業利益、純利益、総資産、ROE — すべての財務フィールドで同じ問題が起きる。基準ごとにタグ名が違う。
しかも1つの有報の中に、同じ値が複数の場所に出現する。サマリーセクションと財務諸表本体で名前空間が違う。どちらを取るか判断が必要。
さらに罠がある。企業が会計基準を移行した場合、有報の中に新旧両方の基準の要素が残っている。当期データは新基準、比較データは旧基準。両方が同じ概念にマッピングされる。基準を考慮した重複排除がないと、今年の売上高ではなく数年前のデータを取得してしまう。
日本最大級の企業で、この誤差は数十兆円になる。エラーなくパースが完了する有報から。
Day 4: 数値パースの闘
XMLだからint(element.text)で終わるでしょ?
int("1,234,567") # ValueError: カンマ
int("-1,234,567") # ValueError: 全角マイナス
int("△1,234,567") # ValueError: 三角
int("▲1,234,567") # ValueError: 黒三角
int("(1,234,567)") # ValueError: 括弧
int("―") # ValueError: ダッシュ(意味: 該当なし)
int("…") # ValueError: 三点リーダ(意味: 同上)
マイナスの表現が5種類。「該当なし」を意味するダッシュ系文字が6種類以上。全角カンマと半角カンマの混在。すべての分岐は、実際にどこかの企業がそう提出したから存在する。設計時に想定したものは一つもない。
インラインXBRL(iXBRL)にはさらに罠がある。HTMLに表示されるテキストと、実際の値が異なるケース。スケーリング属性を見落とすと、データベースの全数値が100万分の1になる。既存のパーサーでこのバグを見たことがある。
古い有報はShift-JISエンコーディング。2015年以前のデータを扱うなら避けられない。
Day 5: これはアプリじゃない、インフラだ
ここまでで、1社のXBRLを正しくパースできるようになった。
問題は規模。
- 11,000社以上 × 15年分 × 3会計基準 × 2フォーマット(従来XBRL + iXBRL)
- 数百のXBRL要素を52の正規化フィールドにマッピング
- 連結と個別(非連結)の判別
- 30分ごとの新規提出チェック
1社のスクリプトを書くのと、全社が確実に動くシステムを作るのは全く別のエンジニアリング。
コンテキスト解決
すべてのXBRL値にはコンテキスト識別子がある。1件の有報に同じ要素が10以上のコンテキストで出現する — 当期連結、当期個別、前期、セグメント別、地域別…
間違ったコンテキストを取ると間違った数値になる。日本最大級の企業で連結と個別の差は10兆円を超える。
正しいコンテキストを選ぶ優先順位ロジックを構築するまでに、生の有報を数百件読んだ。しかもそのロジックは一律ではない — 連結決算を作成しない単体決算企業では、いわゆる「個別」データが唯一の正しいデータになる。一律に除外すると、11,000社のうち7,000社以上がゼロになる。
タクソノミーの継続的メンテナンス
金融庁が公開するXBRLタクソノミーは毎年更新される。要素名が変わる。要素が非推奨になる。新しい要素が登場する。
1つの財務概念に対して、会計基準ごと × 業種ごとに複数のバリエーションがある。製造業とサービス業と銀行と保険会社で、同じ「売上高」のタグ名が違う。
新しいフィールドを追加するたびに、3基準 × 複数の業種バリエーション × 15年分の過去タクソノミーに対してマッピングを確認する必要がある。
作ったもの
結局、パーサーは約900行のPythonになった。LLMなし、機械学習なし。lxmlとタクソノミーマッピングだけ。
- 数百のXBRL要素 → 52の正規化フィールド
- 3つの会計基準(JP-GAAP / IFRS / US-GAAP)
- 2つのXBRLフォーマット(従来XBRL + iXBRL)
- 連結 / 個別の自動判定
- 会計基準移行を考慮した重複排除
テストは4,800行以上。パーサーの5倍。XBRLは正しい表現より間違った表現の方が多いので、テストの方が長くなる。
665の自動正確性チェック — すべて本番で壊れた実際のバグから再構成したシナリオ。テストは単調増加。まだ一つも削除していない。
カバレッジ:
| フィールド | 取得率 |
|---|---|
| 売上高 | 99.6% |
| 純利益 | 99.4% |
| 総資産 | 99.5% |
| 営業利益 | 97.2% |
残りの0.4%〜2.8%は、非標準的なXBRLを提出している企業。たとえばマイナーな独自拡張タクソノミーを使っている場合。
やり直すなら
iXBRLから始める。 最近の提出の約60%がiXBRL。古い有報のために先に別のアプローチで作ったが、逆順のほうが効率的だった。
マッピングの自動化に早く投資する。 最初の1週間はXBRL要素を手動で集めようとした。公式タクソノミーから自動抽出するパイプラインを先に作れば数日節約できた。
エッジケースのログをもっと。 想定外のコンテキストや欠損フィールドの警告を出す仕組みは、本番障害のデバッグ後に追加したものが常に一番役立った。最初から入れておけばよかった。
自分でパースしなくていい方法
このパーサーの結果をAxioraというAPIで公開している。約900行のパーサーが裏で動いて、3行で結果が返る:
from axiora import Axiora
client = Axiora() # 環境変数 AXIORA_API_KEY から読み取り
# IFRS企業もJP-GAAP企業も同じフィールド名
toyota = client.companies.retrieve_financials("7203", years=1)
print(f"トヨタ: ¥{toyota.data[0].revenue:,}")
# トヨタ: ¥48,036,704,000,000
keyence = client.companies.retrieve_financials("6861", years=1)
print(f"キーエンス: ¥{keyence.data[0].revenue:,}")
# キーエンス: ¥1,059,145,000,000
会計基準が違っても同じ.revenueフィールド。スケーリングもコンテキスト解決も数値パースも裏側で処理済み。11,000社以上、14万件以上の有報。
無料プラン。クレカ不要。1日1,000リクエスト。
参考
- PythonでEDINET財務データを取得する方法 — パースの苦労なしでデータを使う側の話
- 金融庁 EDINETタクソノミー — 公式タクソノミーはここから
- Axiora API(無料)→ axiora.dev