Yahooファイナンスのスクレイピングを行います。
今回は、過去の株価を取得します。
いわゆる時系列データというモノです。
 時系列データがあれば、チャートも描くことができます。
本記事の内容
- ここまでの流れ【Yahooファイナンスのスクレイピング】
- 時系列ページのスクレイピング仕様
- 時系列ページから株価時系列データを抽出
それでは、上記に沿って解説していきます。
ここまでの流れ【Yahooファイナンスのスクレイピング】
Yahooファイナンスのスクレイピングに関しては、段階を踏んで解説しています。
今回は、時系列ページのスクレイピングがメインです。
下記で過去の同シリーズを記載します。
第1弾
第2弾
第3弾
第1弾の記事は、スクレイピングをする上では必読の内容です。
その内容を理解していないと、犯罪者になってしまうかもしれません。
第2弾の記事は、銘柄コードのリストの作成方法を解説しています。
 全部で3849件の銘柄リストを取得できました。
このようなリストを作成するのが、スクレイピングの基本中の基本です。
そして、このリストをもとに取得したいデータのページにアクセスが可能となります。
Yahooファイナンスなら、そのためのベースが銘柄コードなのです。
第3弾の記事では、銘柄コードをもとに企業情報ページへアクセスしています。
そして、企業情報ページから企業情報をスクレイピングしています。
第4弾となる今回は、時系列ページから情報を抽出していきます。
 では、時系列ページからデータを抽出するための仕様を確認していきます。
時系列ページのスクレイピング仕様
まずは、時系列ページの確認からです。

上記を見ればわかるように、時系列データには次の二つが存在します。
- 株価時系列データ
- 信用残時系列データ
本記事では、株価時系列データを対象とします。
おそらく、時系列データと言えば、株価時系列データという認識の人がほとんどでしょう。
信用残データも株価との相関関係があるはずです。
その意味では、取得する価値はあるのかもしれません。
もちろん、他にも用途はあるでしょう。
しかし、今回は株価時系列データをターゲットにします。
 株価時系列データをターゲットにする場合、考えるべきポイントは以下。
- 株価時系列データページのURL作成
- データ表示条件の決定
- 改ページ対応
- 株価時系列データの抽出
それぞれを下記で説明します。
株価時系列データページのURL作成
株価時系列データページのURLについて説明しておきます。
銘柄コードのリストから、自動的に各銘柄ごとの株価時系列データページのURLを作成します。
そのときの形式は、以下となります。
「https://stocks.finance.yahoo.co.jp/stocks/history/?code=●」
●は、銘柄コードです。
上記形式のURLで株価時系列データページにアクセス可能となります。
例えば、株式会社 極洋(1301)の場合は次のURLです。
https://stocks.finance.yahoo.co.jp/stocks/history/?code=1301
ただ、上記URLは利用しません。
 次の「データ表示条件の決定」でその理由がわかります。
 結論は、「まとめ」で説明します。
データ表示条件の決定
表示条件とは、以下のことです。

デフォルトだと、デイリーで1ヵ月がデータ表示の条件となっています。
 この条件で「表示」ボタンをクリックすると、次のURLページへ遷移します。
「https://info.finance.yahoo.co.jp/history/?code=1301.T&sy=2021&sm=1&sd=14&ey=2021&em=2&ed=13&tm=d」
上記URLは、以下の2点で注目ポイントがあります。
- ドメイン
- クエリパラメータ
下記で説明します。
ドメイン
「株価時系列データページのURL作成」で確認したURLは、以下。
https://stocks.finance.yahoo.co.jp/stocks/history/?code=1301
「表示」ボタンをクリックして遷移した先のURLは、以下。
https://info.finance.yahoo.co.jp/history/?code=1301.T&sy=2021&sm=1&sd=14&ey=2021&em=2&ed=13&tm=d
それぞれのドメインを抽出すると以下となります。
- stocks.finance.yahoo.co.jp
- info.finance.yahoo.co.jp
正直、これには自分の目を疑いました。
でも、この事実を受け入れていきましょう。
あと、何気にドメイン以降のパスも異なります。
- /stocks/history/
- /history/
せめて、ここは同じにしましょうよ・・・
 まあ、スクレイピング対策になっていると言えば、なっていますけどね。
