どこにでもいる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

出典

日本株の情報をスクレイピングで取得しmatplotlibで可視化する

f:id:predora005:20201123102206j:plain
*1

前回の続きです。前回は、複数銘柄の決算情報と財務情報をスクレイピングで取得しました。今回は前回取得した情報を可視化します。

predora005.hatenablog.com

f:id:predora005:20201205184457p:plain

[1] 基本的な指標の可視化

まずは前々回取得した以下の基本的な指標データを可視化してみます。

配当利回り PER(調整後) PSR PBR 出来高(千株) 時価総額(兆円)
JR東日本 2.53 12.39 0.83 0.77 1965.8 2.458450
JR東海 1.04 7.07 1.60 0.76 701.9 2.957130
JR西日本 3.59 10.85 0.64 0.79 1247.9 0.970065
東急 1.67 19.61 0.73 1.05 1104.6 0.856696
近鉄 1.05 43.77 0.75 2.22 387.7 0.902784
平均値 1.98 18.74 0.91 1.12 1081.6 1.629025
標準偏差 1.09 14.71 0.39 0.63 599.1 1.001244

[1-1] matplotlibをインストール

pip3 install matplotlib --user

[1-2] PERの可視化

まずは、PERを棒グラフで可視化します。

import matplotlib.pyplot as plt

# '標準偏差'の列を削除
df = df.drop(index='標準偏差')

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

# グラフ表示するデータを取得
per = df['PER(調整後)']  # PER
xpos = np.arange(len(per))  # X軸上の位置(0, 1, 2, ...)

# 棒グラフを作成
ax.bar(xpos, per)

# X軸に銘柄名を表示
ax.set(xticks=xpos, xticklabels=df.index)

# 補助線を描画
ax.grid(axis='y', color='gray', ls='--')

# 凡例を表示
ax.legend(['PER(調整後)'])

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

結果、次のようなグラフが得られます。

f:id:predora005:20201129161020p:plain

グラフ内の日本語は文字化けする場合があります。その場合は、日本語フォントのインストールが必要です(Amazon Linuxの場合は拙著参照)。

[1-3] PSRとPBRの可視化

次に、PSRとPBRを可視化します。

# 可視化する列
columns = ['PSR', 'PBR']

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

# データ数を取得
num_data = df.shape[0]      # 銘柄の数
num_column = len(columns)   # 可視化する列の数

# 棒グラフを横並びで表示するためのパラメータ
width = 0.8 / num_column    # 棒グラフの幅
xpos = np.arange(num_data)  # X軸上の位置

# 指定した列数分ループ
for i in range(num_column):
    
    col = columns[i]
    x = xpos + width * i
    y = df[col]
    
    # 棒グラフを表示
    ax.bar(x, y, width=width, align='center')
    
# X軸の目盛位置を調整し、銘柄名を表示
labels = df.index.values
offset = width / 2 * (num_column - 1)
ax.set(xticks=xpos+offset, xticklabels=labels)

# 補助線を描画
ax.grid(axis='y', color='gray', ls='--')

# 凡例を表示
ax.legend(columns)

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

結果、次のグラフが得られました。 棒グラフを横並びにするのには、少々手間がかかります。下記サイトを参考にさせていただきました。

【Python】matplotlibで色んな種類の棒グラフを表示する方法 | 侍エンジニアブログ

f:id:predora005:20201129161042p:plain

近鉄GHDのPBRが高いですね。PBR = 時価総額 / 株主資本ですから、B/S上の価値よりも市場に高く評価されていると言えます。

[2] 決算情報の可視化

続いて、前回得た決算情報・財務情報を可視化します。

ROA ROE
名称 決算期
JR東日本 2020年3月期 2.32 6.25
2019年3月期 3.53 9.54
JR東海 2020年3月期 4.14 10.28
2019年3月期 4.72 12.51

[2-1] ROEの可視化

まずは、ROEを折れ線グラフで可視化します。

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

# 銘柄の名称リスト
brand_names = list(df.index.unique('名称'))

