読者です 読者をやめる 読者になる 読者になる

Pythonでコマンドラインオプションを使う

Pythonの練習としてXBRLをダウンロードするスクリプトを作成している。

前回は証券コードコマンドライン引数で渡したり、提出日をダウンロード条件に加えたりした。hqac.hatenadiary.com

今回は「対象期間をコマンドラインオプションから指定する」機能を追加する。

やりたいこと

ダウンロードの対象期間は下記の通り、スクリプト内で直接指定している。

if __name__=='__main__':
    base_url = 'http://resource.ufocatch.com/atom/edinetx/query/'
    namespace = '{http://www.w3.org/2005/Atom}'
    t_symbols = tuple(sys.argv)
    date_from = datetime.date(2014, 07, 01)
    date_to = datetime.date(2015, 06, 30)

これをコマンドラインオプションを用いて自由に変更できるようにしたい。

オプションは下記の構成とすることに決めた。

  • -f / --from: 対象期間の開始日付を指定する。フォーマットは"%Y-%m-%d"とする。つまり、2015年10月3日であれば"2015-10-3"。これはオプションなので、指定しなくてもよい。
  • -t / --to: 対象期間の終了日付を指定する。フォーマットは-fと同じ。

また、前回の記事でsysモジュールを使いコマンドライン引数を追加したが、これも合わせてコマンドラインオプションとしてまとめることにする。

  • -c / --codes: 対象の証券コードを指定する。フォーマットは4桁の数字。証券コードを指定しなければダウンロードできないので、指定は必須。

コマンドラインオプションの追加方法

Pythonスクリプトコマンドラインオプションを追加するためには、argparseモジュールを使う。公式ドキュメントを参照しながら使い方を勉強する(公式ドキュメントのコードを少し修正してコメントを加えた)。

# coding: utf-8

import argparse

parser = argparse.ArgumentParser(description="Process some integers")
"""
descriptionは引数のヘルプ(-hオプションで使用可能)の前に表示されるテキスト
このプログラムが何をするのかについて短い説明を書く
ArgumentParserオブジェクトには他にもたくさんの引数がある
description以外は使わなさそう
"""

parser.add_argument("integers",
		     metavar="N",
		     type=int,
		     nargs="+",
		     help="an integer for the accumulator"
		   )
"""
add_argumentメソッドはコマンドライン引数がどう解析されるかを定義する
ここで"integers"はparse.args()の属性名となる
つまり、parse.args().integersでコマンドライン引数から与えられたリストを取得できる
後述するように、この名前はdestキーワード引数によって決めることもできる
metavar
    使用法メッセージの中で使われる引数の名前
type
    コマンドライン引数が変換されるべき型
nargs
    消費するべきコマンドライン引数の数
    nargs=1であれば引数は1つ、nargs=2であれば引数は2つとなる
    このとき、コマンドライン引数はリストに格納される
    nargs="?"は1つの引数をコマンドラインから取得し、1つのアイテムを作る
    nargs="*"は全てのコマンドライン引数をリストに集める
    nargs="+"は全ての引数をリストに集めるが、1つも無い場合はエラーを返す
help
    引数が何なのかを示す簡潔な説明
"""

parser.add_argument("-s",
		    "--sum",
		    dest="accumulate",
		    action="store_const",
		    const=tuple,
		    default=max,
		    help="sum the integers (default: find the max)"
		   )
"""
dest
    parse.args()が返すオブジェクトに対する属性として値を追加する
    add_argument()の第一引数として渡すこともできる
action
    コマンドラインにこの引数があったときのアクション
    ここでは"store_const"を使い、constキーワード引数の値を格納する
    argparse.Actionを継承して独自のメソッドを実装することもできる
const
    いくつかのactionとnargsの組み合わせで利用される定数
    ここではsumを格納している
    最後のprint文でsum([list])によって引数から取得したリストの総和を返している
default
    コマンドラインに引数がなかった場合に生成される値
"""

args = parser.parse_args()
print(args.accumulate(args.integers))

nargsをnargs=1とするのとnargs="?"とするのでは、引数の数に違いはない(どちらも1つの引数を持つ)が、返り値の型が異なる。nargs=1では1つの要素(type=strであれば文字列)が格納されたリストが返り値となるが、nargs="?"では要素のみ(type=strであれば文字列)が返り値となる。argparseについてはここに書ききれない多くの機能があるので、詳しくは公式ドキュメントを参照いただきたい。

日付オプションを実装するにはargparseモジュールをimportし、parse_args関数を使って取得した日付(文字列)をdatetime型に変換すれば良さそうだ。また、証券コードについては複数指定を可能とするため、parse_args関数から取得した値をタプルにまとめておくことにする。完成版のコードは下記の通りだ。

# coding: utf-8

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 datetime
import argparse

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

def get_link(tree, namespace):
    yuho_dict = defaultdict(dict)
    for el in tree.findall('.//'+namespace+'entry'):
        title = el.find(namespace+'title').text
        date = el.find(namespace+'updated').text
        if not is_yuho(title, date): 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, 'date':date}
    return yuho_dict

def is_yuho(title, date):
    s_date = datetime.datetime.strptime(date[0:10],'%Y-%m-%d')
    u_date = datetime.datetime(s_date.year, s_date.month, s_date.day)
    if u'有価証券報告書' in unicode(title) and date_from <= u_date and u_date <= date_to:
        return True
    else:
        return False

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

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)

def _download_xbrl_file(url,_id,save_path):
    r = requests.get(url)
    if r.ok:
        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)

if __name__=='__main__':
    base_url = 'http://resource.ufocatch.com/atom/edinetx/query/'
    namespace = '{http://www.w3.org/2005/Atom}'

    parser = argparse.ArgumentParser(description="Receive stock codes, a start date, and an end date")
    parser.add_argument("-c",
                        "--codes",
                        nargs="*",
                        type=str,
                        required=True,
                        help=u"対象の証券コードを4桁の数字で指定してください。",
                        dest="stock_codes"
                        )
    parser.add_argument("-f",
                        "--from",
                        nargs="?",
                        default="2008-3-17",
                        type=str,
                        help=u"対象期間の開始日をY-m-dの形式で指定してください。 | 初期値: %(default)s",
                        dest="start_date"
                       )
    parser.add_argument("-t",
                        "--to",
                        nargs="?",
                        default="2112-9-3",
                        type=str,
                        help=u"対象期間の終了日をY-m-dの形式で指定してください。 | 初期値: %(default)s",
                        dest="end_date"
                       )

    t_symbols = tuple(parser.parse_args().stock_codes)
    date_from = datetime.datetime.strptime(parser.parse_args().start_date, "%Y-%m-%d")
    date_to = datetime.datetime.strptime(parser.parse_args().end_date, "%Y-%m-%d")

    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)
        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')

        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/')

        download_all_xbrl_files(dat_download,directory_path)

使い方

pythonスクリプトを起動するだけで使える。-cオプションで必ず証券コードを指定すること。下記の例では証券コード2432(株式会社ディー・エヌ・エー)を指定している。
f:id:hqac:20151003194006p:plain

cオプションを指定しない場合は、エラーになる。
f:id:hqac:20151003194109p:plain

f、tオプションを指定しない場合は、有報キャッチャーAPIで取得可能な全ての期間の有価証券報告書をダウンロードする。
f:id:hqac:20151003194221p:plain