VPS Notes
セルフホスト

EDINET API の使い方: API キーの取得から Python で有報と財務データ (XBRL) を取るまで

EDINET API v2 で有価証券報告書を取得し、XBRL から売上高や営業利益を取り出すまでを、動くコードつきで解説します。キーが無効でも 200 が返る罠、単体決算・IFRS・銀行の「経常収益」など、8,480 本を取り込んで分かった XBRL の癖も。

edinetxbrlpython株式投資データ収集

本記事にはプロモーション (アフィリエイト広告) が含まれます。

「EDINET API の使い方」「Python で財務データを取りたい」で調べている人向けに、先に結論を置きます。

EDINET API は無料で、API キーを 1 つ取れば、上場会社の有価証券報告書を XBRL ごと取得できます。 呼び方は「日付を指定して書類一覧を取る」→「書類 ID で ZIP を取る」の 2 段です。売上高や営業利益を 1 社分出すだけなら、Python 80 行ほどで動きます。記事の 5 節に、そのままコピーして動くコードを置いています。

難しいのは取得ではなく、XBRL の読み方です。私は自宅のサーバーで毎晩 EDINET から有報を取り込んでいて、2025 年 1 月以降の分で ZIP 8,480 本、財務の数値 610 万行になりました。その間に踏んだ罠を「つまずいた点」にまとめています。キーが無効でもエラーにならない、単体決算の会社は何も取れない、銀行の「経常収益」を利益と取り違える、の 3 つは、最初に知っておくと同じところで止まらずに済みます。

この記事は、決算データを自動で集めて AI に読ませる「投資判断システム」を自宅サーバーで作る話の 1 本目です。

EDINET API とは

EDINET は、金融庁が運営する「金融商品取引法に基づく開示書類」の電子開示システムです。有価証券報告書 (有報)、半期報告書、大量保有報告書などが、提出された日にここに載ります。

EDINET API は、その書類をプログラムから取るための API です。今の版は v2 で、利用には API キーが要ります。料金はかかりません。

似たものに TDnet (東証の適時開示) と J-Quants (JPX の株価・財務データの API) があります。決算短信は TDnet、有報は EDINET、と覚えておくと混乱しません。

この記事で使う環境

毎晩のバッチは、ラズパイでなくても、常時動いている Linux があれば動きます。自宅にサーバーを置けないなら VPS が選択肢です。

XServer VPS クラウド
毎晩の取得バッチを自宅以外で動かすなら。4GB (4 コア) で月 2,480 円〜。私の環境では、2025 年 1 月以降の有報の ZIP だけで 6.8GB でした。DB に取り込むなら、その分のディスクも見ておいてください。
公式サイトを見る

1. API キーを取る

API キーは、アカウントを作ってログインすると発行されます。手順は金融庁の EDINET API 仕様書 (Version 2) の「2-3 アカウントの作成と API キーの発行について」に、画面つきで載っています (2026 年 6 月版で確認)。流れはこうです。

  1. ブラウザのポップアップを許可する。https://api.edinet-fsa.go.jp をポップアップ許可サイトに追加します。キーの発行画面はポップアップで開くので、これをしないと画面が出ません
  2. アカウントを作る。EDINET の閲覧サイトで「ログイン」→「今すぐサインアップ」。メールアドレスに確認コードが届くので入力し、パスワードを決めます。パスワードは 12 文字以上で、小文字・大文字・数字・記号のうち 3 種類以上が必要です
  3. 多要素認証を登録する。電話番号を入れ、SMS か自動音声で本人確認します
  4. 連絡先を登録すると、キーが表示される。氏名と電話番号 (ハイフンなし) を入れて「連絡先登録」を押すと、同じ画面に API キーが出ます。コピーして保存します

2 回目以降にログインすると、同じ画面に今のキーと「API キー再発行」ボタンが出ます。キーが漏れたときは、ここで作り直せます。

「ログインできない」ときは、仕様書に書かれている次の 3 つを確かめてください。

キーの発行画面には、登録した氏名と電話番号、それに API キーがそのまま表示されます。スクリーンショットを人に見せるときや、ブログに貼るときは気をつけてください。

キーは 1 つで、複数のマシンで使ってかまいません。私は Mac とラズパイで同じキーを使っています。

キーはリクエストのヘッダー Ocp-Apim-Subscription-Key に入れて送ります。

HEADERS = {"Ocp-Apim-Subscription-Key": API_KEY}