# 可視化するデータの名称
data_name = 'ROE'

# 全銘柄のデータを折れ線グラフに表示
for brand_name in brand_names:
    
    brand_df = df.loc[(brand_name,)]  # 指定した銘柄のデータ
    x = brand_df.index  # 決算期
    y = brand_df[data_name]  # 可視化するデータ
    
    # 折れ線グラフ表示
    ax.plot(x, y, marker='o')

# 補助線を描画
ax.grid(axis='y', color='gray', ls='--')

# 軸ラベルをセット
plt.xlabel(data_name, size=15)

# 凡例を表示
ax.legend(brand_names)

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

結果、次のグラフが得られました。

f:id:predora005:20201129161221p:plain

[2-2] ROEROAの可視化

次に、ROEROAをセットで可視化して、1つの図に収めます。

# 可視化するデータ
data_names = ['ROE', 'ROA']

# Figurを取得
fig = plt.figure(figsize=(10, 4))

# 指定した全データをデータ別に折れ線グラフで表示する
for i, data_name in enumerate(data_names):
    
    # Axesを取得
    ax = fig.add_subplot(1, 2, i+1)
    
    # 銘柄の名称リスト
    brand_names = list(df.index.unique('名称'))
    
    # 全銘柄のデータを折れ線グラフに表示
    for brand_name in brand_names:
        
        brand_df = df.loc[(brand_name,)]    # 指定した銘柄のデータ
        x = brand_df.index                  # 決算期
        y = brand_df[data_name]             # 可視化するデータ
        
        # 折れ線グラフ表示
        ax.plot(x, y, marker='o')
    
    # 補助線を描画
    ax.grid(axis='y', color='gray', ls='--')
    
    # 軸ラベルをセット
    plt.xlabel(data_name, size=15)
    
    # 凡例を表示
    ax.legend(brand_names)

# 不要な余白を削る
plt.tight_layout()

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

結果、次のグラフが得られました。

f:id:predora005:20201129161356p:plain

JR東海は利益率が高いことで知られていますが、ROEROAが高いことからもそのことが窺えます。

また、2020年3月期はどの鉄道会社もROEROAが低下しています。これは、2020年2月頃からコロナウィルスの感染拡大があったことや、2019年10月の増税の影響によるものではないかと推測できます。

[2-3] 1銘柄の決算情報を可視化

詳細は割愛しますが、これまで紹介してきた内容を組み合わせると、次のようなグラフを作ることも可能です。

f:id:predora005:20201205184457p:plain

2018年度末(2019年3月期)までは、売上高・利益ともに伸びていましたが、2020年3月期は落ち込んでいます。

終わりに

今回は、スクレイピングで取得した情報を「matplotlib」で可視化しました。

今回行った可視化は複雑なものではありませんが、それでも可視化することによって得られる情報はあります。

株に関して言えば、個々の銘柄の情報は簡単に調べることが出来ますが、複数銘柄を比較できるサイトが少ないです。比較はできても、自分が比較したい情報は無いという場合もあります。

まずは、自力でExcelなどに手入力してみて、自動化したくなったら今回のようにするとよいのかもしれません。

*1:Lorenzo CafaroによるPixabayからの画像

日本株の決算情報と財務情報をpythonでスクレイピングを使って取得する

f:id:predora005:20201123102206j:plain
*1

前回の続きです。前回は複数銘柄のPERや配当利回り等をスクレイピングで取得しました。今回は決算情報をスクレイピングで取得します。

predora005.hatenablog.com

[1] 取得する情報

前回以前に引き続き「みんなの株式」から情報を取得させてもらいます。

[1-1] 決算情報と財務情報

今回取得するデータは期ごとの決算情報と財務情報です。以下は、JR東日本のものです。

f:id:predora005:20201123164919p:plain

[1-2] HTMLの中身

決算情報の表に着目すると、以下のHTMLになっていました。

