インラインXBRLの解析方法

投資・ファイナンス

インラインXBRLとXBRLの関係

EDINETの場合はインラインXBRLとXBRLの二つが提供されていますが、TDNetはインラインXBRLしか提供されていないため、インラインXBRLとXBRLの関係性を理解しておくことは分析において非常に重要です。
EDINETでは会社側がインラインXBRLを作成しそれをEDINETに提出します。そして、EDINETがそのインラインXBRLを解析してXBRLを作成しています。もし自分で全ての情報を解析したい場合は大きく二つの方法があります。
一つ目は直接インラインXBRLを解析してそこから情報を抽出するという方法です。
もう一つはインラインXBRLをXBRLに変換するプログラムを作ってXBRLを解析するという二段階の方法です。
一見後者のほうが面倒くさいようなイメージを持つかもしれませんが、XBRLファイルがしっかりと出来ていればそれを解析するのは比較的ライブラリーなどの環境が整っています。
一方で、インラインXBRLは少なくともEDINETやTDNetで提供されているものはXBRL解析で使用されているライブラリーでは一部がうまく機能しません。

そこで、今回はインラインXBRLをXBRLに変換する方法を解説していきます。

インラインXBRLは財務諸表などのデータを機械が読みやすいようにしたファイルであるXBRLと人間が読みやすいhtmlを組み合わせたファイル形式です。このインラインXBRLからXBRLファイルを生成しそのファイルを解析することによって必要なデータを取り出します。
インラインXBRLをXBRLの解析にも使っているライブラリArelleに適用してみましたが、日本の財務諸表の場合には一部エラーが出てしまい上手くいきませんでした。
そこでインラインXBRLをXBRLに変換することにしました。
XBRLの構造をしっかりと理解すれば、それほど難しくはありません。

具体例として、EDINETのixbrlファイルからxbrlファイルを作成する例をやってみたいと思います。

長くなるので、最後に全体を実行できるサンプルのGoogleColabを載せています。

データ準備

まずは、これまでEDINETの使い方などで紹介してきたように、EDINETAPIを使って、有価証券報告書をダウンロードして準備します。

import requests
import zipfile
import os
doc_id='S100PW3K'
def download_zip_file(doc_id):
    url = f"https://disclosure.edinet-fsa.go.jp/api/v2/documents/{doc_id}"
    params = {"type": 1,"Subscription-Key":api_key}
    filename = f"{doc_id}.zip"
    # 解凍先のディレクトリのパス
    extract_dir = filename.replace('.zip','')

    res = requests.get(url, params=params, stream=True)
    if res.status_code == 200:
        with open(filename, 'wb') as file:
            for chunk in res.iter_content(chunk_size=1024):
                file.write(chunk)
        # print(f"ZIP ファイル {filename} のダウンロードが完了しました。")
        # ZipFileオブジェクトを作成
        zip_obj = zipfile.ZipFile(filename, 'r')

        # 解凍先ディレクトリを作成(存在しない場合)

        if not os.path.exists(extract_dir):
            os.makedirs(extract_dir)
        # 全てのファイルを解凍
        zip_obj.extractall(extract_dir)
        # ZipFileオブジェクトをクローズ
        zip_obj.close()
        # print(f"ZIP ファイル {filename} を解凍しました。")
    else:
        print("ZIP ファイルのダウンロードに失敗しました。")
download_zip_file(doc_id)

データのパスを取得しておきます。

from bs4 import BeautifulSoup
import re
import html
folder = f'{doc_id}/XBRL/PublicDoc/'
manifest = f'{doc_id}/XBRL/PublicDoc/manifest_PublicDoc.xml'

with open(manifest,encoding='utf-8') as doc:
    soup = BeautifulSoup(doc,'xml')
paths = [folder + i.text for i in soup.find_all('ixbrl')]

基本構造

インラインXBRLの内部ではタグを使ってXBRLとそうではないhtml要素を分けています。
XBRL要素は基本的にixで始まるタグを持っています、
ですので、このixタグを解析すれば、必要な情報を取得できます。

