LoginSignup
6
6

More than 3 years have passed since last update.

上場会社の有価証券報告書XBRLを取得して値を読み込む(←主に名前空間の取得の部分を改良

Last updated at Posted at 2019-06-25

1.elementTreeとlxml

 上がelementTreeを使ったXML文字列の読み込みおよびタグを閲覧し値を取得するスクリプト、下がlxmlを使ったXML文字列の読み込みおよびタグを閲覧し値を取得するスクリプト↓

a)ElementTreeを使ったスクリプト

test_elmtree.py
# elementTreeを利用する
from xml.etree import ElementTree

def test_xml():

    #XML文字列の生成
    txt = '<?xml version="1.0"?><root xmlns="http://example.com/test" version="1.0">'
    txt = txt +'<language>ja</language><provider>John Doe</provider><entries>'
    txt = txt +'<entry><title>title 1</title><date>2011-07-27</date><body>日は晩景になりにたり</body></entry>'
    txt = txt +'<entry><title>title 3</title><date>2011-07-29</date><body>わが行く先は遥かなり</body></entry></entries></root>'

    print(txt)

    # XML文字列の読み込み
    root = ElementTree.fromstring( txt )

    #nsmap = root.nsmap
    #print(nsmap)

    # タグの閲覧と値取得
    for c1 in root:
        print(c1.tag)
        for c2 in c1:
            for c3 in c2:
                print(c3.tag, c3.text)

# main
if __name__ == '__main__':

    test_xml()

b)lxmlを使ったスクリプト

test_lxml.py
# lxmlを利用する
from lxml import etree

def test_xml():
    #XML文字列の生成
    txt = '<?xml version="1.0"?><root xmlns="http://example.com/test" version="1.0">'
    txt = txt +'<language>ja</language><provider>John Doe</provider><entries>'
    txt = txt +'<entry><title>title 1</title><date>2011-07-27</date><body>日は晩景になりにたり</body></entry>'
    txt = txt +'<entry><title>title 3</title><date>2011-07-29</date><body>わが行く先は遥かなり</body></entry></entries></root>'

    print(txt)

    # XML文字列の読み込み
    root = etree.fromstring( txt )

    # namespaceを取得
    nsmap = root.nsmap
    print(nsmap)

    # タグの閲覧と値取得
    for c1 in root:
        print(c1.tag)
        for c2 in c1:
            for c3 in c2:
                print(c3.tag, c3.text)

# main
if __name__ == '__main__':

    test_xml()

・lxmlでXMLオブジェクトを生成した場合、lxmlではnsmapでXMLの名前空間を取得することが可能。
・elementTreeではXMLの名前空間が取得できない、というかlxmlのように名前空間を取得するファンクション等が用意されていない。

2.lxmlを使ってXBRLファイルを読み込んで、名前空間を取得した上で、各タグを巡回して値を取得する

 EDINET提出の(有報キャッチャーAPIで取得できる)XBRLファイルは名前空間が定義されているので、elementTreeではなくlxmlを使ったほうが、名前空間を簡単に取得することが可能ですよという話。
 まずは修正前のelementTreeを用いたスクリプト。XBRLの名前空間をBeautifulSoupで該当文字列を取得。

before_mod.py

def fn_parse(sic , xbrl_path):

    ### タクソノミーURLの取得(using beautiful Soup)
    ff = open( xbrl_path , "r" ,encoding="utf-8" ).read() 
    soup = BeautifulSoup( ff ,"html.parser")

    x = soup.find_all("xbrli:xbrl" )
    x = str(x)[0:2000] 
    x = x.rsplit("xmlns")

    # 該当attr(crp)の取得
    crp  =['{' + i.replace( i[0:11] ,"").rstrip(' ').replace('"',"") + '}' for i in x[1:10] if i[0:11] == ':jpcrp_cor=']
    dei  =['{' + i.replace( i[0:11] ,"").rstrip(' ').replace('"',"") + '}' for i in x[1:10] if i[0:11] == ':jpdei_cor=']
    crp,dei = crp[0] ,dei[0]

 前回XBRLファイルをElementTreeで読みこむスクリプトを書いた際は、名前空間の取得ができなくて、最初にBeautifulSoupでHTML文字列として名前空間定義箇所をFind_all()で読んで苦し紛れに名前空間を取得していた。これをlxmlを使って名前空間を取得するように修正したのが以下のスクリプト↓

test_xbrl.py

from lxml import etree
import requests
import os
import zipfile
import re 
import pandas as pd


def fn_xbrl(sic,url):
       #XBRLダウンロード
       fn = str(sic) +".zip"
       os.system("wget -O " + str(fn) + " " + str(url))

       # ZIP解凍
       with zipfile.ZipFile( str(fn), 'r' )  as myzip:
          infos = myzip.infolist()
          for info in infos:
              base, ext = os.path.splitext(info.filename)
              if  str(base).find('Public')>0 and ext == '.xbrl':
                  myzip.extract(info.filename)
                  # XBRLパース 
                  print('■' + info.filename)
                  dict = fn_parse(sic , info.filename)

