📊

企業の財務情報を自動収集したい!

2023/04/12に公開

本記事の目的

プログラミングを学習しはじめたら、誰しも一度は「株の自動売買で食っていきたい!」のようなことを考えるのではないでしょうか。そのための第一歩として、企業の財務情報を収集するのはとても大切ですよね(あなたがファンダメンタルズ分析を信じるならば、ですが...)。そこで本記事では、Python を使って、企業の財務情報を取得する 方法についてお伝えしたいと思います。

また、本記事で実装したソースコードについては GitHub にまとめましたので必要に応じてご利用ください。当記事のコードは全て Google Colaboratory で実装しており、2023年4月12日現在正常に動作することを確認しております。

財務情報の取得方法

良いニュースと悪いニュース

さて、早速財務情報の取得方法について伝授していきますが、ここで良いニュースと悪いニュースがあります。何やら不穏ですね。

まずは良いニュースです。実は日本の上場企業の財務情報を取得したい場合、なんと日本政府からAPIが提供されています!このシステムを EDINET と呼ばれています。公式ページはこちら をご確認ください。このEdinet APIがあるために、財務情報が欲しい企業のホームページを見に行って、有価証券報告書を開いて...なんてことをする必要はありません。素晴らしいですね!

では、悪いニュースとは何なのでしょうか。それはこのEdinet APIから目的の情報を収集することがとても難しいということです(笑)。実際、本当に自分の狙った情報を抽出してこれるようになるためには、少なくとも こちらのPDF の内容を全て理解する必要があります。眩暈がしてしまった方もいらっしゃるのではないでしょうか。執筆している筆者自身もこの内容を全て理解できているわけではありません。そのため当記事では、なるべく要点を絞り込んで、基本的な情報が収集できるようになることを目指します。

まず、データの収集の方法についてその概略を示します。以下のようになります。

  1. まずは「提出された財務情報の一覧」をAPIから取得し、欲しい財務情報(docID)を選択する。
  2. 選択した docID に基づいて、その財務情報のデータをダウンロードする。
  3. ダウンロードした情報に含まれる XBRL 形式のデータに対し、スクレイピングを実行し、欲しい情報を抽出する。

それでは早速始めましょう!

Step1 提出された財務情報の一覧をAPIから取得する

まず EDINET API から提出済みの財務情報の一覧をダウンロードしましょう[1]。一旦ソースコードを示すと以下のようになります。

import requests

END_POINT = 'https://disclosure.edinet-fsa.go.jp/api/v1'
submission_info_endpoint = f'{END_POINT}/documents.json'
submission_request_parameters = {
    'date': '2022-01-18',
    'type': 2
}
submission_info_response = requests.get(
    submission_info_endpoint, submission_request_parameters
)
submission_info_json = submission_info_response.json()

提供されている API のエンドポイントに、GET リクエストを投げるだけです。ただしURLパラメータとして以下の2つを与える必要があります。

  • date : EDINET API は提出された情報が日次でまとめられています。そのため、「いつの提出情報を取得するか」を指定する必要があります。
  • type : ここは 2 を選択すればよいです。1も選択できますが、1だとメタ情報だけ抽出されるため必要な情報が抽出できません。

得られる情報は json 形式で返ってきます。そのためこのままだと少し理解しづらいのでおなじみの DataFrame の形式に変更しましょう。

import pandas as pd

raw_submission_info_df = pd.DataFrame(submission_info_json['results'])
raw_submission_info_df.columns

すると、カラムとして以下の情報が格納されていることが分かります。

Index(['seqNumber', 'docID', 'edinetCode', 'secCode', 'JCN', 'filerName',
       'fundCode', 'ordinanceCode', 'formCode', 'docTypeCode', 'periodStart',
       'periodEnd', 'submitDateTime', 'docDescription', 'issuerEdinetCode',
       'subjectEdinetCode', 'subsidiaryEdinetCode', 'currentReportReason',
       'parentDocID', 'opeDateTime', 'withdrawalStatus', 'docInfoEditStatus',
       'disclosureStatus', 'xbrlFlag', 'pdfFlag', 'attachDocFlag',
       'englishDocFlag', 'csvFlag', 'legalStatus'],
      dtype='object')

色々とカラムがありますが、まず押さえておくべきカラムは以下です。

  • docID : 書類を一意に特定するIDを表します。後に書類をダウンロードする際にはこちらのIDを利用してどの書類化を指定します。
  • edinetCode : edinet で企業ごとに割り振られたコードです。企業ごとに情報を整理する場合はこちらをキーにするとよいと思います。
  • secCode : いわゆる「証券コード」です。[2]
  • filerName : 企業名です。
  • docDescription : 提出された書類のタイトルを表します。例えば「有価証券報告書 第〇〇期」といったような感じです。

