【Python】XBRL から『勘定科目』と『リンクベース』の内容を取得するコード例【Arelle】

決算分析システム

XBRL 読み込みライブラリの Arelle アレル を使用して、『勘定科目』に『リンクベース』の内容をひもづけて取得する Python コード例です。

XBRL 報告書インスタンスの『ファクトデータ (fact)』 に、各種リンクベース『表示リンク (presentationLink)、計算リンク (calculationLink)、定義リンク (definitionLink)、フットノートリンク (footnoteLink)』の内容を紐づけて取得してみました。

Fact から『表示リンクの階層構造』や『計算リンクの階層構造』を取得できるようになって、とても便利になりました。

Arelle のインストール方法は、『Arelle のインストール方法』に書きました。

※(追記)以下で、『Fact から リンクベースの内容を取得する』という内容の Python コード例を載せましたが、それよりも、通常は素直に『リンクベースの内容から Fact を拾っていく』というほうが、やっぱり便利で適切だったと、さらにいろいろ実験したあとで思いました。

『リンクベース』のデータを取得する方法

『勘定科目 (fact)』に対応する各種『リンクベース』のデータを取得した手順です。

  1. まず、ファクト (fact) からリンクベースのデータを取得できるように、各種リンクベース(表示リンク、計算リンク、定義リンク、フットノートリンク)の辞書を作りました。
  2. そして、fact を取得していくときに、それらの辞書からリンクベースのデータを取り出して行きました。

これで、1つ1つの『勘定科目 (fact)』が、

  • 有価証券報告書(有報ゆうほう)の『どの表』に『どんな順番』で表示されているのか?
  • どこの小計しょうけいに含まれているのか?

などが分かるようになりました。

ところで、『リンクベース』から『報告書インスタンスの fact』を拾って使うというのが、たぶん通常の使い方だと思います。

ですが、自分は逆に、『報告書インスタンスの fact』から『リンクベース』の内容を参照したいな、って思ったんですよね。

売上高とか利益といった fact が、『ほかのどのような勘定科目と紐づいているのか?』を調べるために必要になったわけです。

『勘定科目のグループ分けを自動でできないかな?』と、そう思ったわけです。

全上場企業の決算書を比較するときには、どうしても、勘定科目の名寄せが必要になりました。

たとえば、『売上高』にあたる勘定科目って、実はたくさんありました。

業種によって、『売上高』にあたる勘定科目がちがっていたわけです。

その関係で、たとえば小売業と建設業と銀行業の比較は、めちゃめちゃ大変でした。

(余談です)

『より良い株式銘柄を選ぶ』という過程では、『業種を超えて比較したい』という場面が、当然ありました。

まあ、『異なる概念を比較することに意味はあるのか?』というのはありましたが、業績の伸びを知るためには、結局比較するしかなかったです。

例え話ですが、規模の異なる金額でも『パーセンテージ(%)』に直せば、一応の比較ができたのと同じように、異なる概念であっても、金額の『推移とか伸び』といった視点で見れば、役に立つ比較になったわけであります。

(余談おわり)

とにかく、そういった異なる企業同士でも『なんとか自動で比較できないか?』と思ったわけです。

今回掲載した Python コード例は、そんな試みの一部でした。

自身の目的によって、リンクベースデータ『表示リンク (presentationLink)、計算リンク (calculationLink)、定義リンク (definitionLink)、フットノートリンク (footnoteLink) のデータ』の使い方は変わってくると思います。

データ分析の用途では、やり方に正解はないと思います。

目的が果たせたら正解です。

コード例

XBRL の『勘定科目 (fact)』に、『リンクベース』の内容を紐づけて取得する Python コード例です。

特に難しかったところは『再帰関数さいきかんすう』でした。

自分は Arelle の『ViewFileFactTable.py』を参考にして、再帰関数を書きました。

再帰のループチェックについては、実際にはループではなくて、アーク (arc) の『use 属性』や『priority 属性』の処理が必要と思われる感じでした。それらの扱い方は簡単には理解できなかったので、自分は無視しています。

あと、原因は不明でしたが、デバッグモードで実行しているときに、ModelRelationship 型の変数を見ようとすると、Python が落ちました。

コード例だと、model_rel にマウスカーソルを当てたり、ウォッチ式に model_rel を入れたりしたら、Python が落ちました。

