旧EDINET XBRLを読み込むコード例 (jpfr, ifrs)【Python】

スポンサーリンク

古いバージョンのEDINET XBRLを読み込むコード例です。

2013年以前の決算を取得するために作りました。

新・旧を組み合わせると、2008年からの決算データベースができます。

これにPDFの手入力を組み合わせれば、ライザップグループの業績グラフを作ることができます。

XBRLを解析するクラス

XBRL解析で財務情報などを取得するクラスです。

EDINET XBRLをExcelに変換するコード例【Python】 のコードとほぼ同じです。

違いは、名前空間定義ファイル名解析会計基準・連結区分の指定方法のあたりです。

重複する部分は省略して、変更した部分のコードを示します。

 

名前空間を定義する

勘定科目のタグや属性を検索する時に名前空間が要りますので、定義を追加します。EDINET XBRLの仕様書から、名前空間の定義を転記します。

 

「self」は企業別タクソノミです。簡単のために、selfだけ名前空間の接頭辞の方にマッチさせるための正規表現を書いています。

 

ちなみに、旧EDINETでは「企業別タクソノミ」と呼ばれていて、新EDINETでは「提出者別タクソノミ」と呼ばれています。色々と範囲が企業だけではなくなったようです。

 

追加した名前空間です。

"""xbrl_namespace.py"""
import re

# 報告書インスタンス作成ガイドライン 名前空間宣言 20130301
NS_INSTANCE_20130301 = {
    'xbrli': 'http://www.xbrl.org/2003/instance',
    'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    'xlink': 'http://www.w3.org/1999/xlink',
    'link': 'http://www.xbrl.org/2003/linkbase',
    'iso4217': 'http://www.xbrl.org/2003/iso4217',
    'self': re.compile('^jp[a-z]*-[a-z0-9]{3}-[A-Z][0-9]{5}-[0-9]{3}$').match,
    'jpfr-di': re.compile('^http://info.edinet-fsa.go.jp/jp/fr/gaap/o/di/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]$').match,
    'jpfr-oe': re.compile('^http://info.edinet-fsa.go.jp/jp/fr/gaap/o/oe/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]$').match,
    }

# 報告書インスタンス作成ガイドライン (その2:IFRS 適用提出者用) 名前空間宣言 20130301
# ifrs-{報告書}-{EDINET コード}-{追番}
NS_INSTANCE_IFRS_20130301 = {
    'xbrli': 'http://www.xbrl.org/2003/instance',
    'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    'xlink': 'http://www.w3.org/1999/xlink',
    'link': 'http://www.xbrl.org/2003/linkbase',
    'iso4217': 'http://www.xbrl.org/2003/iso4217',
    'self': re.compile('^ifrs-[a-z0-9]{3}[-_][A-Z][0-9]{5}-[0-9]{3}$').match,
    'xbrldt': 'http://www.xbrl.org/2005/xbrldt',
    'ifrs': re.compile('^http://xbrl.ifrs.org/taxonomy/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]/ifrs$').match,
    'xbrldi': 'http://xbrl.org/2006/xbrldi',
}

 

XBRLを読み込むところ

XBRL解析クラスの初期化部分です。

 

まず、XBRLのファイル名のフォーマットが変わりましたので、ファイル名解析メソッドを変更しました。それと、仕様と異なるファイル名がありましたので、それも一部対応してみました。

 

次に、コンテキストのscenarioタグです。勘定科目の連結区分の指定方法が変わっていますので、そこを変更しました。

 

あとは、「日本基準&米国基準」と「IFRS」で名前空間が異なっていましたので、会計基準に応じて変えるようにしました。会計基準は、ファイル名から判断するようにしてます

"""xbrl_jpfr.py"""

from os.path import join as os_join
from os.path import basename as os_basename
from os.path import splitext as os_splitext
from os.path import dirname as os_dirname
from re import match as re_match
from collections import OrderedDict
from dateutil.parser import parse as dateutil_parser_parse
from traceback import format_exc
import xbrl_namespace
from xbrl_util import get_etree_obj_from_file

