プログラミングと株式投資のブログ

プログラミングで株式投資に役立つ何かをやってます

pythonで決算短信のXBRLを分析する その4「決算短信XBRLからのデータ取得 インラインXBRLの読み込み方」

はじめに

前回は決算短信のXBRLについて主に有価証券報告書(EDINET)との違いに着目して説明しました。

www.quwechan.com


今回はインラインXBRLからデータを取得する方法について話していきます。ここではファイル名が「.xrbl」で終わるファイルをXBRLインスタンスファイル(決算短信のXBRLには存在しない)、「.ixbrl.html」で終わるファイルをインラインXBRLと呼んでいます。

インラインXBRLからデータを読み込む

方針

概要

インラインXBRLは決算短信の内容を表すhtmlファイルです。以下はインラインXBRLをブラウザで表示した際のスクリーンショットです。


この中に

  • 売上や会社名などの値(数値や文字列など)
  • コンテキスト定義(コンテキストがどの軸・どのメンバーに属するものなのか)

といった情報が保存されています。


以下はインラインXBRL中の情報を保存するために使用されているhtmlタグの一覧です。

決算短信サマリー報告書インスタンス作成要領 p16より引用
 
数値や文字などの値を取得するためにはix:nonFractionおよびix:nonNumericタグを参照し、コンテキスト定義を取得するためにはix:resourcesタグを参照します。
 

数値の取得

インラインXBRLから数値を取得するためには、ix:nonFractionタグを読み込み、値と属性値を保存すればよいです。
 

以下はix:nonFractionタグの属性一覧です。

属性名 説明
name XBRL中の要素名
contextRef コンテキスト名
scale 数値の大きさ単位
unitRef 数値の単位(日本円など)
sign 数値が負数の場合に「-」と設定する
decimals 数値が小数何桁か
format 数値の表示形式
xsi:nil 値が空なら「true」と設定する

 

インラインXBRL中に存在しうる数値型の一覧は以下のとおりです。

決算短信サマリー報告書インスタンス作成要領 p17より引用
 
数値を取得する際にこれらを識別する必要があるかといえば、わたしはないと判断しています。

 
以下はインラインXBRL中の数値を保存するhtmlタグの例です。

<ix:nonFraction contextRef="CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember" 
		decimals="-6"
		format="ixt:numdotdecimal"
		name="tse-ed-t:NetSales"
		scale="6"
		unitRef="JPY">1,580</ix:nonFraction>


数値はix:nonFractionタグ内に表記されています。name属性が要素名、contextRef属性がコンテキスト名を表します。ここでのname属性の値はXBRLの各種リンクベースファイル中に現れる要素の名前と一致しています。上記の例では数値が売上であり、現第2四半期間の実績値(非連結・個別)であることがわかります。


また、unitRef属性が数値の単位を示しており、上記の例の数値が日本円であるとわかります。以下はunitRef属性の一覧です。

決算短信サマリー報告書インスタンス作成要領 p15より引用



decimals属性は何桁の小数なのか、scale属性は数値の大きさ単位(10の何乗)を示しています。上記の例の数値がマイナス6桁の小数であり、100万単位の数値だとわかります。以下は数値の単位やデータ型ごとのdecimals属性およびscale属性の設定値です。


決算短信サマリー報告書インスタンス作成要領 p18より引用
 

数値が負数である場合は、sign属性に「-」が設定されます。以下は例です。

<ix:nonFraction contextRef="CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember"
		decimals="3" 
		format="ixt:numdotdecimal"
		name="tse-ed-t:ChangeInOperatingIncome"
		scale="-2"
		sign="-"
		unitRef="Pure">4.7</ix:nonFraction>

 

この例では今期の営業利益の変化率が記載されており、-4.7%だとわかります。


format属性は数値の表示形式を定義しているようですが数値の場合はあまり気にしなくてもよさそうです。
 

したがって数値データを取得する際の方針としては

  1. 値そのものはdouble型で読んで
  2. scale属性の分だけ値を乗じて数値の大きさを調整し
  3. sign属性から符号を判断する

といった感じでやれば良さそうです。

もちろん、name属性やcontextRef属性を保存して数値がXBRL中のどの要素に対応するのか、どのコンテキストで使われる数値なのかについても取得してください。

