skip to content
barorin&

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キーが必須です。

  1. EDINETの閲覧サイトから「ログイン」でアカウントを作成する
  2. APIキー発行画面でキーを発行する

初回に発行したキーを継続して使えます。キーはコードに直書きせず、環境変数に置いてください。

Terminal window
$ 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 payload

401が返っていることに気づかず「なぜか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 PDF PDF
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():
continue

ZIPの中身

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から財務数値を取り出すに続きます。

この記事を書いた人

barorinのプロフィール画像

barorinCPA & Engineer

会計とITの二足のわらじで働く生き物。Pythonを中心に、会計・監査の実務で使えるコードや、Ubuntu・Docker等の設定メモなどを書いています。