python

pythonで有価証券報告書(XBRL形式)をパースする

投稿日:

今年最後の更新になります。
pythonを使用して、株式会社の財務情報をパースするコードを作成しました。

要件・仕様

要件は以下とします。

  1. 株式会社の財務情報をパースする
  2. 取得する財務情報は、売上高、営業利益、ROEとする

仕様は以下とします。

  1. 株式会社の有価証券報告書のxbrlファイルを、pythonでパースする
  2. 特定の形式のxbrlファイル(Japan GAAP)から、売上高、営業利益、ROEをパースする

XBRLとは

今回使用するxbrlファイルとは、有価証券報告書に採用されている、データ記述方法の一つです。
xbrlについての説明は以下のサイトが詳しいです。

本記事ではxbrlの詳細な説明は割愛します。

有価証券報告書の会計基準について

有価証券報告書は、大きく分けて三種類の会計基準によって記述されます。

  • Japan GAAP
  • US GAAP
  • IFRS

これらの違いとしては、「営業利益」等の項目に載せる内容の違いなどの概念的なものから、xbrlで記述される項目の差異までに及びます。

今回は、単純のために、日本で一般的な会計基準であるJapan GAAPに絞って、パースを行います。

設計・実装

以下のサイトを参考に、xbrlをパースするクラスであるXbrlParser.pyを作成しました。

実装を記述します。

import os
import re
from collections import defaultdict
import xml.etree.ElementTree as ET
from xbrl import XBRLParser, GAAP, GAAPSerializer
import pandas as pd
import requests