非数値(日時、文字列、真偽値、URL)の取得

インラインXBRLから文字等を取得するためには、ix:nonNumericタグを読み込み、値と属性値を保存すればよいです。

以下はix:nonNumericタグの属性一覧です。

属性名 説明
name XBRL中の要素名
contextRef コンテキスト名
escape 構造化データである場合に「true」と設定する
format 値の表示形式
xsi:nil 値が空なら「true」と設定する

 

インラインXBRL中に存在しうる非数値型の一覧は以下のとおりです。

決算短信サマリー報告書インスタンス作成要領 p20より引用
 

非数値を取得する際は上記のうち真偽値であるか否かを判別する必要があります。理由は後述します。
 

以下はインラインXBRLの文書名を保存するhtmlタグの例です。

<ix:nonNumeric contextRef="CurrentAccumulatedQ2Instant"
	     name="tse-ed-t:DocumentName">第2四半期決算短信〔日本基準〕(非連結)</ix:nonNumeric>


name属性とcontextRef属性のみが設定されており、escape属性とformat属性は設定されていません。つまり、文書名は構造化されていない、かつ、フォーマットのない非数値データであるとわかります。
 

以下は構造化された非数値を保存するhtmlタグの例です。サマリーのXBRLでは良い例がなかったので添付資料のXBRLから貸借対照表のテキストブロックを例として持ってきています。

<ix:nonNumeric contextRef="CurrentYTDDuration" name="jpcrp_cor:QuarterlyBalanceSheetTextBlock" escape="true">
        <p class="smt_head2" style="orphans:0;widows:0;padding-left:0pt;padding-right:0pt;margin-top:0pt;margin-bottom:0pt;text-justify:inter-ideograph;text-indent:0pt;font-family:'MS 明朝';text-align:justify;letter-spacing:0pt;line-height:15pt;font-size:10.5pt;">(1)四半期貸借対照表</p>
        <div class="tbld">
          <table cellspacing="0" cellpadding="0" style="border-collapse:collapse;width:471.7pt;table-layout:fixed;">
            <colgroup>
              <col style="width:13pt;min-width:13pt;">

(中略)

              </td>
              <td valign="middle" colspan=
            </tr>
          </tbody></table>
          <p style="clear:both; line-height:0.75pt; width:100%; font-size:0.75pt;">&nbsp;</p>
        </div>
	<p class="smt_text6" style="orphans:0;widows:0;">&nbsp;</p>
</ix:nonNumeric>

 

escape属性が「true」となっており、構造化データとなっています。見ての通り、ix:nonNumericの値がhtml文書となっています。構造化データの場合はhtmlタグも含めて値を取得する必要があります。以下はescape属性の設定値一覧です。

決算短信サマリー報告書インスタンス作成要領 p20より引用
 

以下は真偽値を保存するhtmlタグの例です。この例では東証のグロース市場であるか否かを示しています。簡単のため一部内容を省略しています。

<ix:nonNumeric contextRef="CurrentAccumulatedQ2Instant"
	    format="ixt:booleanfalse"
	         name="tse-ed-t:TokyoStockExchangeGrowth"></ix:nonNumeric>

 

真偽値であるか否かを判断するにはformat属性を見ればよいです。format属性が「booleantrue」「booleanfalse」のいずれかであれば真偽値であると判断できます。この例では「booleanfalse」なので東証グロース市場に属していないということを表しています。以下はformat属性の設定値一覧です。

適時開示システム タクソノミ設定規約書 p16より引用
 

真偽値の場合、真偽値の値をformat属性に保持しており、ix:nonNumericタグの値は設定されていないことがあります。そのため、非数値の値を取得する際にそれが真偽値であるか否かを判別しなければなりません。


したがって非数値データを取得する際の方針としては

  1. format属性から真偽値か否かを判別する
  2. 真偽値であれば
    • format属性から真か偽かを取得する
  3. 真偽値でなければ
    • ix:nonNumericタグの値をデータとして取得する(ただし、実装によってはescape属性を考慮する必要がある)

といった感じでやれば良さそうです。