URL のクエリに Subscription-Key=... と付けても通ります。しかし、それをやると URL ごとログに残ります。私のバッチは httpx のログを cron の出力に流しているので、クエリに載せるとキーがログファイルに平文で残ってしまいます。ヘッダーで送ってください。

2. 書類一覧を取る

ベースの URL は https://api.edinet-fsa.go.jp/api/v2 です。まず、ある日に提出された書類の一覧を取ります。

GET /documents.json?date=2025-06-27&type=2

type=2 で、書類一覧とメタデータの両方が返ります。一覧は提出日単位でしか引けません。「2025 年の有報を全部」のような指定はできないので、期間が欲しいときは 1 日ずつ回します。土日祝も呼べて、提出がなければ空の配列が返ります。

一覧の 1 件には、書類の種類や会社の情報が入っています。有報を拾うための条件はこうです。

項目 条件 意味
docTypeCode 120 (有報) か 130 (訂正有報) 半期報告書は 160 / 170
secCode 空でない 証券コード。5 桁で末尾が 0 (13010 → 1301、130A0 → 130A)
xbrlFlag "1" XBRL がある。文字列の "1" / "0"
withdrawalStatus "0" 取り下げられていない

提出日時の submitDateTime は "2025-06-27 15:00" の形で、秒もタイムゾーンもありません。

実際の件数の目安です (一覧の全件 / 有報 / 有報かつ証券コードあり)。

日 全書類 有報 上場会社の有報
2025-06-25 (1 年で一番多い日) 1,807 489 414
2025-09-10 (閑散期) 317 43 1

3 月決算の会社が多いので、有報は 6 月下旬に集中します。1 年分 (2025-04〜2026-03) の上場会社の有報を月別に数えると、6 月が 2,284 件、3 月が 543 件、ほかの月は 44〜221 件でした。

3. 書類を ZIP で取る

一覧の docID で、書類本体を取ります。

GET /documents/S100W7QE?type=1

type=1 で、XBRL 一式の ZIP が返ります。1 本の大きさは平均 0.8MB、最大で 5.6MB でした。取得には 1 本 2〜3 秒かかります。

ZIP の中身は、ある有報 (圧縮 690KB、展開すると 9.5MB、59 ファイル) でこうなっていました。

