金融庁がEDINETで公開しているXBRLを読み込む方法です。Pythonを使います。あえて自分で作らなくても、XBRLをCSV等に変換するソフトはあるのですが、全上場企業・全期間を集計できるものは、今のところありません(2018年時点)。
ですが、幸いなことに、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の公式サイトに検索方法等の説明があります。
ファイルをバイナリモード '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にも使えます。日本語の情報が多いのも良いですね。
ファイルをテキストモードで開いて読み込みます。(バイナリモードで開いて読み込んでもOKです)
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.
ファイルをテキストモードで開いて読み込みます。(バイナリモードで開いて読み込んでもOKです)
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のメリットはあまり活かせなかったなって、作り終わってから思いました。