一方で、type(model_rel) とか model_rel.__dict__ みたいに、引数に使ったり属性を見たりするのは大丈夫でした。

"""
main_pre_cal_def_foot.py
『fact』に『リンクベースの内容』を紐づけて取得する
Python コード例です。
"""

import sys
sys.path.append(r'F:\project\kabu\Arelle')
from Arelle.arelle import Cntlr, XbrlConst

def main():
    """メイン関数"""
    # (準備 1/4) XBRL の zip を決めます。
    # (例) カネコ種苗株式会社 1376 / E00004
    #      有価証券報告書-第73期(令和1年6月1日-令和2年5月31日)
    #      開示日: 2020/8/28 12:55
    #      書類管理番号: S100JKNH
    xbrl_zip = r'F:\project\kabu\download\S100JKNH.zip'

    # (準備 2/4) zip の中の『報告書インスタンス』のパスをくっつけます。
    xbrl_file = xbrl_zip + r'\S100JKNH\XBRL\PublicDoc\jpcrp030000-asr-001_E00004-000_2020-05-31_01_2020-08-28.xbrl'

    # (準備 3/4) Arelle のコントローラーを取得します。
    # 'logToPrint' とは、Arelle のログを標準出力(stdout)に出す、という意味でした。
    # (出典) Cntlr.py にある class LogToPrintHandler(logging.Handler) の docstring。
    ctrl = Cntlr.Cntlr(logFileName='logToPrint')
    try:
        # (準備 4/4) XBRL ファイルを読み込みます。
        model_xbrl = ctrl.modelManager.load(xbrl_file)
        try:
            # (1/5) presentationLink のデータを取得。(表示リンク)
            # presentation_arcrole = XbrlConst.parentChild
            presentation_arcrole = 'http://www.xbrl.org/2003/arcrole/parent-child'
            presentation = make_linkbase_dict(model_xbrl, presentation_arcrole, 'presentation')

            # (2/5) calculationLink のデータを取得。(計算リンク)
            # calculation_arcrole = XbrlConst.summationItem
            calculation_arcrole = 'http://www.xbrl.org/2003/arcrole/summation-item'
            calculation = make_linkbase_dict(model_xbrl, calculation_arcrole, 'calculation')

            # (3/5) definitionLink のデータを取得。(定義リンク)
            # (出典) ModelXbrl.py ModelXbrl.relationshipSet.__doc__
            definition_arcrole = 'XBRL-dimensions'
            definition = make_linkbase_dict(model_xbrl, definition_arcrole, 'definition')

            # (4/5) footnoteLink のデータを取得。(フットノートリンク、注記リンク)
            # footnote_arcrole = XbrlConst.factFootnote
            footnote_arcrole = 'http://www.xbrl.org/2003/arcrole/fact-footnote'
            footnote = make_linkbase_dict(model_xbrl, footnote_arcrole, 'footnote')

            # (5/5) model_xbrl.facts のリストを取得します。
            # facts の1つ1つに、
            # リンクベース (表示、計算、定義、フットノート) の
            # データを紐づけて取得します。
            fact_datas = get_fact_datas(
                model_xbrl,
                presentation, calculation, definition, footnote,
                )
        finally:
            # modelXbrl を閉じます。
            # (出典) ModelManager.py
            ctrl.modelManager.close()
    finally:
        # Arelle のコントローラーを閉じます。
        # (出典) Cntlr.py
        ctrl.close()

    # (デバッグ) fact のリストを csv に出力します。
    from pathlib import Path
    import csv
    file_name = Path(__file__).stem # .py のファイル名の部分を取得。
    data_dir = Path(r'F:\project\kabu\data') # 保存フォルダを決める。
    csv_file = data_dir.joinpath(f'{file_name}_facts.csv')
    with csv_file.open('w', encoding='utf-8', newline='') as f:
        w = csv.writer(f)
        w.writerows(fact_datas)

    # 以上です。
    return


