EDINET XBRLを読み込む方法【Python】

金融庁がEDINETで公開しているXBRLを読み込む方法です。Pythonを使います。あえて自分で作らなくても、XBRLをCSV等に変換するソフトはあるのですが、全上場企業・全期間を集計できるものは、今のところありません。

ですが、幸いなことに、PythonにはXBRLを扱うのに必要なライブラリが揃っています。これらを使って決算情報の取得プログラムを作っていきます。

lxml.etree と BeautifulSoup

XBRLは単なるテキストファイルですが、中身を自在に検索するためには、XML読み込みライブラリで読み込んであげる必要があります。お勧めは「lxml.etree」です。

私の場合ですが、最初のシステムは「BeautifulSoup」で開発しました。ですが、使っていく中で様々な制限が見つかったので、今はlxml.etreeで開発しています。読み込みも検索も、lxmlの方が凄く速いというメリットもあります。

BeautifulSoupの制限で一番大きかったのは「タグ名が100文字を超えると切り捨てられる」ですね。XBRLのタグ名は非常に長いので、これを超えることがたまにありました。そういう時は、後ろを削って100文字分のタグ名で検索するとヒットしました。

「XBRLを読み込む方法」を3種類紹介します。

lxml.etreeで読み込む

lxml.etreeでXBRLを読み込む方法です。

lxmlの公式サイトに検索方法等の説明があります。

https://lxml.de/index.html

ファイルをバイナリモード 'rb' で開いて読み込みます。

from lxml.etree import fromstring as etree_fromstring
def read_xml_lxml_etree(file):
    """lxml.etreeで読み込む"""
    # バイナリモード('rb')で読み込みます
    with open(file, 'rb') as f:
        return etree_fromstring(f.read())

XBRLから名前空間・タグ名・属性名を指定して、データを取得します。

def proc_lxml_etree(file):
    """lxml.etreeでデータ取得"""
    # ファイル読み込み
    root = read_xml_lxml_etree(file)

    # 【データ取得例】
    # 名前空間(ns:NameSpace)から名前を取得します。
    namespace = root.nsmap['jpcrp_cor']

    # タグ名の大文字小文字は保存されます。
    tag = 'NetSalesSummaryOfBusinessResults'

    # 属性名の大文字小文字も保存されます。
    attr = 'contextRef'
    value = 'CurrentYearDuration'

    # 検索(XPATHのように指定します)
    xpath = './/{%s}%s[@%s="%s"]' % (namespace, tag, attr, value)
    data = root.find(xpath)

    # 表示
    print('%s %s' % (xpath, data.text))
    return

以下のような表示になります。サマリーの売上高の検索例です。

.//{http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2017-02-28/jpcrp_cor}NetSalesSummaryOfBusinessResults[@contextRef="CurrentYearDuration"] 11720041000000

BeautifulSoup (HTMLパーサー)で読み込む

BeautifulSoupでXBRLを読み込む方法です。

BeautifulSoupの公式サイトのドキュメントに検索方法等の説明があります。

https://www.crummy.com/software/BeautifulSoup/bs4/doc/

BeautifulSoup()は、引数に「features='lxml'」を指定するとHTMLパーサーとして働きます。このあたりの説明は installing-a-parser の見出しに載っています。タグ名や属性名が小文字に変換されるなど、HTML向けの仕様になっていますが、XBRLにも使えます。日本語の情報が多いのも良いですね。

ファイルをテキストモードで開いて読み込みます。

from bs4 import BeautifulSoup
def read_xml_beautiful_soup_html(file):
    """BeautifulSoup(HTMLパーサー)で読み込む"""
    with open(file, 'r', encoding='utf-8') as f:
        return BeautifulSoup(f.read(), features='lxml')

XBRLからタグ名と属性名を指定して、データを取得します。

def proc_beautiful_soup_html(file):
    """Beautifulsoup(HTMLパーサー)でデータ取得"""
    # ファイル読み込み
    soup = read_xml_beautiful_soup_html(file)

    # 【データ取得例】
    # タグ名は小文字で指定します。「''.lower()」で小文字に変換します。
    tag = 'jpcrp_cor:NetSalesSummaryOfBusinessResults'.lower()

    # 属性名も小文字で指定します。「''.lower()」で小文字に変換します。
    context_ref = {'contextRef'.lower():'CurrentYearDuration'}

    # 取得
    data = soup.find(tag, attrs=context_ref)

    # 表示
    print('%s %s %s' % (tag, context_ref, data.text))
    return

以下のような表示になります。サマリーの売上高の検索例です。

jpcrp_cor:netsalessummaryofbusinessresults {'contextref': 'CurrentYearDuration'} 11720041000000

