プログラミング童貞を卒業する

かれらの生は狩猟と武事とで占められ、男は幼いころから刻苦にはげむ。
長く童貞をまもった者には、賞賛が待っている。禁欲によって背が伸び、体も強くなると考えられているためである。

カエサルガリア戦記


26歳でプログラミング童貞の私がPythonを学ぼうと思ったのは理由がある。

私はふだん経理の仕事をしており、上場企業の開示情報に触れる機会が多い。上場企業は金融商品取引法証券取引所のルール、公認会計士協会の定めた指針等の様々な決まりに従って有価証券報告書等の開示書類を作るのだが、これが各社とも非常に読み難いものに仕上がっている。
絶えずアップデートされる膨大な情報の中から有用な情報(例えば、直近3年で新規上場した会社が役員及び従業員に付与した新株予約権は発行済株式総数の何%か。また、それを決定する要因は何か。等)を探すためには、XBRLデータの抽出や整形、分析を包括的に行えるツールが必要だ。

Pythonは無料で使えてユーザーも多く、上記の目的に合った機能を提供してくれそうだったので、Pythonを勉強することにした。

f:id:hqac:20150908221958j:plain
" かくいう私も童貞でね "

Pythonについて

まずはPythonの開発者が書いたチュートリアルを読んだ。これはPythonの生みの親が書いた文章であり、とても分かりやすいと思う。

Pythonは、オブジェクト指向で動的型付けのインタープリタ言語である。

動的型付け

Pythonではデータを保持する型が動的に決定される。変数に整数値を格納すればそれは整数型(int)になり、小数点を格納すればそれは浮動小数点型(float)になる。

インタープリタ言語

C言語等一部のプログラミング言語は、実行前にソースコードをコンピュータが解釈できる「機械語」に変換する必要がある。これをコンパイルと呼ぶ。Python機械語への変換が必要だが、実行時に逐次バイトコード」と呼ばれる機械語プログラミング言語の中間の形式に変換しながら実行することができる。これには開発時に結果を確認しやすいというメリットがある。

上記と同じような特徴を持つRuby, Perlとの違いとしては、RubyはWebアプリケーションの開発、Perlは文字列の操作に秀でていることが挙げられるのに対し、Pythonの利点は世界中に利用者が多くて科学技術計算用のライブラリが充実していること、可読性が高いこと(インデントのスタイル等、書き方が言語仕様上制限されているため)がある。

Pythonのインストール

私はMacを使っているので、Pythonをインストールする前に、まずはHomebrewというMacのパッケージマネージャーをインストールした。こちらはソフトウェアの導入と削除、依存関係の管理等をやってくれる便利なソフトウェアとのこと。
Homebrew — OS X用パッケージマネージャー

Homebrewを入れたらターミナルで

brew install python

とすればPythonをインストールできる。

環境変数の設定後、ターミナルを開いてpythonとタイプすればPythonを使えるようになる。ちなみにMacにはデフォルトでPythonが入っているので実は新たにインストールする必要はないのだが、気合をいれるためインストールした。

データの取得

XBRLの入手

XBRLファイルを入手するプログラムを作ろうと思ったところ、幸いにも素晴らしい先人がいた。初心者なのでまずは先人に従おう。qiita.com
Pythonの文法に従ってこのプログラムを読んでみる。

requestsモジュールのインストール

今回使うモジュールのうち、requestsだけは標準ライブラリに含まれていないので、別途インストールする必要がある。
ターミナルを開いて、

pip install requests

で完了となる。
pipはpythonで書かれたパッケージ管理システムで、複数のソフトウェアをpipで一括して管理(インストール、更新、削除など)することができる。pipについて詳しくは下記のドキュメントを参照。
https://pip.pypa.io/en/stable/

文字コードの指定
# coding: utf-8

まずは文字コードUTF-8に指定する。Python2ではデフォルトの文字コードとしてASCIIが使われているが、これは半角英数字を扱うための文字コードであり、日本語を出力するとエラーになる。
日本語を出力するためには、上記の形式で日本語も扱える文字コードを指定する必要がある。なお、Python3ではデフォルトの文字コードutf-8となったため、上記の指定は必要無い。

モジュールのimport
import requests
import xml.etree.ElementTree as ET
from collections import defaultdict
import json
import os
from zipfile import ZipFile
from StringIO import StringIO

import文はモジュール(Pythonの組み込み以外の関数やクラス定義をまとめたファイル)を利用するときに使う。ちなみにPython2の組み込み関数はこちら
"import モジュール名"でimportが可能。モジュールの内の一部の要素(特定の関数等)をimportする際は、"import 関数名 from モジュール名"と書く。