クエリパラメータ
クエリパラメータは、以下。
| パラメータ | 値 | 
| sy | 2021 | 
| sm | 1 | 
| sd | 14 | 
| ey | 2021 | 
| em | 2 | 
| ed | 13 | 
| tm | d | 
説明するまでもありませんね。
ただ、tmだけは確認しておきましょう。
tmには、以下の値が設定可能です。
| d | デイリー | 
| w | 週刊 | 
| m | 月間 | 
わかりやすいですね。
 その意味では、Yahooファイナンスはスクレイピングが容易と言えます。
改ページ対応
データ表示条件を2020年の1年間でデイリーとした場合、件数部分が次のように表示されます。

株価時系列データページでは、1ページに20件しか表示しません。
そのため、改ページへの対応が必要となります。
対応方法は、二つあります。
- クエリパラメータに「&p=●」を付ける
- 「次へ」リンクをクリックする
※●はページ数(1,2,3・・・)
Yahooファイナンスでは、両方の対応を取ることができます。
今回は、「次へ」リンクをクリックする対応を採用します。
クエリパラメータの方式は、対応できないサイトも存在します。
 汎用性を考えたら、「次へ」リンクをクリックする方式がベターです。
 それにSeleniumを利用している以上は、できる限り利用して身に付けていきましょう。
株価時系列データの抽出
各データ行のhtmlタグは、以下。

class名指定ではスクレイピングはできませんね。
 各tr毎に存在するtd要素の順番により、データ項目を決定できそうです。
| 順番(0スタート) | データ項目 | 
| 0 | 日付 | 
| 1 | 始値 | 
| 2 | 高値 | 
| 3 | 安値 | 
| 4 | 終値 | 
| 5 | 出来高 | 
| 6 | 調整後終値* | 
念のため、「class=boardFin」のtable要素以下で対象となるtr・tdを絞り込みます。
まとめ
改ページ処理は、「次へ」リンクをクリックしていく形式を採用します。
そのため、スクレイピングする上でアクセスするURLは最初の一つだけです。
銘柄コード1301の2020年におけるデイリーの株価時系列データを得る場合のURLは、以下。
https://info.finance.yahoo.co.jp/history/?code=1301&sy=2021&sm=1&sd=14&ey=2021&em=2&ed=13&tm=d
上記URLにアクセスして、あとは「次へ」リンクをクリックしていくことになります。
もちろん、各ページ最大20件の株価時系列データを抽出しながらです。
以上より、スクレイピングの仕様が決まりました。
 次は、実際に株価時系列データをスクレイピングしていきましょう。
時系列ページから株価時系列データを抽出
時系列ページから株価時系列データを抽出するコードは、以下。
 現時点(2021年2月13日)では元気モリモリ動いています。
サンプルコード
import sys
import bs4
import traceback
import re
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
# ドライバーのフルパス
CHROMEDRIVER = "chromedriver.exeのパス"
# 改ページ(最大)
PAGE_MAX = 2
# 遷移間隔(秒)
INTERVAL_TIME = 3
# 開始年・月・日
SY = 2020
SM = 1
SD = 1
# 終了年・月・日
EY = 2020
EM = 12
ED = 31
# タイプ(d:デイリー、w:週刊、m:月間)
TM = "d"
# ドライバー準備
def get_driver():
    # ヘッドレスモードでブラウザを起動
    options = Options()
    options.add_argument('--headless')
    # ブラウザーを起動
    driver = webdriver.Chrome(CHROMEDRIVER, options=options)
    return driver
# 対象ページのソース取得
def get_source_from_page(driver, page):
    try:
        # ターゲット
        driver.get(page)
        driver.implicitly_wait(10)  # 見つからないときは、10秒まで待つ
        page_source = driver.page_source
        return page_source
    except Exception as e:
        print("Exception\n" + traceback.format_exc())
        return None