class Parser:
    """xbrlファイル解析クラス"""
    def __init__(self, file, file_data):
        self.file = file

        # ファイル名解析
        self.info = self.parse_filename(os_basename(self.file))
        if self.info['報告書'] is None:
            # 再解析
            if 'E25850-' in self.file:
                self.info = self.parse_filename_e25850(os_basename(self.file))

        # XBRLファイル読み込み
        self.root = get_etree_obj_from_file(self.file, file_data)
        self.nsmap = self.root.nsmap
        self.ns_prefixes = {v: k for (k, v) in self.nsmap.items()}

        # 名前空間 文書情報タクソノミ
        self.ns_di = None

        # 名前空間 企業別タクソノミ
        self.ns_self = None

        # 名前空間 IFRS
        self.ns_ifrs = None

        # 名前空間 その他スキーマ
        self.ns_jpfr_oe = None

        # 名前空間 xbrldi
        self.ns_xbrldi = None

        # 勘定科目などを定義している名前空間を取得
        ns_list = []
        if self.info['会計基準'] == 'jpfr':
            # 名前空間(NameSpace)の定義を取得
            ns_def = xbrl_namespace.NS_INSTANCE_20130301

            for (ns_prefix, namespace) in self.nsmap.items():
                if ns_def['jpfr-di'](namespace):
                    ns_list.append((0, namespace))
                    self.ns_di = namespace
                elif re_match('^jpfr-t-[a-z]*$', ns_prefix):
                    ns_list.append((1, namespace))
                elif ns_def['self'](ns_prefix):
                    ns_list.append((2, namespace))
                    self.ns_self = namespace
                elif ns_def['jpfr-oe'](namespace):
                    self.ns_jpfr_oe = namespace

            ns_list.sort(key=lambda x: (x[0], x[1]), reverse=False)

        elif self.info['会計基準'] == 'ifrs':
            # 名前空間(NameSpace)の定義を取得
            ns_def = xbrl_namespace.NS_INSTANCE_IFRS_20130301

            for (ns_prefix, namespace) in self.nsmap.items():
                if ns_def['ifrs'](namespace):
                    ns_list.append((0, namespace))
                    self.ns_ifrs = namespace
                elif ns_def['self'](ns_prefix):
                    ns_list.append((1, namespace))
                    self.ns_self = namespace
                elif ns_def['xbrldi'] == namespace:
                    self.ns_xbrldi = namespace

            ns_list.sort(key=lambda x: (x[0], x[1]), reverse=False)

        # タグ名/属性名定義
        self.link_schema_ref = '{%s}schemaRef' % ns_def['link']
        self.xlink_href = '{%s}href' % ns_def['xlink']
        self.xbrli_context = '{%s}context' % ns_def['xbrli']
        self.xbrli_entity = '{%s}entity' % ns_def['xbrli']
        self.xbrli_identifier = '{%s}identifier' % ns_def['xbrli']
        self.xbrli_period = '{%s}period' % ns_def['xbrli']
        self.xbrli_start_date = '{%s}startDate' % ns_def['xbrli']
        self.xbrli_end_date = '{%s}endDate' % ns_def['xbrli']
        self.xbrli_instant = '{%s}instant' % ns_def['xbrli']
        self.xbrli_scenario = '{%s}scenario' % ns_def['xbrli']
        self.jpfr_oe_non_consolidated = '{%s}NonConsolidated' % self.ns_jpfr_oe if self.ns_jpfr_oe else None
        self.xbrldi_explicit_member = '{%s}explicitMember' % self.ns_xbrldi if self.ns_xbrldi else None
        self.xsi_nil = '{%s}nil' % ns_def['xsi']

        # xsdのファイルパスと名前空間を取得
        self.xsd = self.get_xsd_filepath(file_data)

        # コンテキストタグ(日付情報)取得
        self.context_tags = self.get_context_tags()

        # 管理情報・財務諸表データ取得
        self.xbrl_datas = []
        for (number, ns) in ns_list:
            self.xbrl_datas.append((ns, self.get_xbrl_datas(ns)))

        # 変数削除
        del self.root
        return

 

ファイル名解析メソッド

ファイル名から文書情報を取得するところです。

 

ファイル名のスライスする位置を変えました。あと、文字列→日付型変換を利用して、簡易的にファイル名のフォーマットをチェックする処理を追加しました。

 

jpfrから始まるファイル名も、ifrsから始まるファイル名も、このメソッドで解析できます。

    @staticmethod
    def parse_filename(s):
        """ファイル名解析"""
        # 2010年版EDINETタクソノミ及び関連資料の公表について http://www.fsa.go.jp/search/20100311.html
        # 報告書インスタンス作成ガイドライン
        # 3-3 報告書インスタンスのファイル名
        # jpfr-{報告書}-{EDINET コード又はファンドコード}-{追番}-{報告対象期間期末日}-{提出回数}-{提出日}.xbrl
        # 0         1         2         3         4
        # 01234567890123456789012345678901234567890123456789
        # jpfr-asr-E00000-000-2012-03-31-01-2012-06-22.xbrl
        od = OrderedDict()
        od.update({'会計基準': s[0:4]})

        try:
            # 第N四半期の数字を判定
            t = s[5:8]
            od.update({'報告書': t})

            if t == 'asr':
                # 有価証券報告書
                od.update({'第N期': 0})
            elif re_match('^q[1-5]r$', t):
                # 四半期報告書
                od.update({'第N期': int(t[1])})
            elif t == 'ssr':
                # 半期報告書
                od.update({'第N期': 2})
            else:
                od.update({'第N期': t})

            od.update({'EDINETコード_ファンドコード': s[9:15]})
            od.update({'追番': s[16:19]})
            od.update({'報告対象期間期末日': dateutil_parser_parse(s[20:30])})
            od.update({'提出回数': s[31:33]})
            od.update({'提出日': dateutil_parser_parse(s[34:44])})
        except ValueError:
            print('不正なファイル名\n%s' % format_exc())
            od.update({
                '報告書': None, '第N期': None, 'EDINETコード_ファンドコード': None,
                '追番': None, '報告対象期間期末日': None, '提出回数': None, '提出日': None,
                })
        return od

 