def fn_srch(sic):
        # 有報キャッチャー検索
        url='http://resource.ufocatch.com/atom/edinet/query/'+str(sic)+'0'
        r = requests.get(url)
        soup = BeautifulSoup( r.text,'html.parser')
        enty = soup.find_all("entry")

        for i in enty:
              ttl=i.find_all("title")
              # 有報のXBRL
              if str(ttl).find('有価証券報告書')>-1and str(ttl).find('訂正')==-1:
                  lnks=i.find_all("link")
                  print(ttl)
                  print(lnks[0])
                  print(lnks[1])
                  url =lnks[1].get('href')
                  fn = fn_xbrl(sic, url) if url !=''else 'hoge'

def fn_parse(sic , xbrl_path):

    # XML項目の取得(using lxml)
    tree = etree.parse( xbrl_path )
    root = tree.getroot()

    #名前空間を取得
    nsmap = root.nsmap
    #print(nsmap)
    crp = '{' + str(nsmap["jpcrp_cor"]) + '}'
    dei = '{' + str(nsmap["jpdei_cor"]) + '}'
    pfs = '{' + str(nsmap["jppfs_cor"]) + '}'

    #変数設定
    ename =''
    cnsl_emp, noncnsl_emp ,noncnsl_temp_emp = 0, 0, 0

    #XMLのタグの閲覧取得
    for c1 in root:
        #print(c1.tag)
        #print(c1.text)
          if c1.tag==crp+'CompanyNameInEnglishCoverPage': 
             #英文提出者(会社)名
               if ename != str(c1.text):
                  ename = str(c1.text)
                  print( ename )

          if c1.tag==crp+'NumberOfEmployees': 
             #連結従業員数
             if c1.get("contextRef")=="CurrentYearInstant":
                if cnsl_emp != str(c1.text):
                  cnsl_emp = str(c1.text)
                  print( cnsl_emp )
             #提出会社の従業員数
             if c1.get("contextRef")=="CurrentYearInstant_NonConsolidatedMember":
                if noncnsl_emp != str(c1.text):
                  noncnsl_emp = str(c1.text)
                  print( noncnsl_emp )
          #提出会社の臨時従業員数
          if c1.tag==crp+'AverageNumberOfTemporaryWorkers': 
             if c1.get("contextRef")=="CurrentYearInstant_NonConsolidatedMember":
                if noncnsl_temp_emp != str(c1.text):
                  noncnsl_temp_emp = str(c1.text)
                  print( noncnsl_temp_emp )

          #提出会社の平均年齢      
          if c1.tag==crp+'AverageAgeYearsInformationAboutReportingCompanyInformationAboutEmployees': 
                avg_age = str(c1.text)
                print( avg_age  )
          #提出会社の平均勤続年数      
          if c1.tag==crp+'AverageLengthOfServiceYearsInformationAboutReportingCompanyInformationAboutEmployees': 
                avg_lgs = str(c1.text)
                print( avg_lgs  )
          #提出会社の平均年収            
          if c1.tag==crp+'AverageAnnualSalaryInformationAboutReportingCompanyInformationAboutEmployees': 
                avg_sly = str(c1.text)
                print( avg_sly  )
          #(販管費の)研究開発費 <new!>
          if c1.tag==pfs+'ResearchAndDevelopmentExpensesSGA': 
             if c1.get("contextRef")=="CurrentYearDuration":
                avg_sly = str(c1.text)
                print( avg_sly  )
          #有形固定資産及び無形固定資産の増加額(≒設備投資額) <new!>
          if c1.tag==crp+'IncreaseInPropertyPlantAndEquipmentAndIntangibleAssets': 
             if c1.get("contextRef")=="CurrentYearDuration_ReportableSegmentsMember":
                avg_sly = str(c1.text)
                print( avg_sly  )


# main
if __name__ == '__main__':

      sic=8005
      fn_srch(sic)

・lxmlは、ElementTreeのラッパーライブラリ?のようなものらしいので、XBRLを読み込んでXMLオブジェクトを生成した後に各タグをループ文で閲覧していく箇所は同じように記述してタグの読み込みと値取得が可能。
・有価証券報告書のXBRLファイルを読み込むと、連結従業員数と非連結従業員数、臨時従業員数のタグが複数記述されている模様。1か所取得できればOkなので、変数格納時に1回取得できればいいように上記スクリプトでは微調整している。

・上場企業の場合、有価証券報告書提出前に大抵適時開示情報の決算短信XBRLを開示しているので、財務項目の数値等は有価証券報告書をEDINET金融庁に提出する約1カ月前に決算短信で取得可能であり、理論上はその際に株価形成されて株価に織り込まれるはず?なので、有価証券報告書XBRLで財務項目の値を取得することはないものとみなし、上記では、決算短信XBRLでは取得できない有価証券報告書XBRLでしか取得できない項目を重点的に値を取得するように考えてみた。それが例えば、従業員の項目(連結非連結の従業員数、従業員の平均年齢、平均勤続年数、平均給与)と大株主の項目(大株主名、大株主の住所、保有株数と保有比率など)、あとは研究開発費設備投資額の数値である。

・設備投資額の数値は、有価証券報告書XBRLでは「有形固定資産及び無形固定資産の増加額」というタグで値がセットされている。連結決算企業の場合、決算短信開示時点で、報告セグメントの表のところに記載されている場合があるが、短信XBRLレベルで設備投資額(有形固定資産及び無形固定資産の増加額)の値を取得できるかどうかは不明(未調査)。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6