【EDINET API】Python で XBRL を取得する方法【決算分析】

Python で XBRL ファイルを取得する方法を書きました。

EDINET API を使用した方法です。

自分の Python コードの書き方を簡単にまとめました。

XBRL は EDINET API で取得できました

XBRL は金融庁の EDINET APIエディネット エーピーアイ で取得することができました。

EDINET API の仕様書 (PDF) も公開されていましたので、自分はそれを見ながら Python コードを書きました。

仕様書は「EDINET(トップページ)⇒操作ガイド等⇒EDINET API関連資料」にある「EDINET API 仕様書 (zip)」の中にありました。

仕様書といっても、平たく言えば「指定された URL にアクセスするだけ」なので、難しくはなかったです。

仕様書の通りにリクエスト URL を作ってアクセスすると、json データ、zip データ、PDF データがダウンロードできる、という仕組みになっていました。

リクエスト URL を作ります

XBRL の「開示情報リスト(書類一覧)」の取得方法です。

「提出書類一覧及びメタデータ (type=2)」のリスエスト URL にアクセスします。

リスエスト URL の例です。

https://disclosure.edinet-fsa.go.jp/api/v1/documents.json?date=2019-04-01&type=2

(参考)EDINET API 仕様書⇒書類一覧 API のリクエスト URL(サンプル)⇒【 取得情報 = “2”(提出書類一覧及びメタデータ) 】p. 8

「メタデータのみ (type=1)」という URL もありましたが、こちらは通常は使う場面がなかったです。

日付を生成します

リクエスト URL には日付 (date=2019-04-01) が必要でした。

日付は Python の datetime(標準ライブラリ)を使用して生成しました。

ファイルは「日付が古いもの」から取得していくのが、少しだけおすすめです。

理由は「どんどん公開終了していくから」なのですが、書類によって公開期間が 5 年だったり 3 年だったりしたので、まあ、もう別にどこから取得してもいいような気もします。

例えば過去 5 年分だと「1826 日(=365 日×5 年+閏年うるうどし閏日うるうびの1日)」くらいの期間になりました。(追記)5年だと閏年の閏日を2回迎えるパターンがあるのを忘れていました。日数は適当に加減してくださいませ。(追記終わり)

日付の文字列の作り方です。

d_now = datetime.datetime.now() で現在の日時を取得します。

d_ は datetime 型の略です。

d_date = d_now - datetime.timedelta(days=1826) で 1826 日前にします。

念のため、さらに数日さかのぼってもいいかもしれません。

t_date = d_date.strftime('%Y-%m-%d') で日付の文字列にします。

t_ は text(str 型)の略です。

あとは days の数値を for 文と range() で変更していって、どんどん日付を変えていきます。

(Python) classmethod datetime.now(tz=None)

(Python) class datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

(Python) datetime.strftime(format)

(Python) '%Y-%m-%d' strftime() の書式コード

(Python) class range(start, stop[, step])

URL にアクセスします

EDINET API の URL には Python の requests(外部ライブラリ)を使用してアクセスしました。

r = requests.get(url, timeout) でアクセスします。

r は Response 型の略です。

r.content に jsonジェイソン 形式のデータが入っていました。

r.content は UTF-8 でエンコードされた bytes 型でした。

(Content-Type) application/json; charset=utf-8

(参考)EDINET API 仕様書⇒書類一覧 API(提出書類一覧及びメタデータ)⇒レスポンスヘッダ p. 12

(developer.mozilla.org) Content-Type – HTTP

(Requests) requests.get(url, params=None, **kwargs)

(Requests) property content

timeout の設定は必要か?(必要でした)

r = requests.get() のタイムアウトについてです。設定は必要でした。

「EDINET のサイト」や「自身のインターネット回線」がなぜか応答しない場合に備えて、timeout の引数には、何らかの秒数を設定します。

タイムアウトを設定しないと、さまざまな障害発生時に無限に待つことになってしまって、Python プログラムを適切に終了する方法が限られてしまいました。そして、サーバーやネット回線の障害は、わりと良くあることでした。なので、必要でした。