それと、ifrsの中で変則的なファイル名に遭遇しましたので、それに対応したメソッドも追加しました。文字列が増えているのと、ハイフンとアンダースコアの違いがありました。

先のメソッドで失敗したら、こちらを呼び出す感じです。

    @staticmethod
    def parse_filename_e25850(s):
        """ファイル名解析(E25850)"""
        # 報告書インスタンス作成ガイドライン
        # (その2: IFRS 適用提出者用) 報告書インスタンスのファイル名
        # ifrs-{報告書}-{EDINET コード}-{追番}-{報告対象期間期末日}-{提出回数}-{提出日}.xbrl
        #        0         1         2         3         4
        #        01234567890123456789012345678901234567890123456789
        #   通常 ifrs-asr-E00000-000-2012-03-31-01-2012-06-22.xbrl
        #        0         1         2         3         4         5
        #        012345678901234567890123456789012345678901234567890123
        # E25850 ifrs-asr-001_E00000-000_2014-12-31_01_2015-03-30.xbrl

        od = OrderedDict()
        od.update({'会計基準': s[0:4]})

        try:
            # 第N期の数字を判定
            t = s[5:8]
            od.update({'報告書': t})

            if t == 'asr':
                # 有価証券報告書
                od.update({'第N期': 0})
            elif re_match('^q[1-5]r$', t):
                # 四半期報告書
                od.update({'第N期': int(t[1])})
            elif t == 'ssr':
                # 半期報告書
                od.update({'第N期': 2})
            else:
                od.update({'第N期': t})

            # s[9:12] <- 無視 (不明な文字列)

            od.update({'EDINETコード_ファンドコード': s[13:19]})
            od.update({'追番': s[20:23]}) # <- 本当に追番なのかは不明
            od.update({'報告対象期間期末日': dateutil_parser_parse(s[24:34])})
            od.update({'提出回数': s[35:37]})
            od.update({'提出日': dateutil_parser_parse(s[38:48])})
        except ValueError:
            print('不正なファイル名\n%s' % format_exc())
            od.update({
                '報告書': None, '第N期': None, 'EDINETコード_ファンドコード': None,
                '追番': None, '報告対象期間期末日': None, '提出回数': None, '提出日': None,
                })
        else:
            print('ファイル名 再解析 OK')
        return od

 

NonConsolidatedタグ取得メソッド(連結区分)

旧EDINET XBRLでは、NonConsolidatedタグで連結区分を指定しています。このタグが存在すると、個別財務諸表の勘定科目になる、とのことです。

 

このあたりの仕様は、2013年3月1日公表の「報告書インスタンス作成ガイドライン ⇒ コンテキストの定義 ⇒ シナリオ要素の設定」に載っていました。

 

コードは、コンテキストのscenarioタグ取得部分を変えました。NonConsolidatedタグを取得するメソッド(日本基準用)と、explicitMemberタグを取得するメソッド(IFRS用)を用意しました。

    def get_context_tags(self):
        """contextタグ取得"""

            (中略)

            # scenarioタグ取得
            scenario = OrderedDict()
            for (n, et_scenario) in enumerate(element.findall('.//%s' % self.xbrli_scenario), start=1):
                # scenarioは通常1つ
                assert n == 1

                if self.info['会計基準'] == 'jpfr':
                    # NonConsolidatedタグ取得
                    scenario.update(self.get_non_consolidated_tag(et_scenario))
                    od[key_id].update({'scenario': scenario})
                elif self.info['会計基準'] == 'ifrs':
                    # explicitMemberタグ取得
                    scenario.update(self.get_explicit_member_tags(et_scenario))
                    od[key_id].update({'scenario': scenario})
        return od

 