今回、インラインXBRLをXBRLに変換するにあたって、基本的に使うのは、BeautifulSoupというHTMLなどを解析する時に用いられるライブラリーです。
このライブラリーを使うことで自分が欲しいと思っているタグを抽出し、そのタグの属性や値を取得したり、加工したりと様々なことができます。

ドキュメント情報

まず、XBRLファイルの上から順番にどのような要素が必要なのか考えていきましょう。
一行目に書いてあるのはどのようなXML形式を使っているかといった定型的な文章です。
これは基本的にファイルの一行目に定義されているので、そちらをそのまま使えばいいのですが、基本的に共通しているので、
今回は改行していないファイルがあることも含め、この部分はそのままテキストとして入力することとします。

xbrl_data ='<?xml version="1.0" encoding="UTF-8"?>\n'

XBRLの開始地点を知るためには以下のような関数を使用します。

def get_xbrl_tag(soup):
    xbrl_tag = '<xbrli:xbrl'
    for key, value in soup.find('html').attrs.items():
        if key not in ['version']:
            xbrl_tag += f' {key}="{value}"'
    xbrl_tag += '>'
    if xbrl_tag != '':
        xbrl_tag += '\n'
    return xbrl_tag

次に、XBRLのコンテンツとして必要な情報は、スキーマロールという情報です。
これはXBRLを解析するために必要な語彙スキーマがどこに存在するか、といったことを記入するファイルがどこにあるか示しています。インラインXBRLにこのリンクを保存する方法はix:referencesというタグになります。

def get_reference_data(soup):
    reference_data = ''
    references = soup.find_all('ix:references')
    for reference in references:
        reference_data += str(reference).replace('<ix:references>','').replace('</ix:references>','')
    if reference_data != '':
        reference_data += '\n'
    return reference_data

そして、このスキーマロールの中に書かれたファイルに、どういったタクソノミを使用しているかといった情報が書いてあります。
XBRLを解析する際には、このたくさんのファイルをダウンロードして、それらを基に全体の内容を理解していくという流れになりますが、ここでは割愛します
続いての要素がリンクロールの情報になります。これはリンクロールといったような形で表示されており、タグとしてはix:resourcesのリタグになります。
このタグばリンクロールと呼ばれる様々な関係性を示すファイルがどこにあるのかといったような内容を示す部分が前半にあり、その後にcontextrefと呼ばれるコンテキストの定義が含まれています。

def get_resource_data(soup):
    resouce_data = ''
    resouces = soup.find_all('ix:resources')
    for resouce in resouces:
        resouce_data += str(resouce).replace('<ix:resources>','').replace('</ix:resources>','')
    if resouce_data != '':
        resouce_data += '\n'
    return resouce_data

これらの多くはインラインXBRLの最初に当たるファイルのヘッダーの中に行くまれていることが多いです。

まず、ここまでの内容がXBRLの様子を解析するために必要な情報の埋め込みになります。

要素抽出

続いて実際の要素をインラインXBRLから取り出しXBRLに変換します

要素の中には大きく分けて三つの要素が存在します。
それらは、
数値を示す要素、
数値以外の示す要素、
そして、注釈の三つです。

それぞれにタグが付いています。
また、それぞれのタグごとに保有する属性値が異なってきます。
これらの属性値やタグを元にXBRLを作成していきます。

数値要素について

まず初めに、売上高の数値であったり、純利益の数値といったような数値に分類される情報を取得して、それをXBRLに変換します。
数値関連のタグは、ix:nonfraction、というタグになっています。
この中にはいくつかの属性と呼ばれるものが存在します、
まず最も重要な属性値は、ネーム属性です。このネームはqnameと呼ばれるXBRL内部の様子を識別するために必要な名前のようなものです。
そして、この名前に対応する値が何なのかということは、HTMLのixタグで囲まれたテキスト部分に当たります。

その他の属性としては、まずcontextrefという属性があります。
これは、その値がいつのものなのかということを示す場合によく用いられます。例えば、qnameは売上高を示す名前だったとして、その値が2020年のものなのか2023年のものなのかといった、いつもものなのかといった情報を持ちます。
ここで注意したいのは直接時期の情報をここで持っているのではなく、最初にリソースの中で取得した部分に特定のコンテキストを示す内容が、何時を示しているのかといったデータがベッド記入されており、それをXBRL解析で解析します。