class XbrlParser():
    def __init__(self, xbrl_filename):
        self.xbrl_filename = xbrl_filename
        self.base_filename = xbrl_filename.replace('.xbrl', '') 
        self.dei = ""
        self.facts = None
        self.labels = None
        self.presentation = None
        self.finance = None
        self.company_name = ""
        self.fund_code = ""
        self.has_report = False
        self.start_date = ""
        self.end_date = ""
        self.net_sales = ""
        self.roe = ""
        self.operating_profit = ""  

    def ParseXbrl(self, namespaces):
        result = defaultdict(dict)
        result['facts'] = self.__GetFactsInfo()

        label_file_name = self.base_filename+'_lab.xml'
        ET.register_namespace('', 'http://www.w3.org/2005/Atom')
        labels = ET.parse(label_file_name)

        # get enterprise taxonomy
        extended_labels = self.__GetLabelInfo(namespaces, labels)

        # get base link
        base_labels = self.__GetBaseLabelInfo(namespaces)

        extended_labels = extended_labels.append(base_labels, ignore_index=True)
        result['labels'] = extended_labels
        result['presentation'] = self.__GetPresentationInfo(namespaces)

        # generate dataframe from dict
        self.facts = result['facts'] # 金額の定義及び文書情報の定義情報
        self.labels = result['labels'] # 名称リンク情報
        self.presentation = result['presentation'] # 表示リンク情報
        self.lavels = self.__ExtractTargetData(self.labels, lang='ja')
        self.labels = self.labels.drop_duplicates()
        self.finance = pd.merge(self.labels, self.facts, on='element_id', how='inner')

        self.dei = self.__GetDei()
        self.company_name = self.__GetCompanyName()
        self.fund_code = self.__GetCompanyFundCode()
        self.has_report = self.__GetHasReportDocs()
        self.start_date = self.__GetStartDate()
        self.end_date = self.__GetEndDate()

        self.finance = self.GetCurrentYearFinance()  # current year only
        self.net_sales = self.__GetNetSales()
        self.roe = self.__GetRoe()
        self.operating_profit = self.__GetOperatingProfit()
        return

    def GetDeiType(self):
        return self.dei

    def GetCompanyName(self):
        return self.company_name

    def GetCompanyFundCode(self):
        return self.fund_code

    def GetHasReportDocs(self):
        return self.has_report

    def GetStartDate(self):
        return self.start_date

    def GetEndDate(self):
        return self.end_date

    def GetCurrentYearFinance(self):
        df_cy1 = self.finance.ix[self.finance.context_ref == 'CurrentYearInstant'] # 当期 時点
        df_cy2 = self.finance.ix[self.finance.context_ref == 'CurrentYearDuration'] # 当期 時点
        df_cyi = df_cy1.append(df_cy2)
        return df_cyi

    def GetNetSales(self):
        if self.net_sales is None:
          return None
        return self.net_sales

    def GetRoe(self):
        if self.roe is None:
          return None
        return self.roe

    def GetOperatingProfit(self):
        if self.operating_profit is None:
          return None
        return self.operating_profit

    def __GetDei(self):
        dei = ""
        for i,e_id in self.finance['element_id'].iteritems():
          if e_id == "jpdei_cor_accountingstandardsdei":
            dei = self.finance.ix[[i], ['amount']].values[0]
            break
        return dei

    def __GetCompanyName(self):
        df = self.finance.ix[self.finance.element_id.str.contains("filernameinenglishdei"), "amount"].drop_duplicates()
        return df.iloc[0]

    def __GetCompanyFundCode(self):
        df = self.finance.ix[self.finance.element_id.str.contains("securitycodedei"), "amount"].drop_duplicates()
        return df.iloc[0]

    def __GetHasReportDocs(self):
        df = self.finance.ix[self.finance.element_id.str.contains("whetherconsolidatedfinancialstatementsareprepareddei"), "amount"].drop_dup
        return df.iloc[0]

    def __GetStartDate(self):
        df = self.finance.ix[self.finance.element_id.str.contains("currentfiscalyearstartdatedei"), "amount"].drop_duplicates()
        return df.iloc[0]

    def __GetEndDate(self):
        df = self.finance.ix[self.finance.element_id.str.contains("currentfiscalyearenddatedei"), "amount"].drop_duplicates()
        return df.iloc[0]



    def __GetNetSales(self):
        if self.dei == "Japan GAAP":
          df = self.finance.ix[self.finance.element_id.str.contains("_netsalessummaryofbusinessresults"), "amount"].drop_duplicates()
        else:
          return None

        if len(df) == 0:
          return None
        else:
          return df.iloc[0]

    def __GetRoe(self):
        if self.dei == "Japan GAAP":
          df = self.finance.ix[self.finance.element_id.str.contains("_equitytoassetratiosummaryofbusinessresults"), "amount"].drop_duplicates
        else:
          return None

        if len(df) == 0:
          return None
        else:
          return df.iloc[0]

    def __GetOperatingProfit(self):
        if self.dei == "Japan GAAP":
          df = self.finance.ix[self.finance.element_id.str.contains("_operatingincome"), "amount"].drop_duplicates()
        else:
          return None

        if len(df) == 0:
          return None
        else:
          return df.iloc[0]

    def __GetBaseLabelInfo(self, namespaces):
        base_file_path = os.getcwd()+'/base_labels/'
        if not os.path.exists(base_file_path):
            os.mkdir(base_file_path)
        base_labels = None

        # get common taxonomy
        for link_base in self.__GetLinkBase(namespaces):
            file_path = base_file_path + link_base.strip().split('/')[-1]

            if os.path.exists(file_path):
                tmp_base_labels = pd.read_csv(file_path)
            else:
                print('creating ', link_base, 'base label data...')
                response = requests.get(link_base)
                html = response.text
                ET.register_namespace('', 'http://www.xbrl.org/2003/linkbase')
                labels = ET.fromstring(html)
                labels = labels.findall('.//link:labelLink', namespaces=namespaces)[0]

                tmp_base_labels = self.__GetLabelInfo(namespaces, labels)
                tmp_base_labels.to_csv(file_path, index=False)
            if base_labels is not None:
                base_labels = base_labels.append(tmp_base_labels, ignore_index=True)
            else:
                base_labels = tmp_base_labels
        return base_labels

    def __ConcatDictionary(self, dict1, dict2):
        for key in dict1.keys():
            dict1[key] = dict1[key]+dict2[key]
        return dict1

    def __GetFactsInfo(self):
        """
        return(element_id, amount, context_ref, unit_ref, decimals)
        """
        # parse xbrl file
        xbrl = XBRLParser.parse(open(self.xbrl_filename)) # beautiful soup type object
        facts_dict = defaultdict(list)

        #print xbrl
        name_space = 'jp*'
        for node in xbrl.find_all(name=re.compile(name_space+':*')):
            if 'xsi:nil' in node.attrs:
                if node.attrs['xsi:nil'] == 'true':
                    continue

            facts_dict['element_id'].append(node.name.replace(':', '_'))
            facts_dict['amount'].append(node.string)

            facts_dict['context_ref'].append(self.__GetAttrValue(node, 'contextref'))
            facts_dict['unit_ref'].append(self.__GetAttrValue(node, 'unitref'))
            facts_dict['decimals'].append(self.__GetAttrValue(node, 'decimals'))
        return pd.DataFrame(facts_dict)

    def __GetAttrValue(self, node, attrib):
        if attrib in node.attrs.keys():
            return node.attrs[attrib]
        else:
            return None

    def __GetLinkBase(self, namespaces):
        label_file_name = self.base_filename+'.xsd'
        ET.register_namespace('', 'http://www.w3.org/2001/XMLSchema')
        labels = ET.parse(label_file_name)
        linkbases = labels.findall('.//link:linkbaseRef', namespaces=namespaces)

        link_base = []
        for link_node in linkbases:
            link_href = link_node.attrib['{'+namespaces['xlink']+'}href']
            if '_lab.xml' in link_href and 'http://' in link_href:
                link_base.append(link_href)
        return link_base

    def __GetLabelInfo(self, namespaces, labels):
        """
        return(element_id, label_string, lang, label_role, href)
        """
        label_dict = defaultdict(list)

        #label_file_name = self.base_filename+'_lab.xml'
        #ET.register_namespace('','http://www.w3.org/2005/Atom')
         #labels = ET.parse(label_file_name)

        for label_node in labels.findall('.//link:label', namespaces=namespaces):
            label_label = label_node.attrib['{'+namespaces['xlink']+'}label']

            for labelArc_node in labels.findall('.//link:labelArc', namespaces=namespaces):
                if label_label != labelArc_node.attrib['{'+namespaces['xlink']+'}to']:
                    continue

                for loc_node in labels.findall('.//link:loc', namespaces=namespaces):
                    loc_label = loc_node.attrib['{'+namespaces['xlink']+'}label']
                    if loc_label != labelArc_node.attrib['{'+namespaces['xlink']+'}from']:
                        continue

                    lang = label_node.attrib['{'+namespaces['xml']+'}lang']
                    label_role = label_node.attrib['{'+namespaces['xlink']+'}role']
                    href = loc_node.attrib['{'+namespaces['xlink']+'}href']

                    label_dict['element_id'].append(href.split('#')[1].lower())
                    label_dict['label_string'].append(label_node.text)
                    label_dict['lang'].append(lang)
                    label_dict['label_role'].append(label_role)
                    label_dict['href'].append(href)
        return pd.DataFrame(label_dict)
    def __GetPresentationInfo(self, namespaces):
        """
        return(element_id, label_string, lang, label_role, href)
        """
        type_dict = defaultdict(list)

        label_file_name = self.base_filename+'_pre.xml'
        ET.register_namespace('', 'http://www.w3.org/2005/Atom')
        types = ET.parse(label_file_name)

        for type_link_node in types.findall('.//link:presentationLink', namespaces=namespaces):
            for type_arc_node in type_link_node.findall('.//link:presentationArc',
                                                        namespaces=namespaces):
                type_arc_from = type_arc_node.attrib['{'+namespaces['xlink']+'}from']
                type_arc_to = type_arc_node.attrib['{'+namespaces['xlink']+'}to']

                matches = 0
                for loc_node in type_link_node.findall('.//link:loc', namespaces=namespaces):
                    loc_label = loc_node.attrib['{'+namespaces['xlink']+'}label']

                    if loc_label == type_arc_from:
                        if '{'+namespaces['xlink']+'}href' in loc_node.attrib.keys():
                            href_str = loc_node.attrib['{'+namespaces['xlink']+'}href']
                            type_dict['from_href'].append(href_str)
                            type_dict['from_element_id'].append(href_str.split('#')[1].lower())
                            matches += 1
                    elif loc_label == type_arc_to:
                        if '{'+namespaces['xlink']+'}href' in loc_node.attrib.keys():
                            href_str = loc_node.attrib['{'+namespaces['xlink']+'}href']
                            type_dict['to_href'].append(href_str)
                            type_dict['to_element_id'].append(href_str.split('#')[1].lower())
                            matches += 1
                    if matches == 2: break

                role_id = type_link_node.attrib['{'+namespaces['xlink']+'}role']
                arcrole = type_arc_node.attrib['{'+namespaces['xlink']+'}arcrole']
                order = self.__GetXmlAttrValue(type_arc_node, 'order')
                closed = self.__GetXmlAttrValue(type_arc_node, 'closed')
                usable = self.__GetXmlAttrValue(type_arc_node, 'usable')
                context_element = self.__GetXmlAttrValue(type_arc_node, 'contextElement')
                preferred_label = self.__GetXmlAttrValue(type_arc_node, 'preferredLabel')

                type_dict['role_id'].append(role_id)
                type_dict['arcrole'].append(arcrole)
                type_dict['order'].append(order)
                type_dict['closed'].append(closed)
                type_dict['usable'].append(usable)
                type_dict['context_element'].append(context_element)
                type_dict['preferred_label'].append(preferred_label)
        return pd.DataFrame(type_dict)

    def __GetXmlAttrValue(self, node, attrib):
        if attrib in node.attrib.keys():
            return node.attrib[attrib]
        else:
            return None

    def __ExtractTargetData(self, df, element_id=None, label_string=None, \
                                lang=None, label_role=None, href=None):
        if element_id is not None:
            df = df.ix[df['element_id'] == element_id, :]
        if label_string is not None:
            df = df.ix[df.label_string.str.contains(label_string), :]
        if lang is not None:
            df = df.ix[df['lang'] == lang, :]
        if label_role is not None:
            df = df.ix[df.label_role.str.contains(label_role), :]
        if href is not None:
            df = df.ix[df['href'] == href, :]
        return df