とりあえずこの上記5つのカラムに絞って表示してみましょう。

# 重要なカラムに絞る
submission_info_df = raw_submission_info_df[['docID', 'edinetCode', 'secCode', 'filerName', 'docDescription']]
submission_info_df.head()

提出物情報

特に docDescription を見てみると、多くの種類の情報が取得されていることが分かります。今回はひとまず「有価証券報告書」の情報に焦点を当てるため、有価証券報告書の情報のみを抽出してみましょう。「有価証券報告書」という文字列が含まれていることだけを条件とすると受益証券の情報が入ってきてしまいます。この情報は今回のスコープである「企業の財務情報を収集する」という目的とは関係ないため、「受益証券」という単語が含まれる書類情報に関しては落としてしまいましょう。

# 有価証券報告書の情報を抽出する。
securities_report_infos = []
for i, row in submission_info_df.iterrows():
    doc_desc = row['docDescription']
    
    if doc_desc is None:
        continue
    
    if ('有価証券報告書' in doc_desc) and ('受益証券' not in doc_desc):
        row_to_dataframe = pd.DataFrame([row])
        securities_report_infos.append(row_to_dataframe)

if len(securities_report_infos) == 0:
    print('有価証券報告書の提出情報がありません。')
else:
    print(f'{len(securities_report_infos)} 件の有価証券報告書が抽出されました。')
    securities_report_info_df = pd.concat(securities_report_infos)
4 件の有価証券報告書が抽出されました。

securities_report_info_df に目的のデータが格納されていますので確認してみましょう。

有価証券報告書の提出物情報

中に「株式会社マネーフォーワード」の提出物情報がありますね。同日に複数提出されているようですが、今回はより新しい情報である第9期決算情報を取得してみましょう。すなわち docID = 'S100N8ST' です。

Step2 選択した docID に基づいて、その財務情報のデータをダウンロードする

それでは財務情報のダウンロードを行っていきましょう。書類一覧情報を抽出した時と同じように、APIに対して GET リクエストを叩きます。

# 今回はマネーフォーワードの第9期決算情報を対象とする。
docID = 'S100N8ST'
document_endpoint = f'{END_POINT}/documents/{docID}'
document_request_parameters = {
    'type': 1
}
document_response = requests.get(document_endpoint, document_request_parameters)

エンドポイントとして取得したい書類情報の docID を指定します。また、URLパラメータとして type を渡す必要があります。後にスクレイピングに利用する XBRL ファイルを取得するためには 1 を指定する必要があります。2 を指定するとPDFファイルのダウンロードとなるようです。

さて、帰ってきたファイルをまずは zip 形式で保存します。

# まず、返ってきたデータを zip 形式で保存する。
zip_file_full_path = f'/content/{docID}.zip'
with open(zip_file_full_path, 'wb') as f:
    for chunk in document_response.iter_content(chunk_size=1024):
        f.write(chunk)

Google Colaboratory の左側のUIでディレクトリを確認することができると思いますが、そこに S100N8ST.zip が現れたことを確認できると思います。確認出来たら、このファイルを解凍しましょう。

# zip ファイルを解凍する
import zipfile
import os
os.makedirs(f'/content/{docID}', exist_ok=True)
with zipfile.ZipFile(zip_file_full_path) as zip_f:
    zip_f.extractall(f'/content/{docID}')

この操作により解凍されたファイルがカレントディレクトリの S100N8ST ディレクトリ以下に展開されています。このディレクトリの PublicDoc ディレクトリの下の階層にある、XBRL 形式のファイルがスクレイピングに利用するファイルです。このファイルのパスを取得しましょう。

# xbrl ファイルを発見する
from glob import glob
# PublicDoc 内に格納されている xbrl ファイルが分析対象となるファイルである。
xbrl_expression = f'/content/{docID}/**/PublicDoc/**/*.xbrl'
xbrl_paths = glob(xbrl_expression, recursive=True)
xbrl_paths
['/content/S100N8ST/XBRL/PublicDoc/jpcrp030000-asr-001_E33390-000_2020-11-30_02_2022-01-18.xbrl']

ついにスクレイピングの対象となる XBRL ファイルを抽出することができました!このファイルを使用し、必要なデータを抽出していきましょう!

Step3. XBRL データに対するスクレイピングを実行する

