どこにでもいる30代SEの学習ブログ

主にプログラミング関連の学習内容。読んだ本の感想や株式投資についても書いてます。

日本株の株価をpythonでスクレイピングを使って取得しmatplotlibで可視化する(単一銘柄)

過去数年分の株価をスクレイピングで取得して可視化する手順を紹介します。最終的に以下のようなグラフを作成します。

f:id:predora005:20201206150825p:plain

各銘柄の時価総額や決算情報を取得する手順や、ライブラリ(pandas-datareader)を用いて株価を取得する手順も別の記事にまとめています。

predora005.hatenablog.com

predora005.hatenablog.com

[1] 取得する情報

[1-1] 株式投資メモから取得

株価は株式投資メモから取得させてもらいます。

f:id:predora005:20201205194126j:plain

株式投資メモでは、各銘柄の株価が過去数十年分にわたって閲覧することができます。CSVファイルでダウンロードすることも可能です。

[1-2] URL

URLはhttps://kabuoji3.com/stock/(証券コード)/(年)/になっています。JR東日本(証券コード=9020)の2020年の株価であればhttps://kabuoji3.com/stock/9020/2020/です。

[1-3] HTMLの構成

株価はtable要素に格納されています。tbody要素がありますが、1つ目以外はタグの開始側がなく機能していないので無視して構わないです。

<table class="stock_table stock_data_table">
  <thead>
    <tr>
      <th>日付</th>
      <th>始値</th>
      <th>高値</th>
      <th>安値</th>
      <th>終値</th>
      <th>出来高</th>
      <th>終値調整</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2020-01-06</td>
      <td>9785</td>
      <td>9816</td>
      <td>9670</td>
      <td>9676</td>
      <td>1179000</td>
      <td>9676</td>
    </tr>
  </tbody>
    <tr>
      <td>2020-01-07</td>
      <td>9727</td>
      <td>9801</td>
      <td>9701</td>
      <td>9774</td>
      <td>900000</td>
      <td>9774</td>
    </tr>
  </tbody>
  〜〜〜(中略)〜〜〜
</table>

[2] 403 Forbiddenで上手くいかないので解決する

[2-1] ソースコードと事象

以下のソースコードを実行したところ「403 Forbidden」が返ってきてしまいました。アクセスが拒否されたということです。

code = 9020
year = 2020

# 指定URLのHTMLデータを取得
url = 'https://kabuoji3.com/stock/{0:d}/{1:d}/'.format(code, year)
html = requests.get(url)

print(html)
# <Response [403]>

print(html.content)
# b'<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
# <html><head>\n<title>403 Forbidden</title>
# </head><body>\n<h1>Forbidden</h1>
# <p>You don\'t have permission to access this resource.</p>
# </body></html>\n'

[2-2] 原因

非ブラウザのリクエストを禁止しているようです。スクレイピングを防ぐ意図で禁止しているのかと思いましたが、規約やrobots.txtを確認するとそうではないようです。

そこで今回は、ブラウザからのリクエストと認識されるようにしていきます。クライアントが何なのかは、HTTPヘッダの'User-Agent'に書かれます。なので、'User-Agent'の内容をブラウザからのリクエストに書き換えます。

なお、'User-Agent'については以下の記事に詳しい説明が書かれておりました。

UserAgentからOS/ブラウザなどの調べかたのまとめ - Qiita

[2-3] 解決法

確認くんにアクセスすると「現在のブラウザー」欄に使用しているブラウザの情報が表示されています。その内容をHTMLヘッダにセットします。

以下のソースではhtml_headersというディクショナリを用意し、keyを'User-Agent'にし、valueに確認くんで表示された内容をコピーしています。