requests のマニュアルにも「timeout は設定するべき」という内容が載っていました。

(Requests) Timeouts

自分は timeout=30(秒)くらいを設定しています。

EDINET が正常でも、インターネット回線が不安定だったり遅い回線(1Mbps 未満とか)を使用している場合は、30 秒以内にダウンロードしきれず、タイムアウトエラーになるかもしれません。そのときは 60 秒とかに増やして様子を見るのがいいと思います。

stream=True や r.iter_content() は必要か?(お好みで)

r = requests.get()stream=Truer.iter_content() は使用しなくていいのか?

どちらでもいいと思います。stream=Truer.iter_content() を使うと少ないメモリ消費でファイルに保存できました(一方でファイルの断片化が起こりやすくなりました)。ですが、EDINET のファイルくらい(最大でも 100 MB くらい)なら、どちらでも問題はないと思います。お好みで。

(Requests) iter_content(chunk_size=1, decode_unicode=False)

(しかしながら、汎用的に使う受信プログラムでは、stream=Truer.iter_content() の組み合わせがほぼ必須でした。「ギガバイト単位のファイル」や「データが無限に受信できてしまう URL」を検知して、処理を止めるために必要になりました。)

ほかには、「レスポンスヘッダーを見てから本文 (content) を受信するか?しないか?」を制御したいときに、 stream=True が必要になりました。

やり方を書きました。

⇒ 【Requests】レスポンスヘッダーだけを取得するコード例【Python】

params 辞書は必要か?(お好みで)

r = requests.get() で params の辞書({'date': '2019-04-01', 'type': 2}{'type': 1} など)は使用しなくていいのか?

どちらでもいいと思います。

EDINET API の場合は「クエリ文字列 (query string) のパラメータ」が少なかった(date と type だけ)ので、どちらでも大して差が出ないと思います(コーディングの手間的にも性能的にも)。

自分は params の辞書を使わずに URL を作ってアクセスしました。お好みで。

(Requests) requests.get(url, params=None, **kwargs) Parameters: params

json を読み込みます

jsonジェイソン 形式のデータは Python の json(標準ライブラリ)を使用して読み込みました。

d = json.loads(r.content)

d は dict 型の略です。

d の辞書データの内容には「項目 ID」の文字列をキーしてアクセスします。

(項目 ID)metadata, …, processDateTime, …, results, seqNumber, docID, edinetCode, secCode, …, xbrlFlag, pdfFlag, attachDocFlag, englishDocFlag.

(参考)EDINET API 仕様書⇒書類一覧 API(提出書類一覧及びメタデータ)⇒出力データ内容⇒項目 ID p. 12

(Python) json.loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)

開示情報リスト(書類一覧)を保存します

json の内容は SQLiteエスキューライト 形式のデータベースファイル (.db) に保存しました。

Python の sqlite3(標準ライブラリ)で保存します。

開示情報リストのファイルサイズは、過去 5 年分をまとめたら、約 100 MB にもなりました(CSV 形式の場合)。

(さすがにこれは Excel や LibreOffice Calc で開くには厳しいサイズでした)

なので、CSV 形式ではなく、SQLite 形式などのデータベースがおすすめです。

自分は最初 CSV で頑張っていましたが、SQLite を試したら世界が変わりました。

開示情報の「ソート&抽出&重複排除」の処理が「簡単化&高速化」したことで、より高度な決算分析に進むことができました。

「やり方を変えただけでこんなにちがうのか」と思いました。

SQLite の使い方を書きました。

【Python】SQLite の使い方【sqlite3】

SQLite 形式の .db ファイルを Excel のように開くときは「DB Browser for SQLite」というソフトウェアが定番でした。

100 MB でも 1 GB でも余裕でした。(自分のPC環境では、1 TB くらいまでの .db が「DB Browser for SQLite」で実用的に開ける限界でした。)

(CSV でもストリームのシークを駆使して仕組みを作れば、必要なところだけを読み込むとかもできるのかもしれません。ですが、それは結局 SQLite のようなものを作るのと同じでした。)

開示情報リストの「表(テーブル)」を作る Python コード例です。