ここでは複数のモジュールをimportしているため、一つずつ確認していく。

requests

PythonでHTTPリクエストやレスポンスを取り扱うためのライブラリ。
組み込みのurllib2より使いやすいらしい。
http://requests-docs-ja.readthedocs.org/en/latest/

xml.etree.ElementTree

PythonXMLを取り扱うためのライブラリ。
XMLをプログラム内で利用しやすい形に変換したり、XMLファイルを生成したりできる。
http://docs.python.jp/2.7/library/xml.etree.elementtree.html
ここではimportの際に"as ET"をつけることで、ETという別名をつけている。
これによってプログラム内ではETという名前で利用可能になる。

collectionsモジュールのdefaultdictクラス

組み込みのデータ型に代わる特殊なデータ型を提供するモジュール。
http://docs.python.jp/2/library/collections.html#collections.defaultdict
defaultdictクラスは組み込みの辞書型(dict)にインスタンス変数を追加した特殊なデータ型を提供する。
公式ドキュメントを見る限りでは、入れ子の辞書型オブジェクトを作るときに使うようだ。

json

PythonJSON形式のファイルを取り扱うためのライブラリ。
http://docs.python.jp/2.7/library/json.html

os

OS依存の機能をPythonから利用するためのモジュール。
http://docs.python.jp/2.7/library/os.html
getcwd()関数で現在の作業ディレクトリを表現する文字列を返したり、mkdir()関数で新たなディレクトリを作成したりすることができる。

zipfileモジュールのZipFile関数

ZIP形式のファイルの作成、読み書き、追記、書庫内のファイル一覧の作成を行うためのツールを提供するモジュール。
http://docs.python.jp/2/library/zipfile.html#zipfile-objects
ZipFile()関数は引数に指定したZip形式のファイルを開く関数。

StringIOモジュールのStringIOクラス

メモリ上の文字列をテキストファイルであるかのように取り扱うことができるモジュール。
http://docs.python.jp/2/library/stringio.html
著者によるとバイナリデータを扱うのに必要らしい。

今回のスクリプトでStringIOを使わずにZipファイルを解凍しようとすると以下のエラーが表示された。

TypeError: file() argument 1 must be encoded string without NULL bytes, not str

この辺は時間があったら詳しく調べてみようと思う。

get_link_info_str関数
def get_link_info_str(ticker_symbol, base_url):
    url = base_url+ticker_symbol
    response = requests.get(url)
    return response.text

この関数はまず、第2引数のURLに第1引数の文字列を足してURLを作る。
引数は後述のmain関数で与えられるが、第1引数は証券コード、第2引数はAPIのリクエスト先URLの証券コード以外の部分になる。
次に、requestsモジュールのget()関数を使い、作ったURLに対しGETリクエストを発行する。
返り値のHTTPレスポンスはresponseオブジェクトに格納され、return文でresponseオブジェクトのtext属性を返す。text属性には、HTTPレスポンスのボディ部分が格納されている。

get_link関数
def get_link(tree, namespace):
    yuho_dict = defaultdict(dict)
    for el in tree.findall('.//'+namespace+'entry'):
        title = el.find(namespace+'title').text
        if not is_yuho(title): continue
        print 'writing:',title[:30],'...'
        _id = el.find(namespace+'id').text
        link = el.find('./'+namespace+'link[@type="application/zip"]')
        url = link.attrib['href']
        yuho_dict[_id] = {'id':_id,'title':title,'url':url}
    return yuho_dict

この関数はまずdefaultdictクラスのインスタンスとしてyuho_dictオブジェクトを作る。このオブジェクトはダウンロード情報を格納する辞書を作るために使う(XBRLファイルをダウンロードする関数に引数として渡したりダウンロード情報を保存したりする)。

