EDINET APIをPythonで使う|有価証券報告書を検索して取得する
/ 9 min read
Table of Contents
はじめに
上場企業の開示書類は、金融庁のEDINETからAPIで取得できます。
有価証券報告書をPDFで1社ずつ開いて数字を拾う作業は、EDINET APIとPythonに置き換えられます。この記事では書類を検索して、XBRL入りのZIPを手元に落とすところまでを扱います。
取得したファイルから財務数値を取り出す手順は、別記事のEDINETのCSVから財務数値を取り出すに分けました。
準備:APIキーを発行する
EDINET API v2はAPIキーが必須です。
- EDINETの閲覧サイトから「ログイン」でアカウントを作成する
- APIキー発行画面でキーを発行する
初回に発行したキーを継続して使えます。キーはコードに直書きせず、環境変数に置いてください。
$ export EDINET_API_KEY="発行されたキー"書類一覧APIで「その日に何が出たか」を調べる
エンドポイントはこれだけです。
https://api.edinet-fsa.go.jp/api/v2/documents.jsonパラメータは3つです。
| パラメータ | 必須 | 内容 |
|---|---|---|
date |
○ | ファイル日付(YYYY-MM-DD)。1日単位で指定 |
type |
- | 1=メタデータのみ(既定)、2=提出書類一覧及びメタデータ |
Subscription-Key |
○ | 発行したAPIキー |
一覧が欲しいときは type=2 です。指定できる日付は当日以前で、直近の財務局営業日の24時において10年を経過していない日付まで。土日祝日も指定できます。
import os
import requests
BASE = "https://api.edinet-fsa.go.jp/api/v2"API_KEY = os.environ["EDINET_API_KEY"]
def fetch_document_list(date: str) -> dict: """指定日の提出書類一覧を取得する""" res = requests.get( f"{BASE}/documents.json", params={"date": date, "type": 2, "Subscription-Key": API_KEY}, timeout=30, ) res.raise_for_status() return res.json()
data = fetch_document_list("2026-06-25")print(data["metadata"]["resultset"]["count"], "件")いちばんの落とし穴:HTTPステータスは常に200
ここが最初に引っかかるところです。
パラメータを間違えても、APIキーが無効でも、HTTPステータスは 200 が返ります。 実際の結果は、レスポンスJSONの metadata.status を見ないと分かりません。
| status | message | 意味 |
|---|---|---|
| 200 | OK | 成功 |
| 400 | Bad Request | パラメータの内容や文字コードの誤り |
| 401 | Access denied due to invalid subscription key… | APIキーが無効、または未指定 |
| 404 | Not Found | データが存在しない |
| 429 | Too Many Requests | 短時間に大量のリクエスト |
| 500 | Internal Server Error | サーバ側のエラー |
raise_for_status() だけでは異常を検知できないので、metadata.status を明示的に確認します。
def fetch_document_list(date: str) -> dict: res = requests.get( f"{BASE}/documents.json", params={"date": date, "type": 2, "Subscription-Key": API_KEY}, timeout=30, ) res.raise_for_status() # これだけでは足りない payload = res.json()
status = payload["metadata"]["status"] if status != "200": message = payload["metadata"]["message"] raise RuntimeError(f"EDINET API error: {status} {message}")
return payload401が返っていることに気づかず「なぜか0件しか取れない」と悩む、というのが典型的なハマり方です。
有価証券報告書だけに絞り込む
一覧の1件1件には、こういう情報が入っています。
{ "seqNumber": 1, "docID": "S1000001", "edinetCode": "E10001", "secCode": "10000", "filerName": "エディネット株式会社", "docTypeCode": "120", "periodStart": "2022-04-01", "periodEnd": "2023-03-31", "submitDateTime": "2023-06-12 12:34", "docDescription": "有価証券報告書-第1期(令和4年4月1日-令和5年3月31日)", "xbrlFlag": "1", "csvFlag": "1", "withdrawalStatus": "0", "legalStatus": "1"}絞り込みで使う項目はこのあたりです。
| 項目 | 用途 |
|---|---|
docTypeCode |
書類種別。有価証券報告書は "120" |
secCode |
証券コード。特定の会社を追うときに使う |
xbrlFlag |
"1" ならXBRLあり |
csvFlag |
"1" ならCSVあり |
withdrawalStatus |
"0" 以外は取下書/取り下げられた書類 |
docID |
書類取得APIに渡す書類管理番号 |
取下げ済みの書類を除外し忘れると、後の集計に無効なデータが混ざります。withdrawalStatus == "0" は入れておいたほうが安全です。
def filter_annual_reports(payload: dict) -> list[dict]: """有価証券報告書かつXBRLありのものだけ抜き出す""" return [ doc for doc in payload["results"] if doc["docTypeCode"] == "120" and doc["xbrlFlag"] == "1" and doc["withdrawalStatus"] == "0" ]
docs = filter_annual_reports(data)for doc in docs: print(doc["docID"], doc["secCode"], doc["filerName"])書類取得APIでファイルを落とす
docID が分かれば、実ファイルを取得できます。
https://api.edinet-fsa.go.jp/api/v2/documents/{docID}type で欲しい形式を選びます。
| type | 内容 | 形式 |
|---|---|---|
| 1 | 提出本文書及び監査報告書(XBRLを含む) | ZIP |
| 2 | ||
| 3 | 代替書面・添付文書 | ZIP |
| 4 | 英文ファイル | ZIP |
| 5 | CSV(XBRLをCSVに変換したもの) | ZIP |
数字を機械的に扱いたいなら type=5 のCSVがいちばん近道です。生のXBRLを解析したい場合は type=1 を選びます。
成功判定はContent-Typeで行う
書類取得APIも、成功・失敗どちらもHTTPステータスは 200 です。しかも本文には何かしらのデータが入るため、中身だけでは判別できません。
そこでレスポンスヘッダの Content-Type を見ます。
| Content-Type | 意味 |
|---|---|
application/octet-stream |
ZIP取得成功 |
application/pdf |
PDF取得成功 |
application/json; charset=utf-8 |
失敗(本文はエラーJSON) |
from pathlib import Path
def download_document(doc_id: str, doc_type: int, out_dir: Path) -> Path: """書類を取得して保存する""" res = requests.get( f"{BASE}/documents/{doc_id}", params={"type": doc_type, "Subscription-Key": API_KEY}, timeout=60, ) res.raise_for_status()
# 成功/失敗はContent-Typeで判定する content_type = res.headers.get("Content-Type", "") if content_type.startswith("application/json"): raise RuntimeError(f"取得失敗 {doc_id}: {res.json()['metadata']}")
suffix = ".pdf" if content_type.startswith("application/pdf") else ".zip" out_dir.mkdir(parents=True, exist_ok=True) path = out_dir / f"{doc_id}{suffix}" path.write_bytes(res.content) return pathまとめて取得する
429(Too Many Requests)があるので、必ず間隔を空けます。
import time
out_dir = Path("edinet")
for doc in docs: try: path = download_document(doc["docID"], doc_type=5, out_dir=out_dir) print("saved:", path, doc["filerName"]) except RuntimeError as e: print("skip:", e) time.sleep(1) # 連続リクエストを避ける同じ書類を何度も落とさないよう、保存済みならスキップする処理を入れておくと、途中で止まっても再実行しやすくなります。
path = out_dir / f"{doc['docID']}.zip"if path.exists(): continueZIPの中身
type=1 で取得したZIPは、こういう構成になっています。
S1000001.zip├── PublicDoc/ 提出本文書(XBRL または インラインXBRL)、マニフェスト、タクソノミ└── AuditDoc/ 監査報告書(インラインXBRL)、マニフェスト、タクソノミEDINETの検索画面からダウンロードできるXBRLとは異なり、XbrlSearchDlInfo.csv は含まれません。この中身に相当する情報は、書類一覧APIのレスポンスに入っているので、そちらを保存しておけば足ります。
おわりに
EDINET APIは、エンドポイントもパラメータも素直です。詰まるとすれば次の2点でした。
- HTTPステータスが常に200で、実際の結果は
metadata.statusかContent-Typeを見ないと分からない - 取下げ済み書類を除外しないと、無効なデータが集計に混ざる
ここさえ押さえれば、あとは docTypeCode と secCode で欲しい書類に絞り込むだけです。取得したファイルから数値を取り出す手順は、EDINETのCSVから財務数値を取り出すに続きます。