長いですが、「EDINET API 仕様書 (p. 12) 書類一覧 API(提出書類一覧及びメタデータ)」の表の通りに作るだけでした。

「メタデータ」の列と「提出書類一覧」の列については、ファイル (.db) を分けたほうが便利でした。

まず、「メタデータ」用の metalist テーブルにデータを追加するコード例です。

m_conn = sqlite3.connect(r'F://kabu/edinet/metalist.db')
m_c = cursor()
m_c.execute('''CREATE TABLE IF NOT EXISTS metalist (
    title TEXT,
    date TEXT,
    type TEXT,
    count TEXT,
    processDateTime TEXT,
    status TEXT,
    message TEXT
    )''')
m_conn.commit() # または m_c.connection.commit()
m_c.execute('REPLACE INTO metalist VALUES (?,?,?,?,?,?,?)', meta_datas)
m_conn.commit() # または m_c.connection.commit()
m_c.close()
m_conn.close()

次に、「提出書類一覧」用の doclist テーブルにデータを追加するコード例です。

d_conn = sqlite3.connect(r'F://kabu/edinet/doclist.db')
d_c = cursor()
d_c.execute('''CREATE TABLE IF NOT EXISTS doclist (
    fileDate TEXT,
    seqNumber TEXT,
    docID TEXT,
    edinetCode TEXT,
    secCode TEXT,
    JCN TEXT,
    filerName TEXT,
    fundCode TEXT,
    ordinanceCode TEXT,
    formCode TEXT,
    docTypeCode TEXT,
    periodStart TEXT,
    periodEnd TEXT,
    submitDateTime TEXT,
    docDescription TEXT,
    issuerEdinetCode TEXT,
    subjectEdinetCode TEXT,
    subsidiaryEdinetCode TEXT,
    currentReportReason TEXT,
    parentDocID TEXT,
    opeDateTime TEXT,
    withdrawalStatus TEXT,
    docInfoEditStatus TEXT,
    disclosureStatus TEXT,
    xbrlFlag TEXT,
    pdfFlag TEXT,
    attachDocFlag TEXT,
    englishDocFlag TEXT,
    PRIMARY KEY(fileDate, seqNumber, docID)
    )''')
d_conn.commit() # または d_c.connection.commit()
d_c.execute('REPLACE INTO doclist VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', doc_datas)
d_conn.commit() # または d_c.connection.commit()
d_c.close()
d_conn.close()

(注意です)

EDINET API の json では、縦覧じゅうらん(公開)が終了した部分に null"0" が設定されました。

(参考)EDINET API 仕様書⇒書類一覧 API⇒縦覧の終了 p. 24

なので、単純に "REPLACE INTO" していくと、過去のリストがどんどん消えていきました。

そういうわけで、実際にはそういったデータをスキップしながら、「開示中の情報だけ」を追加するように作ります(追加済みなら置換 or 新しい行に追加していくように作ります)。

(注意おわり)

XBRL ファイル (.zip) と PDF ファイル (.pdf) を取得します

XBRL と PDF を取得する URL の作り方です。

「書類管理番号 (docID)」に「リクエストパラメーター (type=1 or type=2 or type=3 or type=4)」をくっつけることで、それぞれ取得することができました。

たとえば「書類管理番号 (docID)」が「S1234567」だった場合です。

以下の URL を requests.get(url, timeout) に渡して、データを取得します。

  • 「提出本文書及び監査報告書」の XBRL (.zip)
    https://disclosure.edinet-fsa.go.jp/api/v1/documents/S1234567?type=1
  • 「提出本文書及び監査報告書」の PDF (.pdf)
    https://disclosure.edinet-fsa.go.jp/api/v1/documents/S1234567?type=2
  • 「代替書面・添付文書」の PDF など (.zip)
    https://disclosure.edinet-fsa.go.jp/api/v1/documents/S1234567?type=3
  • 「英文ファイル」の HTML など (.zip)
    https://disclosure.edinet-fsa.go.jp/api/v1/documents/S1234567?type=4

(参考)EDINET API 仕様書⇒書類取得 API⇒リクエストについて⇒エンドポイント p. 39

