前回の記事では、全種類の金利データをFRBのRSSから抜き出すことに成功した。しかし、試作プログラムであったため、コードが汚く、再利用性も低かった。
そのため、今回はプログラムのオブジェクト化を行い、コードの可読性と再利用性の向上させた。
コードの問題点
前回の記事のコードを見てみよう。このコードには、大きく2つの問題があると考えた。
import urllib.request as req
from bs4 import BeautifulSoup
def get_rate(data):
text=url_adjust(data)
url= "https://www.federalreserve.gov/feeds/Data/H15_H15_RIF"+ text + "_N.B.XML"
res=req.urlopen(url)
soup=BeautifulSoup(res,"lxml")
d=soup.item
return d.find("cb:value").string
def url_adjust(text):
if text=="FF":
adjText="SP"+text
elif text in {"M01" ,"M03","M06","Y01","Y02","Y03","Y05","Y10","Y20","Y30"}:
adjText="LGFC"+text
else:
sys.exit()
return adjText
Namelist=["FF","M01","M03","M06","Y01","Y02","Y03","Y05","Y10","Y20","Y30"]
rateList=[]
for Namelist in Namelist:
rateList.append(float(get_rate(Namelist)))
print(rateList)
1.日付がない
今後、データベース化していきたいと考えているが、日付データもほしいところ。
2.RSSのURL生成までの過程
いくらなんでもNamelist=["FF","M01","M03","M06","Y01","Y02","Y03","Y05","Y10","Y20","Y30"]
とリストを作って、別の関数で条件分岐をするのはまずいと思う。
というわけで、この順で問題を解決していこうと思う。
実際の作業
日付データの取得
まずは、日付データの取得だ。前回の記事で作ったget_rate()で、検索するタグを変えればどうにかなりそうだ。
いろいろ試した結果、"dc:date"でfindすれば日付データを抜き出せることがわかった。
import urllib.request as req
from bs4 import BeautifulSoup
def get_date():
url= "https://www.federalreserve.gov/feeds/Data/H15_H15_RIFLGFCY30_N.B.XML"
res=req.urlopen(url)
soup=BeautifulSoup(res,"lxml")
d=soup.item
return d.find("dc:date").string
str=get_date()
print(str)
出力:2018-06-07T12:00:00-05:00
ただ、このデータは"T12:00:00-05:00"の部分が無駄だ。日付の部分は固定長なので、11文字目までを抜き出せば日付データを取り出せそうだ。
import urllib.request as req
from bs4 import BeautifulSoup
#from time import sleep
def get_date():
url= "https://www.federalreserve.gov/feeds/Data/H15_H15_RIFLGFCY30_N.B.XML"
res=req.urlopen(url)
soup=BeautifulSoup(res,"lxml")
d=soup.item
return d.find("dc:date").string
str=get_date()
print(str)
str=str[0:10]
print(str)
出力:2018-06-07
これで日付データはOK。
プログラムのオブジェクト化
さて、プログラムのオブジェクト化を進めていく前に、目的となるデータにアクセスするまでの過程を整理してみる。
1.日付データ
a.対象となるURLを生成する。
b.urllibで対象urlを開く
c.beautifulsoupでXMLをパース
d."dc:date"をfind
e.0文字目から10文字目までを抜き出す
2.金利データ
a.対象となるURLを生成する。
b.urllibで対象urlを開く
c.beautifulsoupでXMLをパース
d."cb:value"をfind
並べて見ると、a.からc.までの手続きは共通している。また、dもfindすること自体は変わらない。つまり、引数を持ったクラスか関数を作成して、そこに"dc:date"か"cb:value"を入れれば処理を共通化できそうだ。そこで、aからdまでの手続きをクラスモジュールにし、それをmainでローカル関数にしていこうと思う。
クラスの作成
というわけで、まずは金利データと日付データの共通処理をクラスにしてしまう。今後他のところでも使えそうなので、"XMLProsesser"という適当な名前をつけておいた。
import urllib.request as req
from bs4 import BeautifulSoup
class XMLprocess:
def __init__(self,url):
self.res=req.urlopen(url)
self.soup=BeautifulSoup(self.res,"lxml")
self.data=self.soup.item
#加工してないデータを入手
def get_intactData(self,tag):
#tag:検索対象のタグ
return self.data.find(tag).string
#start文字目からend文字目まで切り取る。
def get_cutedData(self,tag,start,end):
#start:N文字目から切り取りたい場合、startにはN-1が入ることに注意。
#end:同様。M-1文字目まで切り取る
self.intactdata=self.get_intactData(tag)
self.cutedData=self.intactdata[start:end]
return self.cutedData
def get_floatData(self,tag):
#float型に直したデータを取得
self.intactdata=self.get_intactData(tag)
self.numericalData=float(self.intactdata)
return self.numericalData
url="https://www.federalreserve.gov/feeds/Data/H15_H15_RIFLGFCY30_N.B.XML"
XML=XMLprocess(url)
intactdate=XML.get_intactData("dc:date")
print(intactdate)
strdate=XML.get_cutedData("dc:date",0,10)
print(strdate)
rate=XML.get_floatData("cb:value")
print(rate)
XML=None
出力:
2018-06-07T12:00:00-05:00
2018-06-07
3.08
これでデータの抜き出しは終わった。
URL生成の共通化
次に、URL生成の処理を共通化していきたい。前回の記事で、次のような規則性があることがわかっている。
前半(全データ共通):federalreserve.gov/feeds/Data/H15_H15_RIF
中間部分:FF金利はSPFF、それ以外はLGFC+(残存期間の記号)
後半(全データ共通):_N.B.XML
というわけで、この組み合わせを次のようなJSONファイルにした。
{
"static":
{
"prefix":"https://www.federalreserve.gov/feeds/Data/H15_H15_RIF",
"suffix":"_N.B.XML"
},
"combination":
{
"FF":"SP",
"M01":"LGFC",
"M03":"LGFC",
"M06":"LGFC",
"Y01":"LGFC",
"Y02":"LGFC",
"Y03":"LGFC",
"Y05":"LGFC",
"Y10":"LGFC",
"Y20":"LGFC",
"Y30":"LGFC"
}
}
これを次のようなフォルダに保存しておく
source
|-main.py
|-target
|-FFRate.json
そして、URLを生成してリストにするクラスを作成した。なお、__を付けるとプライベート関数にできることはこのタイミングで知った模様。
import json
import os
#共通部分のインデックス
idx0="static"
#個別の組み合わせ部分
idx1="combination"
#前半の共通部分
prefix="prefix"
#後半の共通部分
suffix="suffix"
class scrape_url:
def __init__(self, filename):
#__変数で擬似ローカル変数になる
#ファイルのあるフォルダまでのパスを取得
__name = os.path.dirname(os.path.abspath(__name__))
#変換対象のjsonファイルまでのパスを作成
__path=__name+'/target/' + filename
#絶対パスに変換
__abspath=os.path.normpath(__path)
#jsonファイルを開く
__f = open(__abspath, 'r')
__jsonData=json.load(__f)
#urlの前半と後半の共通部分を取得
__prefix=__jsonData[idx0][prefix]
__suffix=__jsonData[idx0][suffix]
#個別項のインデックスを取得
__indexList = __jsonData[idx1].keys()
#dict_keyをリストに変換
__indexList=list(__indexList)
#戻り値となるリストを作成
self.urlList=[]
for __index in __indexList:
#インデックスごとの値を取得
__text=__jsonData[idx1][__index]
#FFRateのurlは"SP"+"FF"
__part=__text+__index
#urlを生成してリストに
self.urlList.append(__prefix+__part+__suffix)
__f.close()
def get_url(self):
return self.urlList
あとは、これを__init__.py
に入れてimportできるようにした。
from .XMLProcesser import *
from . import get_url
データのリストを取得
最後に、データをリストにするクラスを作成する。
from time import sleep
from modules import XMLprocess
from modules import get_url
class get_FFRate:
#アクセスマップの入ったファイルを指定
def __init__(self,filename):
#アクセスするurlのリストを取得
__scr=get_url.scrape_url(filename)
self.urlList=__scr.get_url()
#日付取得用の関数
def get_date(self):
__XML=XMLprocess(self.urlList[0])
__strdate=__XML.get_cutedData("dc:date",0,10)
__XML=None
return __strdate
def get_rate(self,url):
__XML=XMLprocess(url)
__rate=__XML.get_floatData("cb:value")
__XML=None
return __rate
#urlのリストに基づいてデータを取得していく
def get_list(self):
rateList=[]
#日付をリストに入れる(主キーのつもり)
rateList.append(self.get_date())
for __url in self.urlList:
__rate=self.get_rate(__url)
rateList.append(__rate)
#1秒間待つ
sleep(1)
__XML=None
return rateList
ff=get_FFRate("FFRate.json")
print(ff.get_list())
出力:['2018-06-07', 1.7, 1.78, 1.94, 2.12, 2.31, 2.5, 2.63, 2.77, 2.93, 3.0, 3.08]
これで目的は達成した。
感想その他
ここまで書いて気がついたけどこれ、絶対にURLを生成するクラスいらないでしょ・・・しかも、対象データがtartgetというフォルダに保存されていることが前提とか他で使う気全く無いでしょ、関数でいいじゃん。XMLかJSONでサイトマップ作ればよかった・・・正直初めてクラスを作って思った以上にうまく行ったから調子に乗っていたのは否定できない。次回以降に修正しよう
参考文献
クラスのこと全般:https://qiita.com/Usek/items/a206b8e49c02f756d636
python公式ドキュメントの9.6
JSONの扱い:https://qiita.com/Morio/items/7538a939cc441367070d