# ソースからスクレイピングする
def get_data_from_source(src):
    # スクレイピングする
    soup = bs4.BeautifulSoup(src, features='lxml')
    # print(soup)
    try:
        info = []
        table = soup.find("table", class_="boardFin")
        if table:
            elems = table.find_all("tr")
            for elem in elems:
                td_tags = elem.find_all("td")
                if len(td_tags) > 0:
                    row_info = []
                    tmp_counter = 0
                    for td_tag in td_tags:
                        tmp_text = td_tag.text
                        if tmp_counter == 0:
                            # 年月日
                            tmp_text = tmp_text
                        else:
                            tmp_text = extract_num(tmp_text)
                        row_info.append(tmp_text)
                        tmp_counter = tmp_counter + 1
                    info.append(row_info)
        return info
    except Exception as e:
        print("Exception\n" + traceback.format_exc())
        return None
# 次のページへ遷移
def next_btn_click(driver):
    try:
        # 次へボタン
        elem_btn = WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.LINK_TEXT, '次へ'))
        )
        # クリック処理
        actions = ActionChains(driver)
        actions.move_to_element(elem_btn)
        actions.click(elem_btn)
        actions.perform()
        # 間隔を設ける(秒単位)
        time.sleep(INTERVAL_TIME)
        return True
    except Exception as e:
        print("Exception\n" + traceback.format_exc())
        return False
# 数値だけ抽出
def extract_num(val):
    num = None
    if val:
        match = re.findall("\d+\.\d+", val)
        if len(match) > 0:
            num = match[0]
        else:
            num = re.sub("\\D", "", val)
    if not num:
        num = 0
    return num
if __name__ == "__main__":
    # 引数
    args = sys.argv
    # 銘柄コード
    code = "1301"
    if len(args) == 2:
        # 引数があれば、それを使う
        code = args[1]
    # 対象ページURL
    page = "https://info.finance.yahoo.co.jp/history/?code=" + code
    page = page + "&sy=" + str(SY) + "&sm=" + str(SM) + "&sd=" + str(SD)
    page = page + "&ey=" + str(EY) + "&em=" + str(EM) + "&ed=" + str(ED)
    page = page + "&tm=" + TM
    # ブラウザのdriver取得
    driver = get_driver()
    # ページのソース取得
    source = get_source_from_page(driver, page)
    result_flg = True
    # ページカウンター制御
    page_counter = 0
    while result_flg:
        page_counter = page_counter + 1
        # ソースからデータ抽出
        data = get_data_from_source(source)
        # データ保存
        print(data)
        # 改ページ処理を抜ける
        if page_counter == PAGE_MAX:
            break
        # 改ページ処理
        result_flg = next_btn_click(driver)
        source = driver.page_source
    # 閉じる
    driver.quit()
プログラム詳細は、「時系列ページのスクレイピング仕様」とコード上のコメントをご覧ください。
不明な点がある場合は、同シリーズの過去記事を確認してください。
ただ、次の「対象ページURL」に関しては説明しておきます。
    page = "https://info.finance.yahoo.co.jp/history/?code=" + code
    page = page + "&sy=" + str(SY) + "&sm=" + str(SM) + "&sd=" + str(SD)
    page = page + "&ey=" + str(EY) + "&em=" + str(EM) + "&ed=" + str(ED)
    page = page + "&tm=" + TM
クエリパラメータを指定したURLを作成しています。
 そして、ここで用いられる定数は以下の部分です。
# 開始年・月・日 SY = 2020 SM = 1 SD = 1 # 終了年・月・日 EY = 2020 EM = 12 ED = 31 # タイプ(d:デイリー、w:週刊、m:月間) TM = "d"
特に問題はありませんね。
 ここの値を変更すれば、取得したい条件でスクレイピングが可能です。