<div class="md_box">
  <table class="data_table md_table is_fix" data-table="">
    <caption class="md_sub_index"><div class="title_box">決算情報</div></caption>
    <thead>
      <tr>
        <th class="w100p">決算期<p class="fsm">(決算発表日)</p></th>
        <th class="vamd">売上高</th>
        <th class="vamd">営業利益</th>
        <th class="vamd">経常利益</th>
        <th class="vamd">純利益</th>
        <th class="vamd">1株益</th>
      </tr>
    </thead>
    <tbody>
        <tr>
          <th class="vamd">2020年<span class="fwn fsm">3月期</span><p class="fsm">(2020/04/28)</p></th>
          <td class="num vamd">2,946,639</td>
          <td class="num vamd">380,841</td>
          <td class="num vamd">339,525</td>
          <td class="num vamd">198,428</td>
          <td class="num vamd">524.91</td>
        </tr>
        <tr>
          <th class="vamd">2019年<span class="fwn fsm">3月期</span><p class="fsm">(2019/04/25)</p></th>
          <td class="num vamd">3,002,043</td>
          <td class="num vamd">484,860</td>
          <td class="num vamd">443,267</td>
          <td class="num vamd">295,216</td>
          <td class="num vamd">773.26</td>
        </tr>
        ...(中略)...
    </tbody>
  </table>
</div>

[2] スクレイピング

[2-1] 複数の表から目当ての表を特定する

table要素は決算情報だけでなく、財務情報など他にも複数の表にありました。まずは、table要素の中から決算情報の表を特定します。

caption要素のタイトルが格納されているので、caption要素の文字列が何かで判断します。

code = 9020 # JR東日本の証券コード

# 指定URLのHTMLデータを取得
url = "https://minkabu.jp/stock/{0:d}/settlement".format(code)
html = requests.get(url)

# BeautifulSoupのHTMLパーサーを生成
soup = BeautifulSoup(html.content, "html.parser")

# 全<table>要素を抽出
table_all = soup.find_all('table')

# 決算情報の<table>要素を検索する。
fin_table1 = None
for table in table_all:
    
    # <caption>要素を取得
    caption = table.find('caption')
    if caption is None:
        continue
    
    # <caption>要素の文字列が目的のものと一致したら終了
    if caption.text == '決算情報':
        fin_table1 = table
        break

[2-2] table要素からデータを取得する

table要素の中は、thead要素とtbody要素に分かれています。

f:id:predora005:20201123194619p:plain

まずは、thead要素のデータを抽出します。

# <table>要素内のヘッダ情報を取得する。
headers = []
thead_th = fin_table1.find('thead').find_all('th')
for th in thead_th:
    headers.append(th.text)

次に、tbody要素のデータを抽出します。

# <table>要素内のデータを取得する。
rows = []
tbody_tr = fin_table1.find('tbody').find_all('tr')
for tr in tbody_tr:
    
    # 1行内のデータを格納するためのリスト
    row = []
    
    # <tr>要素内の<th>要素を取得する。
    th = tr.find('th')
    row.append(th.text)
    
    # <tr>要素内の<td>要素を取得する。
    td_all = tr.find_all('td')
    for td in td_all:
        row.append(td.text)
    
    # 1行のデータを格納したリストを、リストに格納
    rows.append(row)

[2-3] DataFrameに格納する

抽出したデータをDataFrameに格納します。DataFrame作成後、先頭の列である決算期をインデックスに指定します。

# DataFrameを生成する
df = pd.DataFrame(rows, columns=headers)

# 先頭の列(決算期)をインデックスに指定する
df = df.set_index(headers[0])

結果、以下のようなDataFrameが得られます。

売上高 営業利益 経常利益 純利益 1株益
決算期(決算発表日)
2020年3月期(2020/04/28) 2,946,639 380,841 339,525 198,428 524.91
2019年3月期(2019/04/25) 3,002,043 484,860 443,267 295,216 773.26
2018年3月期(2018/04/27) 2,950,156 481,295 439,969 288,957 749.20
2017年3月期(2017/04/28) 2,880,802 466,309 412,311 277,925 713.96