もちろん、name属性やcontextRef属性を保存して数値がXBRL中のどの要素に対応するのか、どのコンテキストで使われる数値なのかについても取得してください。

コンテキスト定義の取得

インラインXBRLからコンテキスト定義を取得するためには、ix:resourcesタグを読み込みます。以下はix:resourcesタグの構造概要です。

<ix:resources>
          <xbrli:context>コンテキストの定義</xbrli:context>
           (中略)
          <xbrli:context>コンテキストの定義</xbrli:context>
</ix:resources>


ix:resourcesタグの内部にはxbrli:contextタグが並んでおり、xbrli:contextタグの内部にコンテキストの定義本体が保存されています。


以下はコンテキスト定義の一例です。

<xbrli:context id="CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember">
	<xbrli:entity>
		<xbrli:identifier scheme="http://www.tse.or.jp/sicc">23910</xbrli:identifier>
	</xbrli:entity>
	<xbrli:period>
		<xbrli:startdate>2023-08-01</xbrli:startdate>
		<xbrli:enddate>2024-01-31</xbrli:enddate>
	</xbrli:period>
	<xbrli:scenario>
		<xbrldi:explicitmember xmlns:xbrldi="http://xbrl.org/2006/xbrldi" dimension="tse-ed-t:ConsolidatedNonconsolidatedAxis">
			tse-ed-t:NonConsolidatedMember
		</xbrldi:explicitmember>
		<xbrldi:explicitmember xmlns:xbrldi="http://xbrl.org/2006/xbrldi" dimension="tse-ed-t:ResultForecastAxis">
			tse-ed-t:ResultMember
		</xbrldi:explicitmember>
	</xbrli:scenario>
</xbrli:context>

 

コンテキストがどの期間・ディメンションに対応するのかが記載されています。コンテキスト定義の構造はEDINETのXBRLと同じであり、この記事で説明しているので読んでください。


したがって、コンテキスト定義の取得はコンテキストの期間やディメンション(どの軸のメンバーに属するのか)を順次読み込んでいけばよいです。

実装

数値および非数値データの取得

適当なパーサを用いてインラインXBRLからix:nonFractionおよびix:nonNumeric要素を読み込みます。各属性値を読み込んでその値に応じてデータの表示を整形します。

インラインXBRLから数値および非数値データを読み込んで一覧化する処理のソースコードです。

from bs4 import BeautifulSoup
import copy

class InlineXBRLData() :

	def __init__(self, data_kind, \
				name, \
				context_ref, \
				scale, \
				unit_ref, \
				sign, \
				decimals, \
				a_format, \
				escape, \
				nil, \
				value) :

		self.data_kind = data_kind
		self.name = name
		self.context_ref = context_ref
		self.scale = scale
		self.unit_ref = unit_ref
		self.sign = sign
		self.decimals = decimals
		self.a_format = a_format
		self.escape = escape
		self.nil = nil
		self.value = value


	def __str__(self) :

		description_str = str(self.data_kind) + ',' + \
						  str(self.name) + ',' + \
						  str(self.context_ref) + ',' + \
						  str(self.scale) + ',' + \
						  str(self.unit_ref) + ',' + \
						  str(self.sign) + ',' + \
						  str(self.decimals) + ',' + \
						  str(self.a_format) + ',' + \
						  str(self.escape) + ',' + \
						  str(self.nil) + ',' + \
						  str(self.value)

		return description_str


	def get_value_str(self) :


		#数値
		if self.data_kind == 'nonFraction' :

			#タグの値が空文字なら空文字を返す
			if self.value == '':

				return ''

			#まず取得した文字列の,を削除する
			#数値が1000単位で区切られている場合がある
			tmp_value_str = copy.deepcopy(self.value)
			tmp_value_str = tmp_value_str.replace(',', '')

			#scaleの値に応じて数値を調整
			float_num = float(tmp_value_str)
			scale_num = None

			if self.scale != None :

				scale_num = int(self.scale)

			else :

				scale_num = 0

			float_num = float_num * pow(10, scale_num)

			#sign属性の値に応じて負数にする
			if self.sign == '-' :

				float_num = float_num * -1


			#decimals属性に応じて小数の桁を調整する
			decimals_digit = self.decimals

			if decimals_digit != None :

				decimals_digit = int(decimals_digit)

			else :

				decimals_digit = 0


			#整数で出力
			if decimals_digit <= 0 :

				return str(int(float_num))

			#小数で出力
			else :

				format_str = '{:.' + str(decimals_digit) + 'f}'

				formatted_num = format_str.format(float_num)

				return str(formatted_num)

		#非数値
		else :

			#まず、真偽値かどうかを調べる
			is_boolean = None

			if self.escape == 'true' :

				is_boolean = False

			elif self.a_format == 'ixt:booleantrue' or self.a_format == 'ixt:booleanfalse' :

				is_boolean = True

			else :

				is_boolean = False


			#真偽値ならformat属性の値に応じて文字列を返す
			if is_boolean == True :


				if self.a_format == 'ixt:booleantrue' :

					return 'True'

				else :

					return 'False'


			#真偽値以外はタグの値をそのまま返す
			return self.value