続いてでdecimalsという値が属性値として存在しています。
これは数値をどのような桁で表示するかということを示していますがインラインXBRLをXBRLに変換する際には、そのままその数値を用いれば問題ありません。
続いての属性がunitRefになりますこれは単位を示す属性値ですが、こちらもインラインXBRLに書かれている情報をそのままXBRLに入力すればOKです。

調整が必要なのはscaleという属性です。
このスケールには-2,0,3,6という四つのパターンの数値が存在します。
ま-2は値がパーセント表示であることを示します。
続いて0はそのまま円表示であることを示しています。
続いて3は表示されている内容が千円の単位であることを示します。
そして6は表示されている内容が百万円の単位であることを示しています。

しかし、XBRLに記述する際にはこれらの数値はすべて円など0の場合だとして処理した数値を入力する必要があります。
ですので、スケールの値に応じてテキスト部分の数値を加工する必要があります。

続いて処理が必要な属性としては、formatという属性があります。
これはテキスト部分の値がどのような形式をしているかを示しています。
いくつかパターンがありますが、中でも数値として使われるケースが多いのは、カンマや小数点を含んだ数値を意味するタグです。
テキスト数値はカンマを含んでいる可能性がありますがXBRLに入力する際にはそのカンマを取り除いて完全な数字の列として入力する必要があるのでこちらも処理が必要です。
これは単純にreplace関数を使って顔を取り除けば問題ありません。

最後に処理が必要な属性はfootnoteRefsです。
これは注釈が存在する場合に、その注釈のIDを示している数値になります。
正直XBRLを解析しても注釈がどれに対応しているのか正確な情報を得ることはできないので、この部分に関しては最悪やらなくても大丈夫ですが、処理は記載しておきます。

このfootnoteRefsはスペースで区切られて複数の注釈が存在している可能性もあるので、スペースで分割をしてリストとして保持するようにします。
この注釈部分の作成は後ほど扱う注釈本体の処理の際にも使用することになりますですので、そちらの情報との連携も必要になってきます。

インラインXBRL上で注釈されているデータとその注釈がどこにあるかという情報はfootnoteRefsの後ろに数値をつけた情報によって識別されます。
しかし、XBRL上ではIDの後に数値をつけた情報によって処理されます。
この部分のIDファクターの数値の決め方は不明だったため、ひとまず桁数を元々分析していた書類を参考に十桁とし、そこをベースに同じIDを使うことがないように順番に注釈にIDを割り振っていくことにしました。(桁数は関係なかったようです)
ですので、後ほど注釈部分のインラインXBRLをXBRLに変換する際にもこのfootnoteRefs+数値とID+数値の対応付けが情報として必要となります。ですので予め辞書としてそれらの数値を登録し、保存しておくようにします。

最後にその値が存在しない場合の判定を行うnilという属性値があります。これは、その場合にはタグの形が特殊になっており、綴じる部分のタグがなくなり、後にスラッシュが付いている形となるのでそれを考慮して処理を行う必要があります。

全体の流れとしては、基本的に処理が不要で、そのまま使用できる属性部分については、その属性値が存在していた場合、そのままの値をXBRLタグの中に入れ込むような処理をします。

数値以外の処理

続いて、数値以外の場合の処理を記述していきます。
こちらについては先程と属性値も変わってきますが、いくつかは共通しています。
ですので、これらの数値と数値でない要素は別々にタグを取得して分析を進めますが、最終的には同じコードを使って処理をします。

まず、初めにcontextrefなどは共通している要素ですので、同じような処理で問題ありません。
最も異なっているのはescapeと呼ばれる属性があることです、これは、True/Falseを持ちます
Trueの場合は内部に更に要素を持つことを意味しており、イメージとしては、表などの場合がこちらに当たります。
一方でFalseの場合にはシンプルな文字列のようなイメージを持っておけばいいでしょう。
Trueの場合にはいくらか処理が追加で必要となります。

必要な処理は大きく分けて二つです。一つはTrueの場合、内部にはHTMLタグが含まれることが多いので、そのタグを一旦、文字実体参照というHTMLコードとしては扱えませんよという文字列に変換する必要があります。