[3] データの加工

[3-1] 不要なデータを削る

前回と同様に、数値のカンマや単位を削っていきます。まずは、数値からカンマを削除します。

# 数値のカンマを削除する関数
def trim_camma(x):
    # 2,946,639.3のようなカンマ区切り、小数点有りの数値か否か確認する
    comma_re = re.search(r"(\d{1,3}(,\d{3})*(\.\d+){0,1})", x)
    if comma_re:
        value = comma_re.group(1)
        value = value.replace(',', '') # カンマを削除
        return np.float64(value) # 数値に変換
    
    return x

# 各列に対して、trim_cammaを適用する
new_df = df.copy()
for col in df.columns:
    new_df[col] = df[col].map(lambda v : trim_camma(v))

次に、決算期に付属している括弧の情報を削除します。削除する理由は、他の銘柄のデータを結合する際に不都合だからです。企業によって決算発表日は異なるため削除しておきます。

# 括弧内の文字列を削除する関数(括弧自体も削除する)
def remove_inparentheses(s):
    
    # 文字列末尾の括弧を削除する
    result = re.search(r"(.+)(\(.+\))", s)
    if result:
        str = result.group(1)
        return str
    
    return s

# インデックス(決算情報)の括弧内要素を削除する。
new_df.index.name = remove_inparentheses(new_df.index.name)
new_df.index = new_df.index.map(lambda s : remove_inparentheses(s))

[3-2] 複数銘柄のデータを結合する

JR東日本以外の鉄道関係銘柄についても、同様に決算情報を取得し、DataFrameに格納します。

# 上記の処理
...(中略)...

# 名称を追加し、MultiIndexにする。
df['名称'] = name
df = df.set_index('名称', append=True)

# 全銘柄データ格納用のDataFrameに1銘柄のDataFrameを追加する
if whole_df is None:
    whole_df = df
else:
    whole_df = whole_df.append(df)

各銘柄の名称と決算期単位でデータを扱えるようにMultiIndexにしておきます。MultiIndexにすると次のような構造になります。

売上高 営業利益 経常利益 ...
名称 決算期
JR東日本 2020年3月期 2,946,639 380,841 339,525 ...
2019年3月期 3,002,043 484,860 443,267 ...
JR東海 2020年3月期 1,844,647 656,163 574,282 ...
2019年3月期 1,878,137 709,775 632,653 ...

全銘柄のデータをDataFrameに追加した後、以下の処理を行うことで、名称と決算期でソートされたデータになります。

# indexを入れ替えてソートする。
whole_df = whole_df.swaplevel('名称', '決算期').sort_index()

[3-3] 財務情報も取得する

決算情報と同様に、財務情報も取得します。caption要素の文字列が'決算情報'から'財務情報'に変わっただけで、処理内容は同じです。

1株純資産 総資産 純資産 ...
名称 決算期
JR東日本 2020年3月期 8,396.81 8,537,059 3,173,427 ...
2019年3月期 8,187.64 8,359,676 3,094,378 ...
JR東海 2020年3月期 18,796.61 9,603,126 3,872,103 ...
2019年3月期 17,029.44 9,295,745 3,508,065 ...

[3-4] ROAROEを算出する

財務情報を使って、ROAROEを求めます。

  • ROA = 純利益 / 総資産
  • ROE = 純利益 / 純資産
# ROAとROEを求める
df['ROA'] = df['純利益'] / df['総資産'] * 100
df['ROE'] = df['純利益'] / df['純資産'] * 100

ROAROEの列が追加されます。

ROA ROE
名称 決算期
JR東日本 2020年3月期 2.32 6.25
2019年3月期 3.53 9.54
JR東海 2020年3月期 4.14 10.28
2019年3月期 4.72 12.51

終わりに

複数銘柄の決算情報を取得し、ROAROEを求めることができました。次回は取得したデータを可視化します。

predora005.hatenablog.com

*1:Lorenzo CafaroによるPixabayからの画像

