python

【python】企業情報をXBRLタグを使って自動収集する方法

投資の判断をするのに、有価証券報告書とか四半期決算報告書とかを見ている人はいると思いますが、
毎回、ダウンロードするのは面倒じゃないですか?
そんなわけで、複数の会社の売上とか営業利益とかをお手軽に収集できるプログラムを作成したので公開します。

1.事前準備

1-1. EDINETのAPIキーの取得

以下のURLにアクセスして、APIキーを取得してください。
(ポップアップがブロックされているとAPIキーが取得できなかったので注意)
https://api.edinet-fsa.go.jp/api/auth/index.aspx?mode=1

【補足説明】
APIキー:API(サービスの入り口)を利用するための鍵のこと。第三者による不正利用を避けるため、他の人に知られないようにする必要がある。

1-2. 「.env」ファイルの作成

「.env」というファイルを作って、その中に、EDINET_API_KEY=”XXXXXXXX”というように1-1で入手したAPIキーを入力して保存してください。

 

1-3. インストールが必要なライブラリ

# pip
pip install requests
pip install pandas
pip install tqdm
pip install python-dotenv
pip install openpyxl

【補足説明】
- python-dotenv.env からAPIキーなど環境変数を読み込むためのライブラリ
- openpyxl:Excel(.xlsx)を読み書きするためのライブラリ

2. 対象を絞るための情報収集

EDINETにはたくさんの情報が掲載されています。そのため、どの情報が欲しいのか?を絞っていく必要があります。

2-1. 「布令コード」と「様式コード」の調べ方