かなり巨大なクラスになってしまいました。。。

これを使用するmain文であるparse_xbrl.pyは以下になります。

import os
import re
import sys 
import XbrlParser
import pandas as pd


def main(namespaces):
    args = sys.argv

    xbrl_list = []
    for root, dirs, files in os.walk(u'./xbrl_files/'):
        for file_ in files:
            if os.path.splitext(file_)[1] == u'.xbrl':
                full_path = os.path.join(root, file_)
                if "PublicDoc" in full_path:
                    xbrl_list.insert(0, full_path)
    print(xbrl_list)

    count = 1 
    for xbrl_filename in xbrl_list:
        count = count + 1 
        # get data
        xp = XbrlParser.XbrlParser(xbrl_filename)

        print('getting data...' + xbrl_filename)
        xp.ParseXbrl(namespaces)
        print('done')
        print('Dei Type : ' + xp.GetDeiType())
        if xp.GetDeiType() != "Japan GAAP":
          print("is not Japan GAAP. skip...")
          continue

        start_date = xp.GetStartDate()
        end_date = xp.GetEndDate()
        name = xp.GetCompanyName()
        code = xp.GetCompanyFundCode()
        net_sales = xp.GetNetSales()
        operating_profit = xp.GetOperatingProfit()
        roe = xp.GetRoe()
        if not start_date or not end_date or not name or not code or not net_sales or not operating_profit or not roe:
          print("can't get something.skip...")
          continue

        print("Start date:" + start_date)
        print("End date:" + end_date)
        print("Name:" + name)
        print("Code:" + code)

        print("Net Sales:" + net_sales)
        print("Operating Profit:" + operating_profit)
        print("ROE:" + roe)