# 指定URLのHTMLデータを取得
url = 'https://kabuoji3.com/stock/{0:d}/{1:d}/'.format(code, year)
html_headers ={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'}
html = requests.get(url, headers=html_headers)

[3] スクレイピング

[3-1] ソースコード

JR東日本(証券コード=9020)の2016年〜2020年の株価を取得してみます。

code = 9020        # 証券コード
start_year = 2016  # 開始年
end_year = 2020    # 終了年

df = None
headers = None

# 指定した年数分の株価を取得する
years = range(start_year, end_year+1)
for year in years:
    
    # 指定URLのHTMLデータを取得
    url = 'https://kabuoji3.com/stock/{0:d}/{1:d}/'.format(code, year)
    html_headers ={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'}
    html = requests.get(url, headers=html_headers)
    
    # BeautifulSoupのHTMLパーサーを生成
    bs = BeautifulSoup(html.content, "html.parser")
    
    # <table>要素を取得
    table = bs.find('table')
    
    # <table>要素内のヘッダ情報を取得する(初回のみ)。
    #  (日付, 始値, 高値, 安値, 終値, 出来高, 終値調整)
    if headers is None:
        headers = []
        thead_th = table.find('thead').find_all('th')
        for th in thead_th:
            headers.append(th.text)
    
    # <tr>要素のデータを取得する。
    rows = []
    tr_all = table.find_all('tr')
    for i, tr in enumerate(tr_all):
        
        # 最初の行は<thead>要素なので飛ばす
        if i==0:
            continue
        
        # <tr>要素内の<td>要素を取得する。
        row = []
        td_all = tr.find_all('td')
        for td in td_all:
            row.append(td.text)
        
        # 1行のデータをリストに追加
        rows.append(row)
        
    # DataFrameを生成する
    df_tmp = pd.DataFrame(rows, columns=headers)
    
    # DataFrameを結合する
    if df is None:
        df = df_tmp
    else:
        df = pd.concat([df, df_tmp])
    
    # 1秒ディレイ
    time.sleep(1)

print(df.head())
#            日付     始値     高値     安値     終値      出来高   終値調整
# 0  2016-01-04  11360  11410  11100  11125   748400  11125
# 1  2016-01-05  11100  11300  11035  11205   896700  11205
# 2  2016-01-06  11275  11400  11055  11135   699200  11135
# 3  2016-01-07  11255  11305  11040  11050  1095000  11050
# 4  2016-01-08  10950  11180  10855  10875  1100300  10875

[3-2] データ型を変換する

取得したデータの型を確認すると、すべて'object'型になっています。このままでは、グラフ表示や移動平均算出の際に都合が悪いです。

print(df.dtypes)
# 日付      object
# 始値      object
# 高値      object
# 安値      object
# 終値      object
# 出来高     object
# 終値調整    object
# dtype: object

日付はdatatime型、それ以外はfloat型に変換します。

# 列とデータ型の組み合わせを設定
dtypes = {}
for column in df.columns:
    if column == '日付':
        dtypes[column] = 'datetime64'
    else:
        dtypes[column] = 'float64'

# データ型を変換
df = df.astype(dtypes)

# インデックスを日付に変更
df = df.set_index('日付')

[4] 可視化

[4-1] 終値をグラフ表示

まずは、終値を単純に折れ線グラフにしてみます。

f:id:predora005:20201206150902p:plain

# FigureとAxesを取得
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

# 終値の折れ線グラフを追加
x = df['日付']
y = df['終値']
ax.plot(x, y, label='終値', linewidth=2.0)

# 目盛り線を表示
ax.grid(color='gray', linestyle='--', linewidth=0.5)

# 凡例を表示
ax.legend()

# グラフを表示
fig.show()

[4-2] 移動平均を算出

続いて、チャートでよくみる移動平均を算出します。

df['5日移動平均'] = df['終値'].rolling(window=5).mean()
df['25日移動平均'] = df['終値'].rolling(window=25).mean()
df['75日移動平均'] = df['終値'].rolling(window=75).mean()

[4-3] 移動平均をグラフ表示

移動平均をグラフ表示するにあたり、2020/01/01以降のデータに絞ります。

# 2020/01/01以降のデータを抽出
df = df['2020-01-01':]

グラフ表示してみると以下のようになりました。以下はJR東日本の株価です。コロナウィルスの影響もあり、長期で見ると株価は下がり続けています。

f:id:predora005:20201206150825p:plain

# FigureとAxesを取得
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

# 終値の折れ線グラフを追加
x = df['日付']
y = df['終値']
ax.plot(x, y, label='終値', linewidth=2.0)

# 移動平均の折れ線グラフを追加
average_columns = ['5日移動平均', '25日移動平均', '75日移動平均']
for column in average_columns:
    x = df['日付']
    y = df[column]
    ax.plot(x, y, label=column, linewidth=1.0)

# 目盛り線を表示
ax.grid(color='gray', linestyle='--', linewidth=0.5)

# 凡例を表示
ax.legend()

# グラフを表示
fig.show()

終わりに

株価をスクレイピングで取得してグラフ表示することができました。

しかし、単一銘柄の株価を見るだけであれば、色々なところで簡単に見れるので、自分でわざわざ作る価値はそれほどないでしょう。

次回は複数銘柄の比較と可視化を行い、作る価値のあるデータを作っていきます。

predora005.hatenablog.com

出典