def make_linkbase_dict(model_xbrl, arcrole, linkbase_name=None):
    """
    presentationLink, calculationLink,
    definitionLink, footnoteLink の内容を
    取得して辞書にする関数です。

    『売上高』とか『利益』の modelFact から、
    presentationLink, calculationLink, definitionLink, footnoteLink の
    内容を取得するために作りました。
    """

    # リンクベースの内容を『linkrole』で取得できるようにするための辞書です。
    obj_linkrole = {}

    # リンクベースの内容を『qname』で取得できるようにするための辞書です。
    obj_qname = {}

    # (1/3) まず、すべての xlink:role="(linkrole)" を取得します。
    relationship_set = model_xbrl.relationshipSet(arcrole=arcrole)

    for linkrole in relationship_set.linkRoleUris:
        # ロールの日本語ラベルを取得します。
        role_types = model_xbrl.roleTypes.get(linkrole)
        if role_types:
            role_definition = role_types[0].genLabel(lang='ja', strip=True)
            if not role_definition:
                # たいてい、ここで取得できました。
                role_definition = role_types[0].definition
                if not role_definition:
                    # 元文字列を、そのままラベルとして使います。
                    role_definition = linkrole
        else:
            role_definition = linkrole

        # 辞書にキーを追加して、空のリストを追加します。
        obj_linkrole[linkrole] = []

        # (2/3) <link:presentationLink xlink:role="(linkrole)" ...> の
        # 下にある presentationArc たちを取得します。
        # (ほかのリンクベースの場合も同様です)
        link_relationship_set = model_xbrl.relationshipSet(arcrole, linkrole)

        for model_rel in link_relationship_set.modelRelationships:
            obj = model_rel.toModelObject

            # 日本語ラベルを取得します。
            obj_label = get_ja_label(obj, model_rel)

            # linkrole の辞書に item を追加します。
            # 'object' に入れるものは、
            #   『link:loc タグ (modelConcept 型 や ModelFact 型)』の内容と
            #   『link:footnote タグ (ModelResource 型)』の内容と
            #   『link:presentationLink タグなどの xlink:role 属性の文字列 (str 型)』です。
            # 'relationship' に入れるものは、『link:presentationArc タグや
            #   link:calculationArc タグなど (ModelRelationship 型)』の内容です。
            item = {'label': obj_label, 'object': obj, 'relationship': model_rel}
            obj_linkrole[linkrole].append(item)

            # (3/3) relationship.fromModelObject を、再帰的にたどって取得していきます。
            items = [] # 1 階層分を入れるリストです。
            visited = set() # 再帰のループチェックに使う集合です
            z = [] # items を追加していって、階層にするためのリストです。

            get_parent_object(
                z, items, visited,
                model_rel.fromModelObject, obj, model_rel,
                link_relationship_set, linkrole, role_definition,
                )

            # qname の辞書に、階層のリストを追加します。
            # (フットノートの場合だけ、qname 属性に加えて
            # id 属性が必要だったので、場合分けしました。)
            if linkbase_name == 'footnote':
                # キーが無ければ追加します。
                # キーは .qname と .id のタプルです。
                role_to_key = (model_rel.fromModelObject.qname, model_rel.fromModelObject.id)
                if role_to_key not in obj_qname:
                    obj_qname[role_to_key] = []
                obj_qname[role_to_key].extend(z)
            else:
                # キーが無ければ追加します。
                if obj.qname not in obj_qname:
                    obj_qname[obj.qname] = []
                obj_qname[obj.qname].extend(z)
    return {'obj_linkrole': obj_linkrole, 'obj_qname': obj_qname}