https://disclosure.edinet-fsa.go.jp/api/バージョン/documents/書類管理番号

(参考)EDINET API 仕様書⇒書類取得 API⇒リクエストについて⇒リクエストパラメータ p. 40

パラメータ名 type の設定値 (type=1 or type=2 or type=3 or type=4)

(参考)EDINET API 仕様書⇒書類取得 API⇒書類取得 API のリクエスト URL(サンプル)p. 41

https://disclosure.edinet-fsa.go.jp/api/v1/documents/S1234567?type=1

次に、取得したデータ (r.content) を「そのまま」ファイルに保存します。

r.content を「そのまま」ファイルに保存する Python コード例です。

※保存ファイルパスの「ファイル名」は r.headers["Content-Disposition"] から取得します(後述)。

from pathlib import Path
import requests
r = requests.get('https://example.com/')
file = Path(r'保存ファイルパス(フルパス)')
with file.open('wb') as f:
    f.write(r.content)

「書き込みバイナリモード」の 'wb' で開くのがポイントです。

これで、zip ファイルや PDF ファイルを保存することができました。

(Python) Path.open(mode='r', buffering=- 1, encoding=None, errors=None, newline=None)

(Python) open(file, mode='r', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

(Python) write(b)

(Requests) requests.get(url, params=None, **kwargs)

(Requests) property content

ファイル名を取得します

EDINET API の仕様書にはファイル名についての説明がないようでした。

ファイルをダウンロードするときのファイル名は r.headers["Content-Disposition"] から取得することができました。

これで、ウェブブラウザでアクセスしたときと同じような「ファイル名 (filename)」が取得できました。

r.headers["Content-Disposition"] の例です。

>>> import requests
>>> r = requests.get('https://disclosure.edinet-fsa.go.jp/api/v1/documents/S100MSG4?type=1', timeout=30)
>>> r.headers["Content-Disposition"]
'inline;filename="S100MSG4_1.zip"'

ここから Python の正規表現 re(標準ライブラリ)を使用して、ファイル名を抽出します。

ファイル名の抽出例です(正規表現の書き方はほかにもあると思います)。

>>> import re
>>> m = re.search(r'(?<=filename=")[^"]*(?=")', r.headers["Content-Disposition"], flags=re.IGNORECASE)
>>> filename = m.group(0)
>>> filename
'S100MSG4_1.zip'

後読あとよ(?<=...)先読さきよ(?=...) を使用して、ダブルコーテーション " で囲まれたファイル名を取得しました。

※念のため、自分はファイル名が「書類管理番号_TYPE番号」の形になっていることを、さらに別の正規表現でチェックしています。違っていたらエラーにしています。

これを保存フォルダパスにくっつければ「保存ファイルパス(フルパス)」の完成です。

ファイル名は、たとえば pathlib の .joinpath() メソッドでくっつけることができました。

(developer.mozilla.org) Content-Disposition – HTTP

(Python) re.search(pattern, string, flags=0)

(Python) Match.group([group1, ...])

(Python) re.IGNORECASE

(Python) 正規表現のシンタックス

(Python) PurePath.joinpath(*other)

取得ファイルの破損をチェックします

ファイルに保存したら、zip ファイルや PDF ファイルが壊れていないかをチェックします。

ノーチェックだと、どうしても1年間に数回は、気づかないまま壊れたファイルを保存していました。

たとえ「HTTP レスポンスステータスコード (r.status_code)」が「200 (OK)」であったとしてもです。

ダウンロード中になぜかファイルが壊れていたり、関係ないファイル(システムメンテナンス中のお知らせとか)を取得していた場合がありました。

仕様書にあった Content-Type の値で「成功/失敗」はほぼ判定できましたが、それでも原因がわからない場合もありました。

それでチェックするようになりました。

簡易的なチェックでもいいと思います。

あまり厳密さを追求すると、それはそれでキリがなかったです(特に PDF は)。

zip ファイルの破損をチェックします

zip ファイルの破損(エラー)は、Python の zipfile (標準モジュール) でチェックできました。

保存したファイルを

try:
    with zipfile.ZipFile(file, 'r') as z:
        pass
except zipfile.BadZipFile as e:
    print(f'{e.__class__.__name__} {file}')

で開けるか否かでチェックします。

壊れていたら zipfile.BadZipFile の例外を出してくれました。

実際にはさらに

try:
    with zipfile.ZipFile(file, 'r') as z:
        result = z.testzip()
        if result is not None:
            print(f'error {file}')
except zipfile.BadZipFile as e:
    print(f'{e.__class__.__name__} {file}')

として、resultNone を返せば OK、そうでなければエラーと判断します。

これで、「zip が完全に壊れている場合: BadZipFile」でも、「zip 内の一部のファイルが壊れている場合: z.testzip() is not None」でも、検出することができました。

異常があったら再取得します。

再取得してもエラーが出たら(まったく同じファイルだったら)、諦めてそのまま保存します(一応エラーはログファイルとかに記録しています)。

(Python) class zipfile.ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True, compresslevel=None, *, strict_timestamps=True)