日本株の基礎指標(PERや配当利回り)をpythonでスクレイピングを使って取得する (2)

f:id:predora005:20201123102206j:plain
*1

前回の続きです。前回は「みんなの株式」から、単一銘柄のPERや配当利回りスクレイピングで取得しました。今回は複数銘柄の情報を統合してみます。

predora005.hatenablog.com

[1] DataFrameにまとめる

pandasのDataFrameに複数銘柄の情報をまとめます。pandasはデータ分析用のライブラリです。DataFrameはpandasの基本構造で表形式のデータです。

[1-1] pandasのインストール

pip3 install numpy  --user
pip3 install pandas --user

[1-2] DataFrameの作成

鉄道関係 5銘柄の情報を取得し、DataFrameにまとめます。get_basic_infoは前回の内容を関数化したものです。

jre_dict = get_basic_info(9020) # JR東日本
jrc_dict = get_basic_info(9022) # JR東海
jrw_dict = get_basic_info(9021) # JR西日本
tokyu_dict = get_basic_info(9005) # 東急
kintetsu_dict = get_basic_info(9041) # 近鉄

jre_sr = pd.Series(jre_dict.values(), index=jre_dict.keys(), name='JR東日本')
jrc_sr = pd.Series(jrc_dict.values(), index=jrc_dict.keys(), name='JR東海')
jrw_sr = pd.Series(jrw_dict.values(), index=jrw_dict.keys(), name='JR西日本')
tokyu_sr = pd.Series(tokyu_dict.values(), index=tokyu_dict.keys(), name='東急')
kintetsu_sr = pd.Series(kintetsu_dict.values(), index=kintetsu_dict.keys(), name='近鉄')

df = pd.DataFrame([jre_sr, jrw_sr, jrc_sr, tokyu_sr, kintetsu_sr])

結果、以下のような内容にまとめることができました。

始値 高値 安値 配当利回り ...
JR東日本 6,510.0円 6,580.0円 6,479.0円 2.53% ...
JR東海 14,280.0円 14,450.0円 14,280.0円 1.04% ...
JR西日本 5,000.0円 5,078.0円 4,959.0円 3.59% ...
東急 1,372.0円 1,379.0円 1,360.0円 1.67% ...
近鉄 4,745.0円 4,770.0円 4,720.0円 1.05% ...

[2] 単位を削る

'円'や'%'などの単位が付与された状態ですが、平均値を出す場合など計算する際には邪魔になります。DataFrameから単位を削っていきます。

[2-1] 正規表現を使う

正規表現を使って、不要なものを削っていきます。

以下は株価(始値, 高値)の例です。カンマと円が不要なので削っていきます。株価は銘柄ごとに異なっており、5桁のものもあれば3桁のものもあるため、どれにも対応できるようにしています。

import re

yen = '1,234.5円'

# 正規表現で'円'より前の文字列を抽出
yen_re = re.search(r"([+-]?\d{1,3}(,\d{3})*\.\d+)円", yen)
yen = yen_re.group(1)

# カンマを削除
yen = value.replace(',', '')

# 実数に変換
yen = float(yen)

print(yen)
# 1234.5

[2-2] すべての列に適用する

[2-1]と同様の処理を、すべての列に適用します。DataFrameのmapメソッドを使い、列ごとに変換処理を行っていきます。

[2-2-1] 変換用の関数を用意する

まずは、以下のような変換関数を用意します。円, 株といった単位ごとに関数を用意してもよいのですが、面倒なので全単位に対応できる関数を用意しました。