例えばhtml内部で<を使いたい時それがタグの一部であるのか文字列の一部であるのか判断できません。
ですので、文字実体参照という別の文字列が必要となります。
今回もXBRLに渡す際にはその文字実体参照を使う必要があります。
これはPythonの場合簡単にできてデフォルトで入っているHTMLモジュールのescapeを使って一行で実現できます。
この時デフォルトではquote=Trueになっていますがquote=Falseにしておきます。
二つ目の処理はこの部分のHTMLの内部には数値を示す、ix:nonfractionなどのタグが含まれており、その部分も既にXBRLに変換されているタグが残っているということです。
XBRL特有のタグ部分は削除してそのタグが持っている数値だけにする必要があります。
これはBeautifulSoupのreplace_wthという関数を使って実現できます

def escape_parse(el):
    for ix_tag_in in ix_tags:
        el_ins = el.find_all(ix_tag_in)
        for el_in in el_ins:
            el_in.replace_with(el_in.text)
    return el

その他の属性値については、基本的に数値の場合と同じとなっていますので、特に追加の処理は必要ありません。

注釈の処理

それでは最後に、注釈部分を変換していきます。
こちらに関しては、数値や数値でない値を処理した時に、footnoteRefsという注釈を示すデータを保存しておきましたので、その値を使っていきます。

インラインXBRLの注釈を示すタグばix:footnoteです。
このタグを探し処理を進めます。

具体的にはこのインラインXBRLの一つ一つのタグにはfootnoteIDと呼ばれるIDが付与されています。
これがそれぞれの数値や数値でない要素で取得したfootnoteRefsと対応しています。
ですので。footnoteRefsで保存したIDファクトの値を取得しその値をXBRLを作成する際には使用します。
その他の属性値については基本的にそのまま使うので、取得して挿入するという一連の流れを記入しておきます。

これらの要素を全て上から順番につなぎ合わせれば一つのXBRLファイルとして成立します。

処理コード

長くなりましたが、これらの要素を取得するコードをまとめる次のようになります。

xbrl_data ='<?xml version="1.0" encoding="UTF-8"?>\n'
id = 1
footnote_dict ={}
conv_eles = []
for i, path in enumerate(paths):
    with open(path, encoding='utf-8') as doc:
        soup = BeautifulSoup(doc,'xml')
    if i == 0:
        xbrl_data += get_xbrl_tag(soup)
        xbrl_data += get_reference_data(soup)
        xbrl_data += get_resource_data(soup)
    for ix_tag in ix_tags:
        elements = soup.find_all(ix_tag)
        for el in elements:
            add_content = ''
            if el.get('contextRef'):
                context = el.get("contextRef")
                add_content += f' contextRef="{context}"'
            if el.get('decimals'):
                decimal = el.get('decimals')
                add_content += f' decimals="{decimal}"'
            if el.get('unitRef'):
                unitref = el.get('unitRef')
                add_content += f' unitRef="{unitref}"'
            if el.get('escape') == 'true':
                el = escape_parse(el)
            s = ''.join([str(i) for i in el.contents])
            val = html.escape(s, quote=False)

            if el.get('format') == 'ixt:numdotdecimal':
                val = val.replace(',','')
            if el.get('format') == 'ixt:dateyearmonthdaycjk':
                val = parse_date(val)
            name = el.get('name')
            scale= el.get('scale')
            if scale == '3':
                cal = float(val) * 1000
            elif scale == '6':
                val = float(val) * 1000000
            elif scale == '-2':
                val = float(val)/100
            if el.get('sign') == '-':
                val = -float(val)
            if el.get('format') not in ['ixt:dateyearmonthdaycjk','ixt:numdotdecimal',None]:
                print(el)
            if el.get('xsi:nil') == 'true':

                conv_eles.append(f'<{name} {add_content} xsi:nil="true"/>')
                continue
            if el.get('format') == 'ixt:booleantrue':
                val = 'true'
            if el.get('format') == 'ixt:booleanfalse':
                val = 'false'
            if val == '':
                conv_eles.append(f'<{name} {add_content} xsi:nil="true"/>')
                continue
            if el.get('footnoteRefs'):
                footnotes = el.get('footnoteRefs').split(' ')
                add_content += f' id="idFacr{id}"'
                id += 1
                for footnote in footnotes:
                    footnote_dict[footnote] = f'idFact{id}'

            conv_eles.append(f'<{name} {add_content}>{val}</{name}>')