inline_xbrl_file_path = '適当なパス'


soup = None
with open(inline_xbrl_file_path, 'rb') as f :

	bdata = f.read()
	soup = BeautifulSoup(bdata, 'xml')


inline_xbrl_data_list = list()

nonfraction_elms = soup.select('ix|nonFraction')
for nonfraction_elm in nonfraction_elms :

	inline_xbrl_data_list.append( InlineXBRLData( 'nonFraction', \
				nonfraction_elm.get('name').replace(':','_'), \
				nonfraction_elm.get('contextRef'), \
				nonfraction_elm.get('scale'), \
				nonfraction_elm.get('unitRef'), \
				nonfraction_elm.get('sign'), \
				nonfraction_elm.get('decimals'), \
				nonfraction_elm.get('format'), \
				nonfraction_elm.get('escape'), \
				nonfraction_elm.get('xsi:nil'), \
				nonfraction_elm.get_text()) )
  
	
nonnumeric_elms = soup.select('ix|nonNumeric')
for nonnumeric_elm in nonnumeric_elms :

	attr_escape_str = nonnumeric_elm.get('escape')
	value_str = None
	if attr_escape_str == 'true' :

		#innerHtmlを取得する際はdecode_contentsを使う
		value_str = nonnumeric_elm.decode_contents()

	else :

		value_str = nonnumeric_elm.get_text()


	inline_xbrl_data_list.append( InlineXBRLData( 'nonNumeric' , \
				nonnumeric_elm.get('name').replace(':','_'), \
				nonnumeric_elm.get('contextRef'), \
				nonnumeric_elm.get('scale'), \
				nonnumeric_elm.get('unitRef'), \
				nonnumeric_elm.get('sign'), \
				nonnumeric_elm.get('decimals'), \
				nonnumeric_elm.get('format'), \
				attr_escape_str, \
				nonnumeric_elm.get('xsi:nil'), \
				value_str) )


for inline_xbrl_data in inline_xbrl_data_list :

	print(f'{inline_xbrl_data.name},{inline_xbrl_data.context_ref},{inline_xbrl_data.get_value_str()}')
	#print(f'{inline_xbrl_data},{inline_xbrl_data.get_value_str()}')

 

以下が実行結果です。

tse-ed-t_NetSales,CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember,1580000000
tse-ed-t_ChangeInNetSales,CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember,0.004
tse-ed-t_OperatingIncome,CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember,301000000
tse-ed-t_ChangeInOperatingIncome,CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember,-0.047
(中略)
tse-ed-t_FASFMemberMark,CurrentAccumulatedQ2Instant,<img alt="財務会計基準機構会員マーク" height="30" src="https://www.release.tdnet.info/img/mark1.gif" width="30"/>
tse-ed-t_DocumentName,CurrentAccumulatedQ2Instant,第2四半期決算短信〔日本基準〕(非連結)
tse-ed-t_FilingDate,CurrentAccumulatedQ2Instant,2024年2月22日
tse-ed-t_CompanyName,CurrentAccumulatedQ2Instant,株式会社 プラネット
(略)

 

要素名、コンテキスト名、値が一覧化できています。あとは要素名とコンテキスト名がわかればインラインXBRLから値を取得できます。