XBRL ファイルの抽出には BuffetCode さまが提供してくださっている、edinet_xbrl パッケージを用いるとやりやすいです。したがって本記事ではこのパッケージを利用してデータの収集を行います。

まず以下のコードにより、パッケージをインストールしましょう。

!pip install edinet_xbrl

スクレイピングにはこのパッケージに含まれている EdinetXbrlParser クラスを用います。以下のコードを打ち込むことで、スクレイピングの準備が完了します。

from edinet_xbrl.edinet_xbrl_parser import EdinetXbrlParser
parser = EdinetXbrlParser()
# Step2で特定した XBRL ファイルのパスを選択
xbrl_path = xbrl_paths[0]
parsed_xbrl = parser.parse_file(xbrl_path)

それでは実際にスクレイピングを行っていきましょう。スクレイピングをするには、スクレイピングしたい対象の key 及び context_ref を指定する必要があります。ざっくりにはなってしまいますがこれらについて説明しておきます。 key とは「取得したい情報が何か」を表す情報で、context_ref は「取得したい情報がいつのものか、あるいはどういった性質を持つものか」といったような付加的な情報になります。

決算末日情報の取得

例えばこの有価証券報告書の決算末日は以下のようにして得ることができます。

# この決算情報がいつのものか確かめる。
key = "jpdei_cor:CurrentFiscalYearEndDateDEI"
context_ref = "FilingDateInstant"
report_end_date_info = parsed_xbrl.get_data_by_context_ref(key, context_ref)
report_end_date = report_end_date_info.get_value()
report_end_date
'2020-11-30'

これは提出物情報(Step1)から得られた docDescription の結果である、令和2年11月30日と整合しています。問題なく情報が得られているようです。

事業の内容の取得

次に、事業の内容についてスクレイピングしましょう。こちらも、指定された key 及び context_ref を用いることで、以下のように情報の取得が可能です。

# 「事業の内容」という事業の概要について記述された情報を取得する。
key = "jpcrp_cor:DescriptionOfBusinessTextBlock"
context_ref = "FilingDateInstant"
summary_of_business_info = parsed_xbrl.get_data_by_context_ref(key, context_ref)
summary_of_business = summary_of_business_info.get_value()
# 結果の出力
from IPython.display import HTML
HTML(summary_of_business)

事業の内容の出力結果の一部

以上で示すように、日付の情報や事業の内容といったテキストデータについては(私の理解の範囲では)context_ref = 'FillingDateInstant' と指定することにより取得できます。

売上情報の取得

さて、いよいよ皆様が最も関心を持たれるであろう売上情報の取得を試みます。この情報は以下のように取得できます。

# 売上情報の取得
key = 'jpcrp_cor:NetSalesSummaryOfBusinessResults'
context_ref = 'CurrentYearDuration'
extracted_data = parsed_xbrl.get_data_by_context_ref(key, context_ref)
net_sales = extracted_data.get_value()
net_sales
'11318217000'

売上情報を取得することができました。先ほど、context_ref は「取得したい情報がいつのものか、あるいはどういった性質を持つものか」を示すものだとご説明致しました。CurrentYearDuration とは「今年度期間」を表します。すなわち、ここで得られた売上データは今年度のもの、つまり2020年度決算における売上を表します。

勘の良い方はお気づきになられたかもしれませんが、この context_ref を変更することで、今年度以外の売上情報を取得することもできます。例えば context_ref = 'Prior1YearDuration' と指定すれば1年前、'Prior2YearDuration' と指定すれば2年前... というように過去に遡及してデータを取得することができます。有価証券報告書には基本的に今年度を含めて5年分売上高が格納されているため、まとめてスクレイピングする関数を定義してしまいましょう。

# 有価証券報告書には基本5年分の情報があるため、5年分の情報をとってくる。
from typing import Dict, List
from datetime import datetime
from dateutil.relativedelta import relativedelta

durations = [
    'CurrentYearDuration',
    'Prior1YearDuration',
    'Prior2YearDuration',
    'Prior3YearDuration',
    'Prior4YearDuration'
]

def translate_period(report_end_date: str, duration: str) -> str:
    '''duration を対応する日付情報に変換'''
    report_end_date_datetime = datetime.strptime(report_end_date, '%Y-%m-%d')
    if duration == 'CurrentYearDuration':
        return report_end_date_datetime.strftime("%Y-%m-%d")
    else:
        n_year = int(duration[5])
        n_year_previous_date = report_end_date_datetime - relativedelta(years=n_year)
        return n_year_previous_date.strftime("%Y-%m-%d")