(Python) ZipFile.testzip()

PDF ファイルの破損をチェックします

PDF ファイルの破損(エラー)は、QPDF というコマンドラインツール (qpdf.exe) でチェックできました。

Python の subprocess.run() から qpdf.exe を呼び出してチェックします。

QPDF の使い方を書きました。

PDF の破損をチェックするコード例【QPDF】

ただし、PDF に関しては、内部の構造的な「エラー」や「警告」がわりと良くあることでした。

一応、異常があったら再取得します。

再取得してもエラーが出たら(まったく同じファイルだったら)、諦めてそのまま保存します(一応エラーはログファイルとかに記録しています)。

XBRL は Arelle で読み込みます

XBRL から決算書の金額や文章を取り出すときは Arelleアレル(外部ライブラリ)が便利でした。

Arelle の使い方を書きました。

Arelle のインストール方法【XBRL 読み込みライブラリ】

XBRL から『勘定科目の金額や文章』を取得するコード例【Arelle】

Arelle は、勘定科目の「日本語ラベル」と「タグ名」が簡単に取得できるのが良かったです。

たとえば、「日本語ラベル」が「売上高」である勘定科目を列挙すれば、微妙な「タグ名の違い」を無視して、全上場企業の売上高の金額を集めることができました。

売上高に当たる「タグ名」のバリエーション(NetSales など)を集める作業も、グッと楽になりました。

以前は「EDINET XBRLの勘定科目タグ集約リスト」に書いたように、手作業でタグ名を集めていました。

それが、Arelle のおかげでとても簡単になりました(日本語ラベルをたどって大部分が自動で拾えるようになりました)。

Arelle はおすすめです。

Arelle で抽出した決算データは、SQLite のデータベースファイル (.db) にどんどん追加していきます。

自分は EDINET コードごとに決算情報の .db ファイルを分けて作っています。

あとは、企業ごとに(EDINET コードごとや証券コードごとに)売上高のグラフや当期純利益のグラフを描くだけです。

業績の推移をグラフにします

グラフを描くためには、まずデータの整理が必要でした。

決算データの整理には Python の pandas(外部ライブラリ)を使いました。

グラフの描画は Python の matplotlib(外部ライブラリ)で行いました。

グラフ画像は PNG 形式で保存しました(2021年の今なら webp 形式でもいいと思います)。

以上です

こうして、ついに全上場企業の業績がサクサク見られるようになりました。

「オリジナル四季報」の完成です。

自身が「見たいと思う視点」でグラフが作れるのは、やはり面白かったです。

何社かを選んで重ねてみても面白かったです。

もちろん、最初からいきなり複雑なグラフは描けませんでした。

最初は「売上高」だけのグラフを出すことに集中して、そこから少しずつグラフを充実させていきました。

SQLite、pandas、matplotlib の使い方も調べながら覚えていきました。

引き続きいろいろ試しています。

以上です。

上場企業の業績は証券口座でも見ることができます。自分は SBI 証券を使用しています。

⇒ 初めての人におすすめのネット証券と証券口座の作り方(無料)

タイトルとURLをコピーしました