データ取得に用いる要素名、コンテキスト名を決定する方法には以下2つがあります。

  1. XBRLの構造(各種リンクベースファイル)から要素名、コンテキスト名を決定する
  2. あらかじめ取得対象となる要素名、コンテキスト名を調べておく

 

2の方法については既にいろいろな方々がやっているし、ネットに情報が沢山あるので基本的にここでは説明しません。

ただ、それでは味気ないので簡単に説明すると要素名の一覧はJPXのページから調べることができるので、その中から必要になりそうな要素名をあらかじめリストアップしておくことが出来ます。また、要素名はインラインXBRLに何があるかは調べればわかるのでそこからアタリをつけてリストアップしておくこともできます。例えば売上の要素名は「tse-ed-t_NetSales」であり、あからさまに売上だとわかります。さらにコンテキスト名には規則性があるので、今第2四半期間の実績値(非連結)とかなら「CurrentAccumulatedQ2Duration_NonConsolidatedMember_ResultMember」のように決定可能です。


ここでは1の方法(XBRLの構造から要素名とコンテキスト名を決定する)でやっていこうと考えています。以下は各種リンクベースファイルから取得した経営成績の項の構造です。

(None)RoleBusinessResultsQuarterlyOperatingResults(None)
  (title)tse-ed-t_BusinessResultsQuarterlyOperatingResultsHeading(四半期経営成績)
       (table)tse-ed-t_BusinessResultsQuarterlyOperatingResultsTable(四半期経営成績)
            (axis)tse-ed-t_ConsolidatedNonconsolidatedAxis(連結・個別又は非連結)
                 (member)tse-ed-t_NonConsolidatedMember(個別又は非連結)
            (axis)tse-ed-t_ResultForecastAxis(実績・予想)
                 (member)tse-ed-t_ResultMember(実績)
       (line_items)tse-ed-t_BusinessResultsQuarterlyOperatingResultsLineItems(四半期経営成績)
            (title)tse-ed-t_OperatingResultsAbstract(経営成績)
                 (title)tse-ed-t_IncomeStatementsInformationAbstract(損益計算書情報)
                      (title)tse-ed-t_NetSalesAbstract(売上高)
                           (number)tse-ed-t_NetSales(売上高)
                           (number)tse-ed-t_ChangeInNetSales(増減率)
                      (title)tse-ed-t_OperatingIncomeAbstract(営業利益)
                           (number)tse-ed-t_OperatingIncome(営業利益)
                           (number)tse-ed-t_ChangeInOperatingIncome(増減率)
                      (title)tse-ed-t_OrdinaryIncomeAbstract(経常利益)
                           (number)tse-ed-t_OrdinaryIncome(経常利益)
                           (number)tse-ed-t_ChangeInOrdinaryIncome(増減率)
                      (title)tse-ed-t_NetIncomeAbstract(当期純利益)
                           (number)tse-ed-t_NetIncome(当期純利益)
                           (number)tse-ed-t_ChangeInNetIncome(増減率)
                 (title)tse-ed-t_OtherOperatingResultsAbstract(その他の経営成績)
                      (number)tse-ed-t_NetIncomePerShare(1株当たり当期純利益)
                      (number)tse-ed-t_DilutedNetIncomePerShare(潜在株式調整後1株当たり当期純利益)
            (title)tse-ed-t_NoteToOperatingResultsAbstract(経営成績に関する注記)
                 (text)tse-ed-t_NoteToOperatingResults(経営成績に関する注記)

 
構造を見るとまず存在する要素名は自明です。コンテキスト名については記載はないですが、コンテキスト名は構造のディメンション(軸とメンバー)から決定可能です。そのため、XBRLの構造さえわかれば欲しいデータを取得することが出来ます。


ここら辺の内容は次回以降にお話ししたいと思っています。
 

コンテキスト定義の取得

ix:resourcesタグ内のコンテキスト定義を順次読み込んで、結果を一覧表示します。以下がソースコードです。

from bs4 import BeautifulSoup
import sys