[EDINETの操作ガイド」の「EDINET API仕様書 別紙1_様式コードリスト」に、「様式名」の列に書かれているのでここから欲しい書類を選んでください。

2-2.「要素ID」と「コンテキストID」の調べ方

「[EDINETの操作ガイド」に、要素IDとコンテキストIDの一覧表がありますが、ものすごい量の数があるので探すのが面倒です。
そこでおすすめなのが、調べたい企業のcsvファイルから見つけるという方法です。

EDINETにアクセスして、企業名、書類種別を選択したら検索をかけます。
②検索結果では、PDF、XBRL、CSVでそれぞれ見ることができるので「CSV」を選びます。
③ダウンロードされたZIPファイルの中には、複数のCSVファイルがありますが、その中で、「一番サイズの大きいCSV」を開いてください。
④CSVを開くと「要素ID」と「コンテキストID」が書かれています。

つまづきポイントと対処メモ

- Excelで文字化けする
- 最初、 UTF-8(BOMなし) で書き出したところ、Excelで保存する際に文字化けをしてしまいました。
- 結局 utf-8-sig(BOM付き) での出力に切り替えることで解決しました。
- 企業によって、XBRLタグが違う

同じ「売上」でも、
「jpcrp_cor:NetSalesSummaryOfBusinessResults」
「jpcrp_cor:RevenueIFRSSummaryOfBusinessResults」
「jpcrp030000-asr_E02144-000:OperatingRevenuesIFRSKeyFinancialData」
などがある。

ディレクトリ構造

メインのmain.pyとともに、APIキーを記載した.envを同じディレクトリにいれればOKです。

project-root/
├─ .env
└─ main.py              

コード全文


import os
import io
import glob
import time
import zipfile
import requests
import pandas as pd
from tqdm import tqdm
import datetime
from dotenv import load_dotenv

# EDINETのAPIキー読み込み
load_dotenv()
EDINET_API_KEY = os.getenv("EDINET_API_KEY")

# 各種定義
EDINET_BASE_URL = "https://api.edinet-fsa.go.jp/api/v2/documents"   # EDINET APIのベースURL(ここが最新でないと動かないので注意)
EDINET_ENDPOINT = f"{EDINET_BASE_URL}.json"                         # 文書一覧取得エンドポイント
EDINET_DOCUMENT_URL_TEMPLATE = f"{EDINET_BASE_URL}/{{docid}}"       # 企業別の文書が保管されている先のURL

# 以下の布令コード+様式コードの2つを指定したときにはじめて一意の様式が確定する。
# 下の例では 010 + 030000が有価証券報告書
ORDINANCE_CODE = "010"                                              # 布令コード
FORM_CODE = "030000"                                                # 様式コード  

# 証券コード・要素ID・コンテキストID
STOCK_CODE = "7203"                                                 # 証券コード
XBRL_FACTOR_ID = "jpcrp030000-asr_E02144-000:OperatingRevenuesIFRSKeyFinancialData"    # XBRL要素ID
XBRL_CONTEXT_ID = "CurrentYearDuration"                             # XBRLコンテキストID

# 調査期間設定
START_DATE = datetime.date(2025, 6, 14)                              # 開始日
END_DATE = datetime.date(2025, 6, 30)                                # 終了日

class DocIDFetcher:
    """
    ある期間内における有価証券報告書からDocIDを取り出すためのクラス
    """
    def __init__(self, start_date: datetime.date, end_date: datetime.date)-> None:
        """
        コンストラクタ

        Args:
            start_date (datetime.date): データ取得開始日
            end_date (datetime.date): データ取得終了日
        """
        self.start_date = start_date
        self.end_date = end_date
        self.dates = [
            self.start_date + datetime.timedelta(days=i)
            for i in range((self.end_date - self.start_date).days + 1)
        ]

    def fetch(self) -> pd.DataFrame:
        """
        EDINET APIから日ごとの有価証券報告書のメタデータを取得し、
        条件に合致するdocID情報をpandas.DataFrameで返す。

        Returns:
            pd.DataFrame: 各報告書に関するdocID、証券コード、提出日、決算期を含むDataFrame
        """
        
        url = EDINET_ENDPOINT
        
        records = []
        for d in tqdm(self.dates, desc="Fetching doc list"):
            params = {
                "date": d.strftime("%Y-%m-%d"),
                "type": 2,  # Document list and metadata
                "Subscription-Key": EDINET_API_KEY,
            }
            try:
                r = requests.get(url, params=params, timeout=10)
                r.raise_for_status()
                items = r.json().get("results", [])
                for item in items:
                    if item.get("ordinanceCode") == ORDINANCE_CODE and item.get("formCode") == FORM_CODE:
                        secCode = item.get("secCode")
                        if secCode:
                            formatted_code = str(secCode).zfill(4) + "0"  # 4桁 + 末尾に0を追加
                        else:
                            formatted_code = None
                        records.append({
                            "docID": item.get("docID"),
                            "証券コード": formatted_code,
                            "提出日": d.strftime("%Y-%m-%d"),
                            "決算期": item.get("periodEnd"),
                        })
            except Exception as e:
                print(f"Error fetching {d}: {e}")
            time.sleep(1)
        df = pd.DataFrame(records, columns=["docID", "証券コード", "提出日", "決算期"] )
        return df

class XBRLTextExtractor:
    """
    EDINET XBRLデータから特定のXBRLタグ(要素ID)を抽出するクラス。
    """
    def __init__(self, keys: dict, docids: list, base_dir: str = "./data")-> None:
        """
        コンストラクタ

        Args:
            keys (Dict[str, str]): ラベル名とXBRL要素IDの辞書
            docids (List[str]): 対象とするdocIDのリスト
            base_dir (str, optional): ファイル保存ディレクトリ(デフォルト: ./data)
        """
        self.keys = keys
        self.docids = docids
        self.base_dir = base_dir
        os.makedirs(base_dir, exist_ok=True)

    def extract(self) -> pd.DataFrame:
        """
        各docIDに対応するXBRLファイルをダウンロードし、指定したXBRL要素のテキストを抽出する。

        Returns:
            pd.DataFrame: 各docIDと抽出テキストを含むDataFrame
        """
        
        rows = []
        for docid in tqdm(self.docids, desc="Extracting text data"):
            params = {"type": 5, "Subscription-Key": EDINET_API_KEY}
            try:
                r = requests.get(EDINET_DOCUMENT_URL_TEMPLATE.format(docid=docid), params=params, timeout=20)
                r.raise_for_status()
                with zipfile.ZipFile(io.BytesIO(r.content)) as z:
                    entries = [e for e in z.namelist() if e.startswith("XBRL_TO_CSV/jpcrp") and e.endswith(".csv")]
                    if not entries:
                        print(f"No CSV found for {docid}")
                        continue
                    e = entries[0]
                    z.extract(e, path=os.path.join(self.base_dir, docid))
                    csv_path = os.path.join(self.base_dir, docid, e)
                    df = pd.read_csv(csv_path, encoding="utf-16", sep="\t")
                    row = {"docID": docid}
                    for label, spec in self.keys.items():
                        element_id = spec.get("要素ID")
                        context_id = spec.get("コンテキストID")
                        cond = df["要素ID"] == element_id
                        if context_id and "コンテキストID" in df.columns:
                            cond &= df["コンテキストID"] == context_id
                        vals = df.loc[cond, "値"]
                        row[label] = vals.values[0] if not vals.empty else None
                    rows.append(row)
            except Exception as e:
                print(f"Error processing {docid}: {e}")
            time.sleep(1)
        return pd.DataFrame(rows, columns=["docID"] + list(self.keys.keys()))

if __name__ == "__main__":
    codes = [STOCK_CODE + "00"]  
    
    # レポート一覧取得
    fetcher = DocIDFetcher(
        start_date=START_DATE,
        end_date=END_DATE,
    )
    df_reports = fetcher.fetch()
    
    # 証券コードを6桁に変換してフィルタリング
    codes_list = [STOCK_CODE + "00"]  
    df_filtered = df_reports[df_reports['証券コード'].isin(codes_list)]
    
    for index, row in df_filtered.iterrows():
        docid = row['docID']
        
        # XBRLファイルをダウンロードして解析
        params = {"type": 5, "Subscription-Key": EDINET_API_KEY}
        try:
            r = requests.get(EDINET_DOCUMENT_URL_TEMPLATE.format(docid=docid), params=params, timeout=20)
            r.raise_for_status()
            with zipfile.ZipFile(io.BytesIO(r.content)) as z:
                entries = [e for e in z.namelist() if e.startswith("XBRL_TO_CSV/jpcrp") and e.endswith(".csv")]
                if not entries:
                    print(f"  エラー: CSV found for {docid}")
                    continue
                e = entries[0]
                z.extract(e, path=os.path.join("./data", docid))
                csv_path = os.path.join("./data", docid, e)
                df = pd.read_csv(csv_path, encoding="utf-16", sep="\t")
                
                # 指定した要素ID・コンテキストIDでフィルタリング
                cond = df["要素ID"] == XBRL_FACTOR_ID
                if XBRL_CONTEXT_ID and "コンテキストID" in df.columns:
                    cond &= df["コンテキストID"] == XBRL_CONTEXT_ID
                vals = df.loc[cond, "値"]
                
            if not vals.empty:
                extracted_value = vals.values[0]
                print(extracted_value)
            else:
                print("データが見つかりませんでした")
                    
        except Exception as e:
            print(f"  エラー: {docid} の処理中にエラーが発生しました: {e}")
        
        time.sleep(1)  # API制限対応
    
    print("\nすべての処理が正常に終了しました")

-python
-