BeautifulSoup (XMLパーサー)で読み込む

こちらもBeautifulSoupでXBRLを読み込む方法です。

XMLパーサーとして使うときは「features='xml'」を指定します。このあたりの説明は installing-a-parser の見出しに載っています。こちらは以前ビューティフル・ストーン・スープ(BeautifulStoneSoup)という名前で、機能が分かれていました。今は統合されています。

BeautifulStoneSoup()を使おうとすると、非推奨だからBeautifulSoup()に「features='xml'」を渡して使ってください、という旨の警告が表示されます。

UserWarning: The BeautifulStoneSoup class is deprecated.
Instead of using it, pass features=”xml” into the BeautifulSoup constructor.

ファイルをテキストモードで開いて読み込みます。

from bs4 import BeautifulSoup
def read_xml_beautiful_soup_xml(file):
    """BeautifulSoup(XMLパーサー)で読み込む"""
    with open(file, 'r', encoding='utf-8') as f:
        # BeautifulStoneSoup()は非推奨になりました。
        # 引数に'xml'を指定することで同じ機能が使えます。
        return BeautifulSoup(f.read(), features='xml')

XBRLからタグ名と属性名を指定して、データを取得します。

大文字小文字は区別されます。

def proc_beautiful_soup_xml(file):
    """BeautifulSoup(XMLパーサー)でデータ取得"""
    # ファイル読み込み
    soup = read_xml_beautiful_soup_xml(file)

    # 【データ取得例】
    # タグ名から名前空間の部分「jpcrp_cor:」を削除して指定します。
    # 大文字小文字は保存されます。
    tag = 'NetSalesSummaryOfBusinessResults'

    # 属性名の大文字小文字も保存されます。
    context_ref = {'contextRef':'CurrentYearDuration'}

    # 取得
    data = soup.find(tag, attrs=context_ref)

    # 表示
    print('%s %s %s' % (tag, context_ref, data.text))
    return

以下のような表示になります。サマリーの売上高の検索例です。

NetSalesSummaryOfBusinessResults {'contextRef': 'CurrentYearDuration'} 11720041000000

それぞれの実行時間を計ってみる

lxmlとBeautifulSoupで、どのくらいの差があるのでしょうか?

上記の読み込み関数をタイムイット(timeit)の中で実行してみます。

XBRLファイルからのデータを取得を10回ずつ繰り返します。

ある企業の有価証券報告書のXBRLを読み込んだ例です。

import timeit
def main():
    """メイン関数"""
    # XBRLファイル
    xbrl_file = r'(EDINETから取得・展開したXBRLのファイルパス)'

    # 関数名・変数名の辞書作成
    g = dict()

    # グローバル変数・関数の辞書を追加(proc_lxml_etree などを含む)
    g.update(globals())

    # ローカル変数・関数の辞書も追加(xbrl_file を含む)
    g.update(locals())

    # 繰り返し回数
    n = 10

    # timeitで計測
    print('■ lxml.etree')
    print(timeit.timeit('proc_lxml_etree(xbrl_file)',
        globals=g, number=n))

    print('■ BeautifulSoup(HTMLパーサー)')
    print(timeit.timeit('proc_beautiful_soup_html(xbrl_file)',
        globals=g, number=n))

    print('■ BeautifulStone(XMLパーサー)')
    print(timeit.timeit('proc_beautiful_soup_xml(xbrl_file)',
        globals=g, number=n))
    return

10回の実行結果です。時間の部分だけ抜粋しました。

■ lxml.etree
0.82

■ BeautifulSoup(HTMLパーサー)
9.38 秒

■ BeautifulSoup(XMLパーサー)
8.98 秒

速いほど開発効率が上がって、電気代も安くなります。lxmlは、名前空間の扱い方とXPATH風の検索方法を知るのに結構時間がかかりました。XBRLのように構造がカチッと決まってるものに向いてる気がします。

分かってしまえば、どれも使い勝手に差がないように思いますが、初期の学習コストの低さはやはりBeautifulSoupでしたね。特に、HTMLは種々雑多なフォーマットに遭遇しますので、正規表現を直接渡して検索できる点は素晴らしいです。たくさんのウェブサイトを扱うのと同じ方法でXBRLにもアプローチできました。

さて、日本のXBRLにはもうひとつ、「ixbrl」というものが存在します。2014年頃から登場しました。これ、中身がHTMLなんですね。なので、「xbrl」と「ixbrl」の両方に対応しやすいようにと、最初のシステムはBeautifulSoupで作りました。

ただ、「ixbrl」もフォーマットがカチッと決まっていたので、BeautifulSoupのメリットはあまり活かせなかったなって、作り終わってから思いました。

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