jsmでYahooファイナンスのデータを片っ端から取得してMongoDBに保存する

はじめに

jsm(Japanese Stock Market)という Yahooファイナンスをクロールして株関連データを取得できるライブラリーがある。 Brand、Finance、Priceデータが取得できるので、東証一部に絞ってデータを片っ端から取得するコードを書いた。

インストール

pipを更新してからjsmをインストールする。 スクレイピングはBeautifulSoup4で行っているので(再)インストール

$ sudo pip install --upgrade pip
$ sudo pip install jsm
$ sudo pip install beautifulsoup4 -U

MongoDBのラッパー

save, read, deleteを用意したMongoDBのラッパーファイル(db.py)を作成する。 DB名とCollection名を初期化で指定する。 saveはdictのlistをdataとして渡す。

# -*- coding: utf-8 -*-
import sys
import pymongo

reload(sys)
sys.setdefaultencoding('utf-8')


class DB:
    def __init__(self, db_name, coll_name):
        self.db_name = db_name
        self.coll_name = coll_name


    def save(self, data):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        coll.insert(data)


    def read(self):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        data = [d for d in coll.find()]

        return data


    def delete(self):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        coll.drop()

前準備

クローラーファイル(clawer.py)を作成し、必要なライブラリーと初期設定を行う。 上記のdb.pyもインポートしておく。 stockデータベースに、Brand、Finance、Priceコレクションを作成して取得したデータを保存する。

# -*- coding: utf-8 -*-
import sys
import jsm
from progressbar import ProgressBar
import pandas as pd
import datetime

from db import DB

reload(sys)
sys.setdefaultencoding('utf-8')

DB_STOCK = 'stock'  # Stock DB
COLL_BRAND = 'brand'  # Brand Collection
COLL_FINANCE = 'finance'  # Finace Collection
COLL_PRICE = 'price'  # Price Collection
START_DATE = datetime.date(2014, 1, 1)  # 株価の取得開始日

Brandデータを取得

get_brand()で全銘柄が取得できるが、取得状況の進捗を見たいのでカテゴリーごとに取得する。

def get_brands(self):
    db = DB(DB_STOCK, COLL_BRAND)
    #db.delete()

    categories = [
        '0050',  # 農林・水産業
        '1050',  # 鉱業
        '2050',  # 建設業
        '3050',  # 食料品
        '3100',  # 繊維製品
        '3150',  # パルプ・紙
        '3200',  # 化学
        '3250',  # 医薬品
        '3300',  # 石油・石炭製品
        '3350',  # ゴム製品
        '3400',  # ガラス・土石製品
        '3450',  # 鉄鋼
        '3500',  # 非鉄金属
        '3550',  # 金属製品
        '3600',  # 機械
        '3650',  # 電気機器
        '3700',  # 輸送機器
        '3750',  # 精密機器
        '3800',  # その他製品
        '4050',  # 電気・ガス業
        '5050',  # 陸運業
        '5100',  # 海運業
        '5150',  # 空運業
        '5200',  # 倉庫・運輸関連業
        '5250',  # 情報・通信
        '6050',  # 卸売業
        '6100',  # 小売業
        '7050',  # 銀行業
        '7100',  # 証券業
        '7150',  # 保険業
        '7200',  # その他金融業
        '8050',  # 不動産業
        '9050'   # サービス業
    ]

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(categories)).start()
    for i in range(len(categories)):
        lis = []
        try:
            brands = q.get_brand(categories[i])
        except:
            pass
        for b in brands:
            dic = {
                    'category': categories[i],
                    'ccode': b.ccode,
                    'market': b.market,
                    'name': b.name,
                    'info': b.info
            }
            lis.append(dic)
        db.save(lis)
        pb.update(i+1)

Finaceデータを取得

Brandデータから東証一部の証券コード(ccode)のみを取得する。

def get_target_ccodes(self):
    data = DB(DB_STOCK, COLL_BRAND).read()
    df_brand = pd.DataFrame(data)
    df_brand = df_brand[df_brand['market']=='東証1部']
    ccodes = df_brand['ccode'].tolist()

    return ccodes

取得した東証一部の証券コードを引数にしてFianceデータを取得する。

def get_finances(self, ccodes):
    db = DB(DB_STOCK, COLL_FINANCE)
    #db.delete()

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(ccodes)).start()
    lis = []
    for i in range(len(ccodes)):
        try:
            f = q.get_finance(ccodes[i])
        except:
            pass
        dic = {
                'ccode': ccodes[i],
                'market_cap': f.market_cap,
                'shares_issued': f.shares_issued,
                'dividend_yield': f.dividend_yield,
                'dividend_one': f.dividend_one,
                'per': f.per,
                'pbr': f.pbr,
                'eps': f.eps,
                'bps': f.bps,
                'price_min': f.price_min,
                'round_lot': f.round_lot,
                'years_high': f.years_high,
                'years_low': f.years_low
        }
        lis.append(dic)
        pb.update(i+1)

    db.save(lis)

Priceデータを取得

Financeデータと同様、東証一部の証券コードを引数にしてPriceデータを取得する。