def get_parent_object(
    z, items, visited,
    prev_obj, obj, model_relationship,
    link_relationship_set, linkrole, role_definition,
    ):
    """
    再帰関数です。
    .fromModelObject を次々にたどって、上の階層を取得していきます。
    やってきた obj の中身は、
    modelConcept 型、modelResource 型、modelFact 型でした。
    """

    # 現在の obj を to 属性に持つ relationship を取得します。
    # model_rels は、model_relationships の略です。
    model_rels = link_relationship_set.toModelObject(obj)

    # ところで、取得結果は『0個(結果無し) か 1個』が理想的なのですが、
    # 無関係の relationship も一緒にヒットしました。
    # これはたぶん仕方がないことなので、
    # 無関係の relationship は、後でスキップします。

    # 取得結果が 0 なら終了します。
    if len(model_rels) == 0:
        # 日本語ラベルを取得します。
        obj_label = get_ja_label(obj, None)

        # 最後の obj を追加します。
        item = {'label': obj_label, 'object': obj, 'relationship': None}
        items.append(item)

        # さらに、一番上の階層として、linkrole_uri を追加します。
        item = {'label': role_definition, 'object': linkrole, 'relationship': None}
        items.append(item)
        z.append(items)
        return

    # ループチェック
    if obj in visited:
        # (ここは実行されないはず)
        # 既に同じ obj をたどっていた⇒(ループの疑い)⇒たどるのをやめて return。
        print(f'skipped 訪問済みの obj。"%s", "%s", "%s"'
            % (items[0]["label"], len(items), role_definition))
        return

    # 現在の obj を訪問済みに追加して継続する。
    visited.add(obj)

    # relationship から、次の obj を取得してたどります。
    for model_rel in model_rels:
        # model_rel が 無関係の relationship ならスキップします。
        # (prev_obj は previous object の略です)
        if prev_obj is not None:
            # .fromModelObject の obj は一致するはず。しなければスキップ。
            if model_rel.fromModelObject != prev_obj:
                continue

            # preferredLabel 属性は一致するはず。しなければスキップ。
            # (preferredLabel 属性は、presentationArc で使われていました。)
            preferred_label = getattr(model_rel, 'preferredLabel', None)
            prev_preferred_label = getattr(model_relationship, 'preferredLabel', None)
            if preferred_label != prev_preferred_label:
                continue

            # order は一致するはず。しなければスキップ。
            # (order 属性は、presentationArc と calculationArc と
            # definitionArc で使われていました。)
            order = getattr(model_rel, 'order', None)
            prev_order = getattr(model_relationship, 'order', None)
            if order != prev_order:
                continue

            # weight は一致するはず。しなければスキップ。
            # (weight 属性は、calculationArc で使われていました。)
            weight = getattr(model_rel, 'weight', None)
            prev_weight = getattr(model_relationship, 'weight', None)
            if weight != prev_weight:
                continue

        # 日本語ラベルを取得します。
        obj_label = get_ja_label(obj, model_rel)

        # リストに追加します。
        # (実際は obj と relationship の 2 つだけあれば十分でしたが、
        # デバッグするときの見やすさのために、日本語ラベルも入れました。)
        item = {'label': obj_label, 'object': obj, 'relationship': model_rel}
        items.append(item)

        # 再帰
        copied_items = items.copy()
        get_parent_object(
            z, copied_items, visited,
            None, model_rel.fromModelObject, model_rel,
            link_relationship_set, linkrole, role_definition,
            )

    # 1 つの経路をたどり終わったので、訪問済みから外します。
    visited.remove(obj)
    return


def get_ja_label(obj, model_relationship):
    """日本語ラベルを取得する関数です。"""

    # もし model_relationship が .preferredLabel 属性を
    # 持っていたら取得。無ければ None にします。
    preferred_label = getattr(model_relationship, 'preferredLabel', None)

    # (1/3) obj が .label メソッドを持っていた場合。
    #      (obj が modelConcept 型であった場合など)
    if hasattr(obj, 'label'):
        label = obj.label(
            preferredLabel=preferred_label,
            fallbackToQname=False,
            lang='ja',
            linkroleHint=XbrlConst.defaultLinkRole,
            )

        if label:
            # 取得成功。
            return label

        if preferred_label is None:
            # 失敗。(とりあえず qname を入れておきます)
            label = obj.qname
            return label

        # 再取得。(preferredLabel 無しで再取得を試みます)
        label = obj.label(
            preferredLabel=None,
            fallbackToQname=True, # 見つからなかった時は qname にします。
            lang='ja',
            linkroleHint=XbrlConst.defaultLinkRole,
            )
        return label

    # (2/3) obj が .role 属性を持っていた場合。
    #      (obj が ModelResource 型であった場合など)
    if hasattr(obj, 'role'):
        # ラベルは無かったので値を使いました。
        label = obj.xValue
        return label

    # (3/3) その他の場合。
    #      (obj が modelFact 型であった場合など)
    label = obj.concept.label(
        preferredLabel=None,
        fallbackToQname=True,
        lang='ja',
        linkroleHint=None,
        )

    # 引数の fallbackToQname は、ラベルが見つからなかった時の指示です。
    #   True: qname 文字列を返します。
    #   False: None を返します。
    # (出典) ModelDtsObject.py -> class ModelConcept -> def label
    return label