for i, path in enumerate(paths):
    with open(path, encoding='utf-8') as doc:
        soup = BeautifulSoup(doc,'xml')

    for ix_tag in ix_tags:
        elements = soup.find_all('ix:footnote')
        for el in elements:
            add_content = ''
            fact_id = footnote_dict[el.get('footnoteID')]
            link_role = el.get('footnoteLinkRole')
            role = el.get('footnoteRole')
            lang = el.get('xml:lang')
            arcrole = el.get('arcrole')

            result = f'''
<link:footnoteLink xlink:role="{link_role}" xlink:type="extended">
    <link:footnote xlink:label="footnote" xlink:role="{role}" xlink:type="resource" xml:lang="{lang}">※1</link:footnote>
    <link:loc xlink:href="#{fact_id}" xlink:label="fact" xlink:type="locator"/>
    <link:footnoteArc xlink:arcrole="{arcrole}" xlink:from="fact" xlink:to="footnote" xlink:type="arc"/>
</link:footnoteLink>
  '''


            conv_eles.append(result)

xbrl_data += '\n'+'\n'.join(conv_eles) + '</xbrli:xbrl>'
with open(folder + 'myxbrl.xbrl','w',encoding='utf-8')as f:
    f.write(xbrl_data)

XBRLファイルは順番に特に意味はないですが、基本的にこの説明した順番に従って複数のインラインXBRLファイルを結合してXBRLファイルを作っていきます。どのようなファイルを結合していくかという情報はマニフェストというファイルに記入されています。
これらのファイルをpathsというファイルパスを保存する変数に代入しそれぞれの情報をファイルから取り出していきます.

from bs4 import BeautifulSoup
import re
import html
folder = f'{doc_id}/XBRL/PublicDoc/'
manifest = f'{doc_id}/XBRL/PublicDoc/manifest_PublicDoc.xml'

with open(manifest,encoding='utf-8') as doc:
    soup = BeautifulSoup(doc,'xml')
paths = [folder + i.text for i in soup.find_all('ixbrl')]

基本的に一つ目のファイルにリファレンスとリソースの情報が含まれているので、このファイルではその部分は読み込まないようにしています
最後に読み込んだデータをすべて一つに繋ぎ合わせてXBRLファイルとして保存します、うまくXBRLファイルになっているかはArelleを使ってXBRLを解析してみるのが一番手っ取り早いです、もしうまく変換されていない場合にはXBRLファイルの何行目がおかしいのかという情報が表示されるので、その場に行き、その行の内容を確認してみます。

from arelle import ViewFileFactTable, Cntlr, XbrlConst

cntlr = Cntlr.Cntlr(logFileName='logToPrint')
modelXbrl = cntlr.modelManager.load(folder + 'myxbrl.xbrl')
cols = ['Concept',
 'Facts',
 'Label',
 'Name',
 'LocalName',
 'Namespace',
 'ParentName',
 'ParentLocalName',
 'ParentNamespace',
 'ID',
 'Type',
 'PeriodType',
 'Balance',
 'StandardLabel',
 'TerseLabel',
 'Documentation',
 'LinkRole',
 'LinkDefinition',
 'PreferredLabelRole',
 'Depth',
 'ArcRole',
]
ViewFileFactTable.viewFacts(modelXbrl,'test.csv',arcrole=XbrlConst.parentChild,lang='ja',cols=cols)

EDINETのデータの場合にはあいXBRLとXBRLの二つが存在しているので答え合わせができます。
自分で作成したXBRLと元々付属されているXBRLがどう違うのか比較することで簡単にデバッグが進みます。
一方でTDNetの場合にはインラインXBRLしか提供されていないので地道に解析を進めるしかありません。
ですのでまずはEDINETの文章を使ってきちんと動くかどうかを確認してTDNetに試してみることをおすすめします

今回使用したコードはこちらのGoogleColabから確認できます。

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