def get_all_periods_value(
    xbrl: EdinetXbrlParser,
    report_end_date: str,
    key: str, 
    context_refs: List[str]
) -> Dict[str, str]:
    '''キーで指定した項目の各期間の情報を一括取得'''
    results: Dict[str, str] = {}
    for context_ref in context_refs:
        extracted_data = xbrl.get_data_by_context_ref(key, context_ref)
        _date = translate_period(report_end_date, context_ref)
        results[_date] = extracted_data.get_value()
    return results

上記で定義した関数を用いれば、以下のようにまとめて売上情報を取得できます。

# 直近5年分の売上をまとめて取得する
net_sales = get_all_periods_value(
    xbrl=parsed_xbrl,
    key='jpcrp_cor:NetSalesSummaryOfBusinessResults',
    report_end_date=report_end_date,
    context_refs=durations
)
net_sales
{'2020-11-30': '11318217000',
 '2019-11-30': '7156784000',
 '2018-11-30': '4594789000',
 '2017-11-30': '2899548000',
 '2016-11-30': None}

利益情報の取得

せっかく関数を定義したので、利益についてもまとめて取得してしまいましょう!

# 直近5年分の利益をまとめて取得する
net_profits = get_all_periods_value(
    xbrl=parsed_xbrl,
    key='jpcrp_cor:ProfitLossAttributableToOwnersOfParentSummaryOfBusinessResults',
    report_end_date=report_end_date,
    context_refs=durations
)
net_profits
{'2020-11-30': '-2423282000',
 '2019-11-30': '-2572050000',
 '2018-11-30': '-815445000',
 '2017-11-30': '-842814000',
 '2016-11-30': None}

以上でスクレイピングについての実装は以上となります。お疲れさまでした。

keycontext_ref はどう知るの?

さて、肝心の「keycontext_ref はどう知るの?」という問いにまだ答えていませんね。実はこの問いに対する回答こそが、この API の難解さを表す最大の理由となります。

基本的に各企業は概ね同じ名前で特定の指標を定義しています。例えば売上を示す概念は普通「売上」ですよね。ところが銀行業界では売上に相当する概念は「経常収益」と表現されます。このように、売上を示す概念でも、業界によって違ったりします。そういう差異はあるのですが、大体各企業が使う単語は同じものが多く、こうした「売上」「経常収益」といった多くの企業が共通で採用する名前については共通の key が割り当てられています。この共通で割り当てられたタグを 共通タクソノミ と呼ぶことにします。この共通タクソノミは EDINET のページで確認することができます。具体的には、このページ に存在する タクソノミ要素リスト というエクセルファイルにまとまっています。中でも有価証券報告書に関わる情報はシート9に乗っています。

つまりこのエクセルシートをにらめっこしながら、例えば売上に相当する指標をすべてスクレイピングするようにしてくれば、概ねの情報を集めることができます。これだけでも結構大変です。

しかしさらに残念なことにこれだけの操作では全企業の情報を網羅的に収集することができません。というのも、共通タクソノミに存在しない独自に定義されたタグで表現する企業もいるからです。こういったタグを共通タクソノミと切り分けて、個別タクソノミ と呼ぶことにします。当然ですが、個別タクソノミは一部の企業しか利用しない独自タグであるため、タクソノミ要素リストに掲載されていません。

じゃあどうすれば個別タクソノミの keycontext_ref を知ることができるの...という話になりますね。この話については本記事の範囲を超えるため、申し訳ございませんが詳細にはご説明できません。ここでは、ダウンロード及び解凍したものの中にその「個別定義タグ」をまとめたファイルがある、ということだけお伝えしておきます。

終わりに

非常に長い記事ですが、最後まで目を通して頂き有難うございました!皆様にとって実りある記事となっていれば幸いです。

最後に宣伝になりますが、この EDINET を活用して企業を検索することのできる Web アプリケーションを作成いたしました。宜しければ覗いてみてください!

URLはこちらです。

https://www.serendipity-disclosure.com/

また、アプリ開発において使用した技術をまとめた記事も投稿しております!

https://zenn.dev/joanofarc/articles/serendipity-development-report

重ね重ねにはなりますが、最後まで目を通していただき有難うございました。

脚注
  1. 厳密な仕様を確認したい方は こちら の「EdinetAPI 仕様書」を確認してください。 ↩︎

  2. 実際に中身を見てみると「何故5桁...?」と思われる方もいるかもしれませんが、末尾一桁を削除すると証券コードに一致します。 ↩︎

Discussion