def get_fact_datas(
    model_xbrl,
    presentation, calculation, definition, footnote,
    ):
    """
    fact を取得する関数です。
    fact に、リンクベースの内容を紐づけて取得します。
    """

    # ヘッダを決めます。
    head = [
        '名前空間',
        '日本語', '英語',
        '接頭辞', 'タグ', 'ファクトID',
        '値', '単位', '貸借',
        '開始日', '終了日', '時点(期末日)',
        'コンテキストID', 'シナリオ',
        '表示リンク',
        '計算リンク',
        '定義リンク',
        'フットノートリンク',
        ]

    # fact を入れるリストを作ります。
    fact_datas = [head]

    # すべての fact からデータを取得していきます。
    for fact in model_xbrl.facts:
        # (1/8) 日本語ラベルを取得します。
        # (例) '売上高'
        label_ja = fact.concept.label(preferredLabel=None, lang='ja', linkroleHint=None)

        # (2/8) 英語ラベルを取得します。
        # (例) 'Net sales'
        label_en = fact.concept.label(preferredLabel=None, lang='en', linkroleHint=None)

        # (3/8) タグの値を取得します。(勘定科目の金額や文章など)
        # (例) Decimal('58179890000')
        x_value = fact.xValue

        # # (デバッグ) Excel や LibreOffice Calc などで見るために、
        # # 長い文字列を 100 文字で切り捨てて、改行も削除するコード。
        # if isinstance(x_value, str):
        #     x_value = x_value[:100].replace('\n', ' ')

        # (4/8) 単位を取得します。
        if fact.unit is None:
            unit = None
        else:
            # .unit の.value とは、複雑な分数形式の単位などを、
            # Arelle が人の見やすい形式に整えた文字列でした。
            # (例) 'JPY / shares'
            unit = fact.unit.value

        # (5/8) 開始日、終了日、時点(期末日) の日付を取得します。
        if fact.context.startDatetime:
            # 開始日
            # (例) datetime.datetime(2019, 6, 1, 0, 0)
            # 今回は csv に書き出すために、.strftime() メソッドを使って
            # YYYY-MM-DD 形式の文字列にしました。
            start_date = fact.context.startDatetime.strftime('%Y-%m-%d')
        else:
            start_date = None

        if fact.context.endDatetime:
            # 終了日
            # (例) datetime.datetime(2020, 6, 1, 0, 0)
            # ※ (注意)
            # ・1日分だけ加算された日付になっていました。
            # ・また、開始日が無い時でも『instance 時点 (期末日) 』の
            #   日付が設定されました。
            #
            # XBRL ファイルに書かれているのと同じ日付の
            # 『終了日』を取得するときは、
            # 『fact.propertyView 属性』や
            # 『fact.context.propertyView 属性』の中から
            # 取得することができました。
            end_date = fact.context.endDatetime.strftime('%Y-%m-%d')
        else:
            end_date = None

        if fact.context.instantDatetime:
            # 時点 (期末日)
            # (例) datetime.datetime(2020, 8, 29, 0, 0)
            # ※ (注意)
            # ・1日分だけ加算された日付になっていました。
            # 
            # XBRL ファイルに書かれているのと同じ日付の
            # 『時点 (期末日)』を取得するときは、
            # 『fact.propertyView 属性』や
            # 『fact.context.propertyView 属性』の中から
            # 取得することができました。
            instant_date = fact.context.instantDatetime.strftime('%Y-%m-%d')
        else:
            instant_date = None

        # (6/8) シナリオ (scenario) を取得します。
        scenario_datas = []
        for (dimension, dim_value) in fact.context.scenDimValues.items():
            # fact.context.scenDimValues の型は、普通の dict 型でした。
            # dimension は ModelConcept 型でした。
            # dim_value は ModelDimensionValue 型でした。
            # dim_value.member は ModelConcept 型でした。
            scenario_datas.append([
                dimension.label(preferredLabel=None, lang='ja', linkroleHint=None),
                dimension.id,
                dim_value.member.label(preferredLabel=None, lang='ja', linkroleHint=None),
                dim_value.member.id,
                ])
        if len(scenario_datas) == 0:
            scenario_datas = None

        # (7/8) リンクベースの辞書から、fact に該当するデータを取得します。
        pre_data = get_linkbase_data(model_xbrl, fact, presentation)
        cal_data = get_linkbase_data(model_xbrl, fact, calculation)
        def_data = get_linkbase_data(model_xbrl, fact, definition)
        foot_data = get_footnote_data(model_xbrl, fact, footnote)

        # (8/8) リストに追加します。
        fact_datas.append([
            fact.namespaceURI, # (例) 'http://disclosure.edinet-fsa.go.jp/taxonomy/jppfs/2019-11-01/jppfs_cor'
            label_ja, # (例) '売上高'
            label_en, # (例) 'Net sales'
            fact.prefix, # (例) 'jppfs_cor'
            fact.localName, # (例) 'NetSales'
            fact.id, # (例) 'IdFact1707927900' (id は footnoteLink を参照するときに使いました)
            x_value, # (例) Decimal('58179890000') (値は Decimal, int, None, str 型など色々でした)
            unit, # (例) 'JPY'
            fact.concept.balance, # (例) 'credit'
            start_date, # (例) datetime.datetime(2019, 6, 1, 0, 0)
            end_date, # (例) datetime.datetime(2020, 6, 1, 0, 0)
            instant_date, # (例) datetime.datetime(2020, 8, 29, 0, 0)
            fact.contextID, # (例) 'CurrentYearDuration'
            scenario_datas, # (例) "[[
                            #   '連結個別',
                            #   'jppfs_cor_ConsolidatedOrNonConsolidatedAxis',
                            #   '非連結又は個別',
                            #   'jppfs_cor_NonConsolidatedMember',
                            #  ],
                            #  [
                            #   '事業セグメント',
                            #   'jpcrp_cor_OperatingSegmentsAxis',
                            #   '種苗事業',
                            #   'jpcrp030000-asr_E00004-000_SeedsAndSeedlingsReportableSegmentsMember',
                            #  ]]"
            pre_data, # presentationLink のデータ。
            cal_data, # calculationLink のデータ。
            def_data, # definitionLink のデータ。
            foot_data, # footnoteLink のデータ。
        ])
    return fact_datas