#コンテキスト
class Context() :

	def __init__(self, name, period_type, instant_date, start_date, end_date, scenario) :

		self.__name = name
		self.__period_type = period_type
		self.__instant_date = instant_date
		self.__start_date = start_date
		self.__end_date = end_date

		#シナリオ
		# axis -> member == ( axis, member )のlist
		self.__scenario = scenario

	
	def __str__(self) :


		date_str = None
		if self.__period_type == 'instant' :

			date_str = self.__instant_date

		else :

			date_str = 'from ' + self.__start_date + ' to ' + self.__end_date


		axis_member_str_list = list()

		for axis_to_member_tuple in self.__scenario :

			axis_name = axis_to_member_tuple[0]
			member_name = axis_to_member_tuple[1]

			axis_member_str_list.append( axis_name + '->' + member_name)


		if len(axis_member_str_list) == 0 :

			axis_member_str_list.append('no scenario')


		return self.__name + ' ' + date_str + ' ' + ','.join(axis_member_str_list)


#コンテキスト定義の読み込み
def read_context(soup, context_list) :


	#xbrlファイルを調査する
	context_elms = soup.select('context')
	for context_elm in context_elms :

		context_name = context_elm.get('id')


		period_elm = context_elm.select_one('period')

		if period_elm == None :

			print('period 要素がない')
			sys.exit()


		period_type = None

		if 'Instant' in context_name.split('_')[0] :

			period_type = 'instant'

		elif 'Duration' in context_name.split('_')[0] :

			period_type = 'duration'

		if period_type == None :

			print('period 要素が決定できない')
			sys.exit()


		instant_date = None
		start_date = None
		end_date = None
		if period_type == 'instant' :

			instant_date = period_elm.select_one('instant').get_text()

		else :

			start_date = period_elm.select_one('startDate').get_text()
			end_date = period_elm.select_one('endDate').get_text()



		scenario_elm = context_elm.select_one('scenario')
		axis_to_member_tuple_list = list()
		if scenario_elm != None :

			axis_elms = scenario_elm.select('explicitMember')
			for axis_elm in axis_elms :
				axis_to_member_tuple_list.append( (axis_elm.get('dimension').replace(':', '_') \
											, axis_elm.get_text().replace(':', '_')) )


		context_data = Context(context_name, \
					period_type, \
					instant_date, \
					start_date, \
					end_date, \
					axis_to_member_tuple_list)


		context_list.append(context_data)




inline_xbrl_file_path = '適当なパス'


soup = None
with open(inline_xbrl_file_path, 'rb') as f :

	bdata = f.read()
	soup = BeautifulSoup(bdata, 'xml')


context_list = list()

read_context(soup, context_list)


for context in context_list :

	print(context)

 

以下が実行結果です。

PriorAccumulatedQ2Duration_NonConsolidatedMember_ResultMember from 2022-08-01 to 2023-01-31 tse-ed-t_ConsolidatedNonconsolidatedAxis->tse-ed-t_NonConsolidatedMember,tse-ed-t_ResultForecastAxis->tse-ed-t_ResultMember
PriorYearDuration_FirstQuarterMember_NonConsolidatedMember_ResultMember from 2022-08-01 to 2023-07-31 tse-ed-t_AnnualDividendPaymentScheduleAxis->tse-ed-t_FirstQuarterMember,tse-ed-t_ConsolidatedNonconsolidatedAxis->tse-ed-t_NonConsolidatedMember,tse-ed-t_ResultForecastAxis->tse-ed-t_ResultMember
PriorYearDuration_SecondQuarterMember_NonConsolidatedMember_ResultMember from 2022-08-01 to 2023-07-31 tse-ed-t_AnnualDividendPaymentScheduleAxis->tse-ed-t_SecondQuarterMember,tse-ed-t_ConsolidatedNonconsolidatedAxis->tse-ed-t_NonConsolidatedMember,tse-ed-t_ResultForecastAxis->tse-ed-t_ResultMember
(略)

 

コンテキスト定義の一覧が取得できています。
 

おわりに

今回は決算短信のインラインXBRLの読み込み方について説明しました。具体的にはXBRL中の数値と非数値およびコンテキスト定義の一覧を取得できました。


次回は今回取得したデータに加えて、XBRLの構造情報を用いることで実際に決算短信から欲しいデータを取得してみたいと思います。


以下が続きです。
www.quwechan.com


今回はここまでです。