# 単位を削除する関数
def trim_unit(x):
    
    # 単位=円を削除
    yen_re = re.search(r"(\d{1,3}(,\d{3})*\.\d+)円", x)
    if yen_re:
        value = yen_re.group(1)
        value = value.replace(',', '')
        return np.float64(value)
    
    # 単位=%を削除
    perc_re = re.search(r"(\d+\.\d+)%", x)
    if perc_re:
        value = perc_re.group(1)
        return np.float64(value)
    
    # 単位=株を削除
    stock_re = re.search(r"(\d{1,3}(,\d{3})*)株", x)
    if stock_re:
        value = stock_re.group(1)
        value = value.replace(',', '')
        return np.int64(value)
    
    # 単位=倍を削除
    times_re = re.search(r"(\d+\.\d+)倍", x)
    if times_re:
        value = times_re.group(1)
        return np.float64(value)
    
    # 単位=百万円を削除
    million_yen_re = re.search(r"(\d{1,3}(,\d{3})*)百万円", x)
    if million_yen_re:
        value = million_yen_re.group(1)
        value = value.replace(',', '')
        value = np.int64(value) * 1000000
        return value
    
    # 単位=千株を削除
    thousand_stock_re = re.search(r"(\d{1,3}(,\d{3})*)千株", x)
    if thousand_stock_re:
        value = thousand_stock_re.group(1)
        value = value.replace(',', '')
        value = np.int64(value) * 1000
        return value
    
    return x

[2-2-2] map関数で各列に変換処理を行う

pandasのmap関数を使用して、上に挙げたtrim_unitを各列に対して適用します。

# 各列に対して、trim_unitを適用する
new_df = df.copy()
for col in df.columns:
    new_df[col] = new_df[col].map(lambda v : trim_unit(v))

[3] 各列の統計量を算出する

[3-1] 平均値と標準偏差の算出

単位を削り数値に変換できたので、各列の平均値と標準偏差を求めます。

mean = new_df.mean() # 平均値
std = new_df.std() # 標準偏差

求めた平均値と標準偏差をDataFrameにまとめると、以下の結果が得られました。

statistics = pd.DataFrame(
    {'平均値': new_df.mean(), '標準偏差': new_df.std()})
平均値 標準偏差
始値 6381.4 4798.0
高値 6451.4 4858.9
安値 6359.6 4806.1
配当利回り 1.976 1.088
単元株数 100 0
PER(調整後) 18.738 14.714
PSR 0.91 0.39
PBR 1.118 0.628
出来高 1.081580e+06 5.990959e+05
時価総額 1.629025e+12 1.001244e+12
発行済株数 3.181594e+08 1.887879e+08

[3-2] 統計量と各銘柄データを結合

求めた統計量を、各銘柄のデータと結合して比較してみます。

ここで比較に役に立たないと思われるデータは削ります。また、出来高時価総額は桁数が多いので、見やすい桁数になるように単位を変換します。

# 各銘柄のデータと統計量を結合する。
new_df2 = new_df.append(statistics.T)

# 出来高と時価総額の単位を変換する。
new_df2['出来高'] = new_df2['出来高'] / 1.0e+3
new_df2['時価総額'] = new_df2['時価総額'] / 1.0e+12
new_df2 = new_df2.rename(columns=
            {'出来高': '出来高(千株)', '時価総額': '時価総額(兆円)'})

# 不要な列を削除する。
new_df2 = new_df2.drop(columns=
            ['始値', '高値', '安値', '単元株数', '発行済株数', '購入金額'])

以下だけで読み取れる情報は未だ少ないですが、近鉄のみPER, PBRが大きいと分かります。

配当利回り PER(調整後) PSR PBR 出来高(千株) 時価総額(兆円) 株主優待
JR東日本 2.53 12.39 0.83 0.77 1965.8 2.458450 株主優待割引券、自社施設サービス券、人間ドック割引券
JR東海 1.04 7.07 1.60 0.76 701.9 2.957130 株主優待乗車証
JR西日本 3.59 10.85 0.64 0.79 1247.9 0.970065 鉄道優待、グループ優待
東急 1.67 19.61 0.73 1.05 1104.6 0.856696 株主優待乗車証、自社グループ優待券
近鉄 1.05 43.77 0.75 2.22 387.7 0.902784 株主優待乗車証、自社施設割引券
平均値 1.98 18.74 0.91 1.12 1081.6 1.629025 NaN
標準偏差 1.09 14.71 0.39 0.63 599.1 1.001244 NaN