def get_linkbase_data(model_xbrl, fact, linkbase_dict):
    """
    リンクベースの辞書 (obj_linkrole と obj_qname) から、
    受け取った fact に対応するデータを取得する関数です。
    presentationLink, calculationLink, definitionLink の内容から、
    fact に対応するデータを取得します。
    """

    # リンクベースの辞書を取得します。
    obj_linkrole = linkbase_dict['obj_linkrole']
    obj_qname = linkbase_dict['obj_qname']

    # fact の qname が辞書に無ければスキップ。
    if fact.qname not in obj_qname:
        return None

    # 連結区分の定数を取得します。
    ConsolidatedMember = model_xbrl.nameConcepts['ConsolidatedMember'][0]
    NonConsolidatedMember = model_xbrl.nameConcepts['NonConsolidatedMember'][0]

    # fact が『連結』なのか、それとも『非連結 (個別)』なのかを判定します。
    for scen_dim_value in fact.context.scenDimValues.values():
        if scen_dim_value.member.qname == NonConsolidatedMember.qname:
            # fact は『非連結 (個別)』の値だった。
            ok_member = NonConsolidatedMember
            ng_member = ConsolidatedMember
            break
    else:
        # break しなかった。つまり『連結』の値だった。
        ok_member = ConsolidatedMember
        ng_member = NonConsolidatedMember

    # (データの取得方法の例)
    # 1. リンクベースの辞書から
    #    『fact.context.scenDimValues』に一致するデータを選びます。
    # 2. その中から『fact.qname』に一致するデータを選びます。
    # 3. fact.context の dimension の role に一致するデータを選びます。
    # 4. 最後に『連結 or 非連結 (個別)』の区分が一致したデータを取得します。

    # (1/4) 『fact.context.scenDimValues』に一致するデータを選びます。
    # dim は dimension の略です。
    # scen は scenario の略です。
    dim_roles_members = []
    for scen_dim_value in fact.context.scenDimValues.values():
        if scen_dim_value.member.qname in obj_qname:
            dim_roles_members.append(obj_qname[scen_dim_value.member.qname])

    # (2/4) 『fact.qname』に一致するデータを選びます。
    roles = []
    for obj in obj_qname[fact.qname]:
        # obj (fact) が属している role を取得します。
        obj_role = obj[-1]['object']

        # (3/4) fact.context の dimension の『role』に一致するデータを選びます。
        is_ok = True
        for dim_roles in dim_roles_members:
            for dim_role in dim_roles:
                if dim_role[-1]['object'] == obj_role:
                    break
            else:
                # break しなかったので、不一致だった。
                is_ok = False
                break

        # 一致したデータが無かったのでスキップ。
        if not is_ok:
            continue

        # (4/4) 『連結 or 非連結 (個別)』の区分が一致するデータを取得します。
        for obj_lr in obj_linkrole[obj_role]:
            if obj_lr['object'].qname == ok_member.qname:
                # 同じ連結区分を持っていた。
                roles.append(obj)
                break
            elif obj_lr['object'].qname == ng_member.qname:
                # ちがう連結区分を持っていた。
                # 不一致なので、追加せずに break。
                break
            else:
                # 判定対象ではなかった。
                continue
        else:
            # 『連結 or 非連結 (個別)』の区分が無いので break しなかった。
            roles.append(obj)

    # 結果が無ければスキップ。
    if len(roles) == 0:
        return None

    # リストをソートして、結果の並びが毎回同じになるようにします。
    # 自分は、WinMerge などで結果の差分比較をするためにソートしました。
    roles.sort(key=lambda x: (x[0]['label'], x[-1]['label']))

    # object と relationship から、ラベルや属性の値などを取得します。
    # 今回は csv に書き込むために、取得した値を全部文字列に変換しました。
    temps = []
    for role in roles:
        rs = []
        for r in role:
            model_rel = r['relationship']

            # ii とか jj とかの変数名に意味は無いです。適当に付けました。
            ii = []
            order = getattr(model_rel, 'order', None)
            if order is not None:
                ii.append(str(order))
            preferred_label = getattr(model_rel, 'preferredLabel', None)
            if preferred_label is not None:
                ii.append(preferred_label)
            if len(ii) == 0:
                ii = ''
            else:
                ii = '(%s)' % ','.join(ii)

            jj = []
            weight = getattr(model_rel, 'weight', None)
            if weight is not None:
                jj.append(str(weight))
            if len(jj) == 0:
                jj = ''
            else:
                jj = '[%s]' % ','.join(jj)

            kk = []
            arcrole = getattr(model_rel, 'arcrole', None)
            if arcrole is not None:
                kk.append(arcrole.split('/')[-1]) # (例) 'domain-member'
            context_element = getattr(model_rel, 'contextElement', None)
            if context_element is not None:
                kk.append(context_element)
            is_closed = getattr(model_rel, 'isClosed', None)
            if is_closed is not None:
                kk.append(str(is_closed))
            usable = getattr(model_rel, 'usable', None)
            if usable is not None:
                kk.append(str(usable))
            if len(kk) == 0:
                kk = ''
            else:
                kk = '{%s}' % ','.join(kk)

            rs.append(r['label'] + ii + jj + kk)
        temps.append('⇒'.join(rs))
    roles = temps
    return roles