第1引数のtreeは後述のmain関数を見れば分かるようにElementクラスのインスタンスが入ることが想定されており、findall()メソッドを使うことができる。これは引数に取るパス名(またはタグ名)に合致する全てのサブエレメントを探し、ドキュメント上の順序で返すメソッドである。引数はXPath式となっている。
XMLは要素間に親子関係があるため、要素や属性、内容をノードとみなすと木構造のデータの一種であるといえ、この性質を利用してXML内の任意の位置を指定するための書式がXPathである。Pathの基本的な記述方法はルートからの絶対指定であるが、他にもいくつかの記述方法がある。
例えば、スラッシュ2つ(//)の後に目的の要素を記述することで、特的の要素名をもつ要素ノード全てを指定することができる。

ここでは、namespace+entryの要素をもつノード全てを探す。よってこのfor文は、namespace+entryの要素を持つノード全てに対しfor以下のプログラムを実行する。

まずは変数titleにel.find関数でnamespace+titleにマッチする最初のサブエレメントを返す。
次に、そのインスタンスから.text属性でその文字列を取り出す。更に、if以下で先ほどのtitleをis_yuho()関数に引数として渡す。
is_yuho()がfalseを返す場合は、continueによってループの先頭に戻る。print文では処理対象の有価証券報告書のtitleを出力する。

_idにnamespace+idに合致するサブエレメントの内容の文字列を格納し、linkにnamespace+linkタグのうちtype="application/zip"となる要素を格納し、urlにlinkのhref属性を格納する。
そしてyuho_dictの_idキーの値として、ダウンロード情報をまとめた辞書オブジェクトを格納する。

is_yuho関数
def is_yuho(title):
    if u'有価証券報告書' in unicode(title):
        return True
    else:
        return False

引数titleをunicode()コンストラクタを使ってUnicode文字列に変換し、その中に「有価証券報告書」が含まれるかを判定している。
なお、Python2においてUnicode文字列はUnicode型、Unicode以外の文字列は文字列型になるが、Python3においては文字列は全てUnicodeになっているため、このコンストラクタは必要無い。

write_download_info関数
def write_download_info(ofname):
    with open(ofname,'w') as of:
        json.dump(dat_download, of, indent=4)

with句を使うことで、そのブロックを抜けたときに自動でファイルをcloseすることができる。openは組み込みの関数で、引数で指定されたファイル(ofname)をオプションで指定されたモード(ここでは編集モード)で開く。
json.dump()関数はdat_downloadとofnameをJSON形式のファイルオブジェクトに直列化する。これでdat_downloadが一段と読みやすくなる(実際にこのスクリプトを実行してdat_downloadのJSONファイルを開いてみると読みやすくインデントされている)。

download_all_xbrl_files関数
def download_all_xbrl_files(download_info_dict,directory_path):
    for ticker_symbol, info_dicts in download_info_dict.items():
        save_path = directory_path+ticker_symbol
        if not os.path.exists(save_path):
            os.mkdir(save_path)

        for _id, info_dict in info_dicts.items():
            _download_xbrl_file(info_dict['url'],_id,save_path)

main関数のdat_downloadがdownload_info_dictに入る。
save_pathに第2引数directory_pathとticker_symbolを足したpathを格納する。if文以下で、もしsave_pathのパス名が存在しなければos.mkdir()関数で作成する。
その次のfor文でinfo_dict辞書のurlキーに保存された値、info_dicts辞書の_id、save_pathを使い_download_xbrl_file()関数でXBRLファイルをダウンロードする。

_download_xbrl_file関数
def _download_xbrl_file(url,_id,save_path):
    r = requests.get(url)
    if r.ok:
        #print url
        path = save_path+'/'+_id
        if not os.path.exists(path):
            os.mkdir(path)
        r = requests.get(url)
        z = ZipFile(StringIO(r.content))
        z.extractall(path) # unzip the file and save files to path.

まずはrequests.get()関数で引数のURLに対しHTTPリクエストを発行し、rにレスポンスオブジェクトを格納する。ok属性でリクエストが成功したかを確認できる(成功した場合はTrue)。
pathにはsave_path/_idのパスを格納する(これらは引数で与えられる)。次に、os.path.exists関数でpathが存在するか確かめる(存在する場合はTrue)。もしpathが存在する場合はなにもせず、存在しない場合はos.mkdir関数を使いpathで指定されたディレクトリを作成する。
次に、r.contentでレスポンスの本文を取り出し、ZipFileで開く。このとき、zip形式でアーカイブされているのでバイナリデータとして扱う必要があるため、StringIO関数を経由してZipFile関数に渡す。
最後にextractall()関数で全てのメンバーをアーカイブからカレントワーキングディレクトリに展開する。pathは、展開先のディレクトリとなる。

main関数
if __name__=='__main__':
    base_url = 'http://resource.ufocatch.com/atom/edinetx/query/'
    namespace = '{http://www.w3.org/2005/Atom}'
    t_symbols = ('1301','2432',)

    for t_symbol in t_symbols:
        response_string = get_link_info_str(t_symbol, base_url)
        ET_tree = ET.fromstring(response_string.encode('utf-8'))
        ET.register_namespace('',namespace[1:-1])

        dat_download = defaultdict(dict)
        # get download file info
        info_dict = get_link(ET_tree,namespace)
        dat_download[t_symbol] = info_dict

        ofname = os.getcwd()+'/downloaded_info/dat_download_'+t_symbol+'.json'
        if not os.path.exists(os.getcwd()+'/downloaded_info'):
            os.makedirs(os.getcwd()+'/downloaded_info')
        """
        if文を追加
        作業ディレクトリにofnameのJSONを格納するディレクトリがない場合は作成する
        """
        write_download_info(ofname)

        directory_path = os.getcwd()+'/xbrl_files/'
        if not os.path.exists(os.getcwd()+'/xbrl_files/'):
            os.makedirs(os.getcwd()+'/xbrl_files/')
        """
        if文を追加
        作業ディレクトリにofnameのJSONを格納するディレクトリがない場合は作成する
        """
        download_all_xbrl_files(dat_download,directory_path)

if __name__=='__main__'は、スクリプトファイルとして実行されたときに実行される文である(スクリプトとして実行されたとき、変数__name__に__main__が代入されるため)。

base_urlは有価証券報告書を取得するための外部APIのアドレスになっている。リクエストの発行やレスポンスの内容について詳しくはこちら↓
有報キャッチャー ウェブサービス AtomAPI

namespaceはXML名前空間URIになっている。これは外部APIのレスポンスに含まれるXML名前空間と合致している。XML名前空間についてはこちら↓
XML名前空間の簡単な説明

t_symbolsには証券コードを表す数字がタプルとして格納されている。このif関数では、t_symbolsに格納された要素ひとつひとつについてfor文以下のプログラムが実行される。
まずは変数response_stringにget_link_info_str()関数の返り値を格納する。これはt_symbolsの要素を含んだufocatchへのHTTPリクエストに対するレスポンスのボディ部分となる。
次に、変数ET_treeにxml.etree.ElementTreeモジュールのfromstring()関数の返り値を格納する。これはresponse_stringをXMLデータの文字列として引数にとり、Elementクラスのインスタンスを返す。
更に、register_namespace()関数で名前空間のprefixを登録する。register_namespace()関数は引数を二つ取り、第1引数はなし、第2引数はnamespaceとなる。
dat_downloadはdefaultdictクラスのインスタンスとなり、ここにダウンロード関連の情報をgetlink関数を使って保存する。

変数info_dictにget_link()関数の返り値を格納する。get_link()関数はElementクラスのインスタンスとnamespaceを引数にとり、辞書型のオブジェクトを返しinfo_dictに格納する。

dat_downloadのキー「t_symbol」の値としてinfo_dictを格納する。
ofnameはjsonファイルの名前を格納する。os.getcwd()関数は現在の作業ディレクトリを返すので、ofnameは以下のようになる。
現在の作業ディレクトリ/downloaded_info/dat_download_/証券コード.json

次に、write_download_info()関数でdat_downloadとofnameをJSON形式で直列化する。

最後に、downloaded_all_xbrl_files()関数でdat_downloadに格納されている情報を使ってXBRLファイルをダウンロードし、作業ディレクトリ直下のxbrl_filesディレクトリに保存する。
なお、xbrl_filesディレクトリがない場合は先に作る必要がある。

main関数でコメントを入れたif文については私の方で追加したが、最初の1回しか使われないのでわざわざif文を書かずに手動でディレクトリを作っても良いと思う(著者が追加しなかったのは何か理由があるにちがいない)。

XBRLデータの分析

本来の目的であったXBRLを使ったデータ分析だが、記事が長くなりそうなのでまた別の機会に書くことにする。
今回の記事で参考にしたQiitaの著者が良い記事を書かれているので、大いに参考にしようと思う。qiita.com

次世代EDINETについて

2013年9月より次世代EDINETが稼働している。
次世代EDINETの稼働開始(9月17日)について:金融庁
これにより、有価証券報告書の財務諸表以外の部分についてもXBRLデータによる公開がされ始めた。

次世代EDINETの非財務諸表部分についてはまた今度取り組んでみたい。

今回の反省

Pythonの勉強に時間をかけすぎてしまった。しかし、教科書的なコードではなく現実の問題に取り組むコードに触れられたことは大いに勉強になった。XMLの仕様とか参考にしたコードに使われていたライブラリとかは教科書には載っていなかったから。

プログラミングを勉強したい人は、教科書をさらっと1冊(もしくは公式ドキュメント)を読んで、自分がやりたいことをやりながら周辺の技術を習得していくのが良いと思う。

次は任意の年月までに提出された最新の有価証券報告書を取得する機能をこのプログラムに加えてみようと考えている。

最後に

初心者なのでとんちんかんなこと書いてるかもしれません。ごめんなさい。