終わりに

今回は複数銘柄の基本指標を取得し、比較するところまで行いました。次回は、決算情報の取得を行います。

predora005.hatenablog.com

*1:Lorenzo CafaroによるPixabayからの画像

日本株の基礎指標(PERや配当利回り)をpythonでスクレイピングを使って取得する (1)

f:id:predora005:20201123102206j:plain
*1

本記事では、日本株の基礎指標をスクレイピングで取得する手順を紹介しています。また、スクレイピング先のサイトがスクレイピングを禁止していないか確認する方法にも触れています。

[1] きっかけ

今年はコロナウィルスやアメリカ大統領選挙の影響で、株価が大きく変動しましたね。 また、「人生100年時代」や「老後資金が2,000万円必要」とか言われたりしていますから、株式投資に興味を持ち始めた人も多いのではないかと思います。

株式投資をする際に何を基準にして売買するのかは人によって違います。「ドルコスト平均法で積立」「チャートを見る」「企業の業績を見る」など色々な方法があると思います。

私はこれまで、企業の業績を見る方法は取ってきませんでしたが、取り入れてみることにしました。ただ、どうせ取り入れるなら、面倒なところはプログラミングと絡めて自動化したいなと思いました。

しかし、下記サイトを参考にさせていただいたところ、APIを使ってお手軽に取得とはいかないようです。そこで、気象データ取得の際にも活用したスクレイピングで、企業情報を取得してみます。

別記事では、pandas-datareaderを用いたAPIでの株価取得、スクレイピングでの株価取得を紹介しています。よければ、そちらもご覧になってください。

predora005.hatenablog.com

predora005.hatenablog.com

[2] 時価総額やPERなどの指標を取得

みんなの株式」から情報を取得させてもらいます。

今回は、時価総額やPERなどの基本的な指標を取得します。JR東日本をはじめとした鉄道会社の情報を集めます。

f:id:predora005:20201122160155p:plain

[3] スクレピング可能か確認する

スクレイピングを禁止しているサイトも多くあります。「みんなの株式」がスクレピング可能か否かを確認します。

スクレイピング可能であったとしても、取得元のWebサイトに負荷をかけないように気をつけましょう。

[3-1] 規約を確認する

まずは規約を確認します。例えば、Yahoo!ファイナンスではスクレイピングを禁止しています。利用規約やFAQ、ヘルプを探してみましょう。

Yahoo!ファイナンスはヘルプに書かれています(2020/11/23時点)。

Yahoo!ファイナンスでは、Yahoo!ファイナンスに掲載している株価やその他のデータを、プログラム等を用いて機械的に取得する行為(スクレイピング等)について、システムに過度の負荷がかかり、安定したサービス提供に支障をきたす恐れがあることから禁止しています。

[3-2] robots.txtの確認

Googleなどの検索エンジンのクローリング(Webサイトの巡回)を最適化するためのファイルがrobots.txtです。robots.txtは各Webサイトで持っており、クローリング時にアクセスを許可 または 禁止するURLを定義しています。

robots.txt」が見つからない場合もありますが、見ることができればスクレイピング先へのアクセスが禁止されていないか確認できます。

詳細は下記サイトの説明を参考にさせてもらいました。

スクレイピング、クローリングする時の注意点 — Pythonオンライン学習サービス PyQ(パイキュー)ドキュメント

みんなの株式のrobots.txtを確認してみます。スクレイピングしたいのは「stock/」以下です。具体的には「stock/銘柄のコード/」になりますが、禁止されていないようです。

User-Agent: *

Allow: /
Sitemap: https://assets.minkabu.jp/concerns/sitemap/sitemap.xml.gz
...(中略)...