NonConsolidatedタグ取得メソッドです(日本基準用)

    def get_non_consolidated_tag(self, element):
        """NonConsolidatedタグ取得"""
        od = OrderedDict()
        for (n, et_non_consolidated) in enumerate(element.findall('.//%s' % self.jpfr_oe_non_consolidated), start=1):
            # NonConsolidatedは通常1つ
            assert n == 1

            # <jpfr-oe:NonColsolidated/>
            # タグ名のみ取り出す
            od.update({'tag': et_non_consolidated.tag.rsplit('}', maxsplit=1)[1]})
        return od

 

explicitMemberタグ取得メソッドです(IFRS用)

    def get_explicit_member_tags(self, element):
        """explicitMemberタグ取得"""
        od = OrderedDict()
        for et_explicit_member in element.findall('.//%s' % self.xbrldi_explicit_member):
            key = et_explicit_member.get('dimension')

            assert key not in od

            od.update({key: et_explicit_member.attrib})
            od[key].update({'text': et_explicit_member.text})
        return od

 

これらに合わせて、連結区分の真偽値を判定する関数も変更しました。ただし、日本基準(jpfr)だけの対応です。IFRSに関しては、連結区分を簡単に判別することができませんでした。よって、とりあえず None を設定しています。

 

このIFRS用XBRLの仕様は以下に載っていましたが、scenarioタグの仕様が日本基準の場合と大きく異なっていました。コンテキストでは区別しない旨が書かれています。

  • 2013年版「報告書インスタンス作成ガイドライン(その2:IFRS 適用提出者用) ⇒ コンテキストの期間時点(period)要素の設定方法 ⇒ シナリオ要素の設定
  • 2013年版「企業別タクソノミ作成ガイドライン(その2:IFRS 適用提出者用) ⇒ IFRSタクソノミの概要 ⇒ 連結財務諸表等と個別財務諸表等の区別について

■ 連結財務諸表等と個別財務諸表等の区別について
IFRS タクソノミでは、連結財務諸表等と個別財務諸表等の区別を行う場合、報告書インスタンスにおいて設定するコンテキストで区別するのではなく XBRL Dimensions を使用します。ただし、IFRS 基準で連結財務諸表のみを作成する場合は XBRL Dimensionsの使用を省略できます。XBRL Dimensions に関する詳細は「6-2 XBRL Dimensions 関連の要素について」を参照してください。

 

以下、連結の真偽値を取得するコードです。

    # コンテキストタグのscenario辞書から連結の真偽値を取得する関数
    def get_consolidated_or_nonconsolidated(x):
        """scenarioから連結の真偽値を取得"""
        if xbrl.info['会計基準'] == 'jpfr':
            if 'scenario' in x:
                if x['scenario']['tag'] == 'NonConsolidated':
                    return False
            return True
        else:
            return None

 

ほかのメソッドは2014年以降のものと同じ

xsdファイルパス取得・コンテキストタグ取得・勘定科目データ取得のあたりは同じなので、EDINET XBRLをExcelに変換するコード例【Python】 のメソッドをコピーして使っています。

 

同じなら新・旧でモジュール一緒にしても良さそうなんですが、とりあえず、パイソンファイルを分けています。ほかにも色々な違いに遭遇するかもしれないので。

 

あと、実際にXBRLを読み込んでExcelに保存する部分も同じです。

xbrl_jpcor.py の代わりに xbrl_jpfr.py をインポートして、xbrl_jpfr.Parser(xbrl_file)を使うようにします。

 

新・旧を切り替えながら全上場企業について実行する

ファイル名でバージョン判定しながらモジュールを切り替えれば、2008年からの決算情報が取得できます。さて、これで新・旧あわせてデータが揃ったわけですが、ここから時系列分析や他社との比較をするためには、勘定科目の選択・集約といった作業が入ってきます

 

一番多彩な勘定科目は、売上高でした。企業・業種によって、何を売上高と見なすかはかなり幅があったんですね。特に、巨大な企業では全体的な売上高と実質的な売上高みたいな感じで、2種類の概念があったりしました。

 

次いで営業利益・経常利益・当期純利益です。このあたりは、会計基準によっても変わってきました。同じ会計基準でも意味づけが異なるのか、これもタグ名に幅がありました。一方で、流動資産・負債、固定資産・負債といった科目は、ほぼ共通のタグ名でした。

 

このような感じで、異業種はもちろん、同業でもかなり違いがありましたし、同じ企業の時系列でも、タグづけが変化したりしてました。

 

こういう集約する部分のコードもなかなか大変ですが、ここはまた、面白いところでもありました。自分が何を見たいか、何と何を比較したいかは、まさに自分の興味・研究に依って判断していくところでしたので。

 

業績のグラフができたら、株価やニュース・開示情報を重ねて、さらに深い分析が可能になります。決算情報も、このくらいの時系列が手元にあると、かなり納得のいく分析ができます。

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