def get_prices(self, ccodes):
    start_date = START_DATE
    end_date = datetime.date.today()

    db = DB(DB_STOCK, COLL_PRICE)
    #db.delete()

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(ccodes)).start()
    for i in range(len(ccodes)):
        lis = []
        try:
            prices = q.get_historical_prices(ccodes[i], jsm.DAILY, start_date, end_date)
        except:
            pass
        for p in prices:
            dic = {
                    'ccode': ccodes[i],
                    'date': p.date,
                    'open': p.open,
                    'high': p.high,
                    'low': p.low,
                    'close': p.close,
                    'volume': p.volume
            }
            lis.append(dic)
        db.save(lis)
        pb.update(i+1)

crawler.pyの全ソース

db.pyと同じ階層にファイルを置いて$ python crawler.pyを実行すればクロールを開始する。

# -*- coding: utf-8 -*-
import sys
import jsm
from progressbar import ProgressBar
import pandas as pd
import datetime

from db import DB

reload(sys)
sys.setdefaultencoding('utf-8')

DB_STOCK = 'stock'
COLL_BRAND = 'brand'
COLL_PRICE = 'price'
COLL_FINANCE = 'finance'
START_DATE = datetime.date(2014, 1, 1)


class Crawler:
    """
    Refer to https://pypi.python.org/pypi/jsm/0.19
    """

    def __init__(self):
        pass


    def main(self):
        print 'getting brands...'
        self.get_brands()

        print 'getting finances...'
        ccodes = self.get_target_ccodes()
        self.get_finances(ccodes)

        print 'getting prices...'
        self.get_prices(ccodes)


    def get_brands(self):
        db = DB(DB_STOCK, COLL_BRAND)
        #db.delete()

        categories = [
            '0050',  # 農林・水産業
            '1050',  # 鉱業
            '2050',  # 建設業
            '3050',  # 食料品
            '3100',  # 繊維製品
            '3150',  # パルプ・紙
            '3200',  # 化学
            '3250',  # 医薬品
            '3300',  # 石油・石炭製品
            '3350',  # ゴム製品
            '3400',  # ガラス・土石製品
            '3450',  # 鉄鋼
            '3500',  # 非鉄金属
            '3550',  # 金属製品
            '3600',  # 機械
            '3650',  # 電気機器
            '3700',  # 輸送機器
            '3750',  # 精密機器
            '3800',  # その他製品
            '4050',  # 電気・ガス業
            '5050',  # 陸運業
            '5100',  # 海運業
            '5150',  # 空運業
            '5200',  # 倉庫・運輸関連業
            '5250',  # 情報・通信
            '6050',  # 卸売業
            '6100',  # 小売業
            '7050',  # 銀行業
            '7100',  # 証券業
            '7150',  # 保険業
            '7200',  # その他金融業
            '8050',  # 不動産業
            '9050'   # サービス業
        ]

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(categories)).start()
        for i in range(len(categories)):
            lis = []
            try:
                brands = q.get_brand(categories[i])
            except:
                pass
            for b in brands:
                dic = {
                        'category': categories[i],
                        'ccode': b.ccode, 
                        'market': b.market, 
                        'name': b.name, 
                        'info': b.info
                }
                lis.append(dic)
            db.save(lis)
            pb.update(i+1)


    def get_finances(self, ccodes):
        db = DB(DB_STOCK, COLL_FINANCE)
        #db.delete()

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(ccodes)).start()
        lis = []
        for i in range(len(ccodes)):
            try:
                f = q.get_finance(ccodes[i])
            except:
                pass
            dic = {
                    'ccode': ccodes[i],
                    'market_cap': f.market_cap,
                    'shares_issued': f.shares_issued,
                    'dividend_yield': f.dividend_yield,
                    'dividend_one': f.dividend_one,
                    'per': f.per,
                    'pbr': f.pbr,
                    'eps': f.eps,
                    'bps': f.bps,
                    'price_min': f.price_min,
                    'round_lot': f.round_lot,
                    'years_high': f.years_high,
                    'years_low': f.years_low
            }
            lis.append(dic)
            pb.update(i+1)
        
        db.save(lis)
        

    def get_prices(self, ccodes):
        start_date = START_DATE
        end_date = datetime.date.today()

        db = DB(DB_STOCK, COLL_PRICE)
        #db.delete()

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(ccodes)).start()
        for i in range(len(ccodes)):
            lis = []
            try:
                prices = q.get_historical_prices(ccodes[i], jsm.DAILY, start_date, end_date)
            except:
                pass
            for p in prices:
                dic = {
                        'ccode': ccodes[i],
                        'date': p.date, 
                        'open': p.open, 
                        'high': p.high, 
                        'low': p.low,
                        'close': p.close,
                        'volume': p.volume
                }
                lis.append(dic)
            db.save(lis)
            pb.update(i+1)


    def get_target_ccodes(self):
        data = DB(DB_STOCK, COLL_BRAND).read()
        df_brand = pd.DataFrame(data)
        df_brand = df_brand[df_brand['market']=='東証1部']
        ccodes = df_brand['ccode'].tolist()

        return ccodes


if __name__ == '__main__':
    Crawler().main()

所感

Priceデータを取得するのに15時間ぐらいかかったので、マルチスレッドにした方がいい。 上記のコードで継続的に最新のデータを収集し続けるには、もう少し改良が必要だけど、 とりあえず、サクッとデータを収集して分析したい人は使えると思う。 jsm自体は、 更新が2015年で止まっていて、GitHubからは削除されているが、ソース自体はPyPIに上がってるので、 これまで自作してた人は動かなくなっても改修できると思う。