if __name__ == '__main__':
    main({
        'link': 'http://www.xbrl.org/2003/linkbase',
        'xml':'http://www.w3.org/XML/1998/namespace',
        'xlink':'http://www.w3.org/1999/xlink',
        'xsi':'http://www.w3.org/2001/XMLSchema-instance'
        })
  • 先程作成したXbrlParserクラスをインスタンス化し、xbrlをパースします。
  • パース後、XbrlParserクラスから以下を取得し標準出力で出力します
    • 取得した報告書の対象日時(開始・終了)
    • 社名
    • 証券コード
    • 売上高
    • 営業利益
    • ROE
  • 会計基準がJapan GAAPでなかったり、取得できない項目があった場合は、スキップして次のxbrlファイルを読み込みます

テスト

カレントディレクトリに、xbrlファイルを以下のようなディレクトリ構成で配置します。

(カレントディレクトリ)
|--xbrl_files
|  |--7201
|  |  |--ED2015062500303
|  |  |  |--S10056GN
|  |  |  |  |--XBRL
|  |  |  |  | |--(略)
|  |--7203
(略)
  • xbrl_filesというディレクトリを作ります
  • その下に、EDINET(http://disclosure.edinet-fsa.go.jp/)からダウンロードした有価証券報告書のzipファイルを展開したもの(上図のED2015062500303ディレクトリ)を配置します。
    • 今回は、各証券コードでディレクトリを作り、その下に配置しています

この状態で、カレントディレクトリからparse_xbrl.pyを実行します。

user@hostname:~# python parse_xbrl.py 
'./xbrl_files/7201/ED2015062500303/S10056GN/XBRL/PublicDoc/jpcrp030000-asr-001_E02142-000_2015-03-31_01_2015-06-25.xbrl', './xbrl_files/7267/ED2015062603312/S100577O/XBRL/PublicDoc/jpcrp030000-asr-001_E02166-000_2015-03-31_01_2015-06-26.xbrl', './xbrl_files/7203/ED2015062400848/S1005156/XBRL/PublicDoc/jpcrp030000-asr-001_E02144-000_2015-03-31_01_2015-06-24.xbrl', './xbrl_files/7270/ED2015062400586/S10054RJ/XBRL/PublicDoc/jpcrp030000-asr-001_E02152-000_2015-03-31_01_2015-06-24.xbrl', './xbrl_files/7269/ED2015063000880/S10056TN/XBRL/PublicDoc/jpcrp030000-asr-001_E02167-000_2015-03-31_01_2015-06-30.xbrl']
getting data..../xbrl_files/7201/ED2015062500303/S10056GN/XBRL/PublicDoc/jpcrp030000-asr-001_E02142-000_2015-03-31_01_2015-06-25.xbrl
done
['Dei Type : Japan GAAP']
Start date:2014-04-01
End date:2015-03-31
Name:NISSAN MOTOR CO., LTD.
Code:72010
Net Sales:11375207000000
Operating Profit:589561000000
ROE:0.284
getting data..../xbrl_files/7267/ED2015062603312/S100577O/XBRL/PublicDoc/jpcrp030000-asr-001_E02166-000_2015-03-31_01_2015-06-26.xbrl
done
['Dei Type : IFRS']
is not Japan GAAP. skip...
getting data..../xbrl_files/7203/ED2015062400848/S1005156/XBRL/PublicDoc/jpcrp030000-asr-001_E02144-000_2015-03-31_01_2015-06-24.xbrl
done
['Dei Type : US GAAP']
is not Japan GAAP. skip...
getting data..../xbrl_files/7270/ED2015062400586/S10054RJ/XBRL/PublicDoc/jpcrp030000-asr-001_E02152-000_2015-03-31_01_2015-06-24.xbrl
done
['Dei Type : Japan GAAP']
Start date:2014-04-01
End date:2015-03-31
Name:Fuji Heavy Industries Ltd.
Code:72700
Net Sales:2877913000000
Operating Profit:423045000000
ROE:0.465
getting data..../xbrl_files/7269/ED2015063000880/S10056TN/XBRL/PublicDoc/jpcrp030000-asr-001_E02167-000_2015-03-31_01_2015-06-30.xbrl
done
['Dei Type : Japan GAAP']
Start date:2014-04-01
End date:2015-03-31
Name:SUZUKI MOTOR CORPORATION
Code:72690
Net Sales:3015461000000
Operating Profit:179424000000
ROE:0.456

今回は例として、日産、トヨタ、ホンダ、スバル、スズキの2014年度の有価証券報告書をパースしました。
このうち、トヨタとホンダは会計基準がそれぞれUS GAAP、IFRSだったため読み込めませんでした。
一方、日産、スバル、スズキに関しては、正しく売上高、営業利益、ROEを読み込むことができました。

-python
-

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

関連記事

Travis CIを使ったPythonプロジェクト構築

Travis CIを使ってみたかったのでメモ。 目次1 概要2 前準備2.1 Travis CIのアカウントを作成3 リポジトリ作成4 テストのための設定4.1 Travis CI用の設定5 コミット …

PythonでQuandlから株価を取得する

今回は、巷で流行りのプログラミング言語であるPythonの勉強として、Pythonを利用して株価を取得するコードを作成しました。 環境は以下です。 OS:Ubuntu16.04 Python:3.6. …

PyQueryを使用し株式会社の決算速報をスクレイピング

今回はpythonを利用したスクレイピングの練習として、PyQueryを利用したスクレイピングを行うコードを作成しました。 目次1 注意2 要件と仕様3 PyQueryとは4 設計・実装5 テスト5. …