def get_footnote_data(model_xbrl, fact, linkbase_dict):
    """
    リンクベースの辞書 (obj_linkrole と obj_qname) から、
    受け取った fact に対応するデータを取得する関数です。
    footnoteLink の内容から、fact に対応するデータを取得します。
    """
    # リンクベースの辞書を取得します。
    _obj_linkrole = linkbase_dict['obj_linkrole'] # (今回は使いませんでした)
    obj_qname = linkbase_dict['obj_qname']

    # fact に該当する <link:footnote ...> は、
    # fact.qname と fact.id で取得できました。
    role_to_key = (fact.qname, fact.id)
    if role_to_key in obj_qname:
        roles = []
        for role in obj_qname[role_to_key]:
            roles.append(role)
    else:
        roles = None

    # 結果が無ければスキップ。
    if not isinstance(roles, list):
        return None

    # リストをソートして、結果の並びが毎回同じになるようにします。
    roles.sort(key=lambda x: (x[0]['label'], x[-1]['label']))

    # object と relationship から、ラベルや属性の値などを取得します。
    # 今回は csv に書き込むために、取得した値を全部文字列に変換しました。
    temps = []
    for role in roles:
        rs = []
        for r in role:
            obj = r['object']
            model_rel = r['relationship']

            if isinstance(obj, str):
                # obj は str 型 (roleURI の文字列) であった。
                obj_label = r['label']
                obj_roledefinition = None
            elif '{http://www.w3.org/1999/xlink}type' in obj.xAttributes:
                # obj は ModelResource 型であった。
                # (自分は、obj に type 属性があれば ModelResource 型だとみなしました。)
                obj_label = obj.qname.localName
                obj_role_types = model_xbrl.roleTypes.get(obj.role)
                if obj_role_types:
                    obj_roledefinition = obj_role_types[0].genLabel(lang='ja', strip=True)
                    if not obj_roledefinition:
                        obj_roledefinition = obj_role_types[0].definition
                        if not obj_roledefinition:
                            obj_roledefinition = obj.role
                else:
                    obj_roledefinition = obj.role
            else:
                # obj は ModelFact 型であった。
                if hasattr(model_rel, 'linkrole'):
                    linkrole_hint = model_rel.linkrole
                else:
                    linkrole_hint = None
                obj_label = obj.concept.label(preferredLabel=None, lang='ja', linkroleHint=linkrole_hint)
                obj_roledefinition = None

            ii = []
            x_value = getattr(obj, 'xValue', None)
            if x_value is not None:
                ii.append(str(x_value))
            obj_id = getattr(obj, 'id', None)
            if obj_id is not None:
                ii.append(str(obj_id))
            if obj_roledefinition is not None:
                ii.append(str(obj_roledefinition))
            if len(ii) == 0:
                ii = ''
            else:
                ii = '(%s)' % ','.join(ii)

            rs.append(obj_label + ii)
        temps.append('⇒'.join(rs))
    roles = temps
    return roles