実行結果
サンプルコードを実行した結果は、以下。
[['2020年12月30日', '2947', '2960', '2923', '2951', '11100', '2951'], ['2020年12月29日', '2948', '2963', '2945', '2961', '15100', '2961'], ['2020年12月28日', '2929', '2950', '2910', '2950', '22900', '2950'], ['2020年12月25日', '2903', '2930', '2903', '2916', '8300', '2916'], ['2020年12月24日', '2913', '2937', '2909', '2917', '13900', '2917'], ['2020年12月23日', '2913', '2920', '2906', '2913', '6300', '2913'], ['2020年12月22日', '2947', '2947', '2907', '2913', '18000', '2913'], ['2020年12月21日', '2939', '2947', '2912', '2947', '11300', '2947'], ['2020年12月18日', '2935', '2938', '2907', '2937', '15800', '2937'], ['2020年12月17日', '2872', '2920', '2870', '2920', '18900', '2920'], ['2020年12月16日', '2882', '2882', '2871', '2871', '8900', '2871'], ['2020年12月15日', '2880', '2897', '2874', '2881', '12900', '2881'], ['2020年12月14日', '2830', '2888', '2830', '2888', '31200', '2888'], ['2020年12月11日', '2810', '2846', '2810', '2841', '18400', '2841'], ['2020年12月10日', '2812', '2828', '2806', '2815', '14900', '2815'], ['2020年12月9日', '2816', '2827', '2807', '2820', '6600', '2820'], ['2020年12月8日', '2807', '2824', '2805', '2816', '10400', '2816'], ['2020年12月7日', '2820', '2830', '2806', '2811', '12900', '2811'], ['2020年12月4日', '2819', '2819', '2800', '2811', '9200', '2811'], ['2020年12月3日', '2810', '2816', '2796', '2807', '9400', '2807']][['2020年12月2日', '2806', '2817', '2789', '2811', '25700', '2811'], ['2020年12月1日', '2824', '2824', '2780', '2787', '17900', '2787'], ['2020年11月30日', '2816', '2825', '2795', '2795', '18900', '2795'], ['2020年11月27日', '2807', '2825', '2803', '2819', '22000', '2819'], ['2020年11月26日', '2820', '2826', '2801', '2809', '8700', '2809'], ['2020年11月25日', '2839', '2846', '2795', '2831', '27800', '2831'], ['2020年11月24日', '2850', '2860', '2806', '2807', '20100', '2807'], ['2020年11月20日', '2848', '2848', '2820', '2832', '10200', '2832'], ['2020年11月19日', '2834', '2846', '2812', '2838', '11600', '2838'], ['2020年11月18日', '2813', '2838', '2790', '2838', '14400', '2838'], ['2020年11月17日', '2800', '2813', '2785', '2813', '13100', '2813'], ['2020年11月16日', '2820', '2820', '2788', '2804', '15800', '2804'], ['2020年11月13日', '2821', '2821', '2771', '2784', '10500', '2784'], ['2020年11月12日', '2807', '2839', '2805', '2814', '13900', '2814'], ['2020年11月11日', '2810', '2850', '2796', '2850', '27300', '2850'], ['2020年11月10日', '2795', '2819', '2772', '2793', '27000', '2793'], ['2020年11月9日', '2800', '2800', '2757', '2795', '15800', '2795'], ['2020年11月6日', '2771', '2786', '2671', '2782', '30900', '2782'], ['2020年11月5日', '2745', '2790', '2671', '2671', '31400', '2671'], ['2020年11月4日', '2763', '2768', '2744', '2746', '12000', '2746']]
最初の2ページ分の40件だけです。
 それは、以下のように「2」と設定しているからです。
# 改ページ(最大) PAGE_MAX = 2
適当にここを「99999」などにすれば、全ページ分をスクレイピングします。
 もしくは、PAGE_MAXの制御を無効にするかです。
まとめ
以下は、本ブログにおけるスクレイピングでは当たり前の定数です。
# ドライバーのフルパス CHROMEDRIVER = "chromedriver.exeのパス" # 改ページ(最大) PAGE_MAX = 2 # 遷移間隔(秒) INTERVAL_TIME = 3
この定数に関して、わからないところがある場合は過去記事をご覧ください。
 メルカリのスクレイピングシリーズが、特に参考になります。
メルカリのスクレイピング第1弾
メルカリのスクレイピング第2弾
メルカリのスクレイピング第3弾
 
  
  
  
  