Disallow: /user/follow/
...(中略)...
Disallow: /stocks/pick/open_form
Disallow: /stock/*/community/edit
...(中略)...

[4] ライブラリをインストールする

pip3コマンドでRequestsとBeautifulSoupをインストールします。

pip3 install requests --user
pip3 install bs4 --user

また、取得したデータを加工しグラフ表示するために、pandasやmatplotlibもインストールします。

pip3 install numpy  --user
pip3 install pandas --user
pip3 install matplotlib --user

[5] HTMLの中身を確認する

ピンク枠で囲った部分(単元株数やPER)のHTMLを、ブラウザのデベロッパーツールで確認します。

f:id:predora005:20201122170010p:plain

以下のように、リスト要素(ul, li)を使っており、li要素の中にさらにリスト(dt, dd)を持っています。欲しい情報はdt, dd要素に格納されていることが分かりました。

<div class="ly_col ly_colsize_6_fix">
  <ul class=" md_list">
    <li class="ly_vamd">
      <dt class="ly_vamd_inner ly_colsize_3_fix wsnw">単元株数</dt><dd class="ly_vamd_inner ly_colsize_9_fix fwb tar wsnw">100株</dd>
    </li>
    <li class="ly_vamd">
      <dt class="ly_vamd_inner ly_colsize_3_fix wsnw">PER<span class="fss">(調整後)</span></dt><dd class="ly_vamd_inner ly_colsize_9_fix fwb tar wsnw">12.39倍</dd>
    </li>
    <li class="ly_vamd">
      <dt class="ly_vamd_inner ly_colsize_3_fix wsnw">PSR</dt><dd class="ly_vamd_inner ly_colsize_9_fix fwb tar wsnw">0.83倍</dd>
    </li>
    <li class="ly_vamd">
      <dt class="ly_vamd_inner ly_colsize_3_fix wsnw">PBR</dt><dd class="ly_vamd_inner ly_colsize_9_fix fwb tar wsnw">0.77倍</dd>
    </li>
  </ul>
</div>

[6] ソースコード

[6-1] HTML取得・解析開始

import requests
from bs4 import BeautifulSoup

# 指定URLのHTMLデータを取得
url = "https://minkabu.jp/stock/9020"
html = requests.get(url)

# BeautifulSoupのHTMLパーサーを生成
soup = BeautifulSoup(html.content, "html.parser")

[6-2] リスト要素の解析

ul要素のclass名"md_list"で抽出したいところですが、他の箇所でも使われていました。なので、全li要素を抽出して、li要素内のdd, dt要素を取り出していきます。

# データ格納用のディクショナリを準備
basic_info = {}

# 全<li>要素を抽出
li_all = soup.find_all('li')
for li in li_all:
    
    # <li>要素内の<dt>要素を抽出
    dt = li.find('dt')
    if dt is None:
        # <dt>要素がなければ処理不要
        continue
    
    # <li>要素内の<dd>要素を抽出
    dd = li.find('dd')
    
    # <dt><dd>要素から文字列を取得
    key = dt.text
    value = dd.text
    
    # ディクショナリに格納
    basic_info[key] = value

[6-3] 取り出した結果

以下の情報が取得できました。

print(basic_info)
#{'始値': '6,510.0円', 
# '高値': '6,580.0円', 
# '安値': '6,479.0円', 
# '配当利回り': '2.53%', 
# '単元株数': '100株', 
# 'PER(調整後)': '12.39倍', 
# 'PSR': '0.83倍', 
# 'PBR': '0.77倍', 
# '出来高': '1,965,800株', 
# '時価総額': '2,458,450百万円', 
# '発行済株数': '377,932千株', 
# '株主優待': '株主優待割引券、自社施設サービス券、人間ドック割引券', 
# '購入金額': '最安---'}

意図せず、広い範囲の情報が取れましたが、結果オーライです。

「購入金額」のみ情報を取ってこれていませんが、これはスクリプトによって表示する情報(HTMLの中身)を変えていることが理由です。

f:id:predora005:20201123090019p:plain

終わりに

今回は1銘柄の基本指標を取得するところまで行いました。次回は、複数銘柄の情報を取得し統合します。

predora005.hatenablog.com

*1:Lorenzo CafaroによるPixabayからの画像