if __name__ == '__main__':
    main()

実行結果

XBRL から取得した『勘定科目』と『リンクベース』のデータです。

LibreOffice Calc で CSV を見た時のスクリーンショットです。

『表示リンク、計算リンク、定義リンク、フットノートリンク』の列の内容です。

スクリーンショットだと見えなかったので、内容を取り出してみました。

階層構造を右矢印『⇒』でつなげているのは、デバッグ用です。

Arelle の GUI (arelle.pyw) で見た時と同じように、一番下の階層から上の階層に向かって、意図した通りにデータが取得できているかをチェックするために、右矢印『⇒』を使いました。

【fact と それに対応するリンクベースの取得例】
(fact)
日本語:現金及び預金
接頭辞:jppfs_cor
タグ:CashAndDeposits
ファクトID:IdFact1707927900
値:Decimal('4906928000')

(リンクベース)
書式:日本語ラベル(order,preferredLabel)[weight]{arcrole,
contextElement,isClosed,usable}

・表示リンク (presentationLink)
現金及び預金(1.0){parent-child,False}
⇒流動資産(1.0){parent-child,False}
⇒資産の部(1.0){parent-child,False}
⇒連結貸借対照表(2.0,http://disclosure.edinet-fsa.go.jp/
  jppfs/Consolidated/role/label){parent-child,False}
⇒連結貸借対照表⇒310010 連結貸借対照表

・計算リンク (calculationLink)
現金及び預金(1.0)[1.0]{summation-item,False}
⇒流動資産(1.0)[1.0]{summation-item,False}
⇒資産⇒310010 連結貸借対照表

現金及び預金(1.0)[1.0]{summation-item,False}
⇒流動資産(1.0)[1.0]{summation-item,False}
⇒資産⇒310040 貸借対照表

・定義リンク (definitionLink)
現金及び預金(1.0){domain-member,False,true}
⇒流動資産(1.0){domain-member,False,true}
⇒資産の部(1.0){domain-member,False,true}
⇒貸借対照表(2.0){domain-member,False,true}
⇒連結貸借対照表⇒310010 連結貸借対照表

・フットノートリンク (footnoteLink)
footnote(※2,注記番号)
⇒現金及び預金(4906928000,IdFact1707927900)
⇒310010 連結貸借対照表

fact から『表示リンクの階層構造』や『計算リンクの階層構造』を取得できるようになって、とても便利になりました。

Arelle をマルチプロセスで使用するコード例も書きました。

Arelle をマルチプロセスで実行するコード例【XBRL】

さて、データが取得出来たら、次は『分析』ですね。

自分の場合は、XBRL の勘定科目タグを整理して、各社の『決算グラフ』描いて、株式銘柄の分析に役立ててきました。

そのあたりの話は、『やればできる!XBRLで決算分析システムを作るにあたって難しかったところ妥協したところ』に書きました。

スポンサーリンク
シェアする(押すとSNS投稿用の『編集ページ』に移動します)
フォローする(RSSフィードに移動します)
スポンサーリンク
シラベルノート
タイトルとURLをコピーしました