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

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

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

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

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

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

『勘定科目 (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 から『表示リンクの階層構造』や『計算リンクの階層構造』を取得できるようになって、とても便利になりました。

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