XBRL/PublicDoc/
├── jpcrp030000-asr-001_XXXXX-000_2025-03-31_01_2025-06-27.xbrl   インスタンス。数値の本体 (4.0MB)
├── *_pre.xml     表示リンク。勘定科目の並び順と階層
├── *_lab.xml     ラベル (会社が独自に足した科目の名前)
├── *_cal.xml     計算リンク (足し算の関係)
├── *_def.xml     定義リンク
├── *.xsd         スキーマ
├── 0101010_honbun_*_ixbrl.htm   本文 (企業の概況)
├── 0102010_honbun_*_ixbrl.htm   本文 (事業の状況、事業等のリスク)
└── 0104010_honbun_*_ixbrl.htm   本文 (株式と大株主)
XBRL/AuditDoc/    監査報告書。財務を読むときは見ない
fuzoku/*.gif      図

数字が欲しいなら .xbrl のインスタンス、文章が欲しいなら honbun の HTML です。

4. XBRL から売上高と営業利益を取り出す

XBRL は、1 つの数値の情報が 3 か所に分かれています。

売上高や営業利益を 1 社分出すだけなら、インスタンスだけで足ります。要素の名前 (ローカル名) と contextRef で拾います。

# 当期・連結全体の売上高
if local == "NetSales" and el.get("contextRef") == "CurrentYearDuration":
    sales = el.text

要素の名前空間の URI で引かないのがコツです。URI にはタクソノミの年度が入っていて (.../jppfs/2024-11-01/... のように)、年度が変わると固定の URI では外れます。ローカル名 (NetSales) で拾えば、年度をまたいでも動きます。

5. 動くコード

ここまでをまとめた、最小のコードです。指定した日に提出された有報の 1 件目を取って、売上高・営業利益・経常利益・純利益を出します。

# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "lxml"]
# ///
"""EDINET API v2 で、ある日に提出された有報を 1 件落として売上高と営業利益を出す."""

import io
import os
import sys
import time
import zipfile

import httpx
from lxml import etree

BASE = "https://api.edinet-fsa.go.jp/api/v2"
API_KEY = os.environ["EDINET_API_KEY"]
HEADERS = {"Ocp-Apim-Subscription-Key": API_KEY}  # クエリに載せるとログに残る

# 取り出したい要素 (ローカル名) とラベル。日本基準の連結 PL
TARGETS = {
    "NetSales": "売上高",
    "OperatingIncome": "営業利益",
    "OrdinaryIncome": "経常利益",
    "ProfitLossAttributableToOwnersOfParent": "親会社株主に帰属する当期純利益",
}


def list_documents(day: str) -> list[dict]:
    r = httpx.get(f"{BASE}/documents.json", params={"date": day, "type": 2},
                  headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    # エラーでも HTTP 200 で返ってくる。本文を見る
    if "StatusCode" in body:
        sys.exit(f"API エラー: {body['StatusCode']} {body.get('message')}")
    if body["metadata"]["status"] != "200":
        sys.exit(f"API エラー: {body['metadata']}")
    return body["results"] or []


def download_xbrl_zip(doc_id: str) -> bytes:
    r = httpx.get(f"{BASE}/documents/{doc_id}", params={"type": 1},
                  headers=HEADERS, timeout=120)
    r.raise_for_status()
    return r.content


def read_facts(zip_bytes: bytes) -> dict[str, str]:
    with zipfile.ZipFile(io.BytesIO(zip_bytes)) as z:
        # 本文の XBRL は XBRL/PublicDoc/ の下に 1 本。AuditDoc は監査報告書
        name = next(n for n in z.namelist()
                    if n.startswith("XBRL/PublicDoc/") and n.endswith(".xbrl"))
        root = etree.parse(z.open(name)).getroot()

    found = {}
    for el in root.iter():
        if not isinstance(el.tag, str):
            continue
        local = etree.QName(el).localname
        # CurrentYearDuration が当期・連結全体。_ が付くとセグメント別や単体になる
        if local in TARGETS and el.get("contextRef") == "CurrentYearDuration":
            found[TARGETS[local]] = el.text
    return found


def main() -> None:
    day = sys.argv[1] if len(sys.argv) > 1 else "2025-06-27"
    docs = [d for d in list_documents(day)
            if d["docTypeCode"] == "120" and d["secCode"] and d["xbrlFlag"] == "1"]
    print(f"{day}: 上場会社の有報 {len(docs)} 件")
    if not docs:
        return

    doc = docs[0]
    time.sleep(1)  # 連続で叩かない
    facts = read_facts(download_xbrl_zip(doc["docID"]))
    print(doc["docID"], doc["secCode"], doc["filerName"], doc["periodEnd"])
    for label, value in facts.items():
        print(f"  {label}: {int(value):,} 円")


if __name__ == "__main__":
    main()

先頭のコメント (# /// script) は、依存ライブラリをファイルの中に書く書式 (PEP 723) です。uv があれば、インストールなしで動きます。

EDINET_API_KEY=あなたのキー uv run edinet_min.py 2025-06-27

pip なら pip install httpx lxml してから python edinet_min.py 2025-06-27 です。

2025-06-27: 上場会社の有報 286 件
S100W7QE 17110 株式会社SDSホールディングス 2025-03-31
  売上高: 4,035,492,000 円
  営業利益: -14,691,000 円
  経常利益: -97,208,000 円
  親会社株主に帰属する当期純利益: -151,714,000 円

このコードが読めるのは、日本基準で連結決算をしている会社だけ です。IFRS の会社は要素名が違い (jpigp_cor_RevenueIFRS など)、単体決算の会社は contextRef が CurrentYearDuration_NonConsolidatedMember になるので、何も出ません。次の「つまずいた点」に、それぞれの直し方を書いています。

6. 毎晩取り込むときの工夫

1 社を取るだけなら上のコードで十分です。毎晩のバッチにするときは、次の点を入れています。

閑散期の夜間バッチは、ラズパイで一覧の取得が 3〜4 秒、解析が 1〜2 秒です。6 月のピークは実測できていませんが、1 本 2.5 秒と待ち 0.25 秒から計算すると、400 件で 18 分ほどです。

つまずいた点

取り込みを続ける中で踏んだものです。上から順に、知らないと時間を失う順に並べました。

取得から XBRL の読み取りまで、ここにある罠の多くは土日の 2 日で踏み終えました。訂正有報や REIT のように、後からデータを眺めていて気づいたものもあります。

次にやること

まとめ

XServer VPS クラウド
毎晩の取得バッチを 24 時間動かしておく場所として。自宅にサーバーを置けない人向けです。
公式サイトを見る