動機
以前までこちらのサイトのデータを使用していたのですが、年末から更新が滞ってしまったため、スクレイピングが禁止されていない日本経済新聞の株のページから日足の株価を取得しようと思いました。
日本経済新聞からその日の日経平均株価を取得するような記事はよく見かけるのですが、上場企業の株価を取得するような記事はさらっと見た感じなかったので今回作ることにしました。
サイト構造
ページ
このページの構造を見ながら、スクレイピングする方法を考えます。企業は僕が楽天モバイルを使っているので、楽天にしました。特に深い意味はないです。
日足に関しては6ヶ月までがチャートから得られるようなので、6ヶ月をクリックします。チャート下のスマートチャートで見るにするともう少し長い期間で得られるかもしれませんが、自分的に必要がなかったので、6ヶ月にします。
カーソルをgifのように動かすことで、その日の始値、高値、安値、終値、25日移動平均、75日移動平均、売買高が取れるようです。移動平均に関しては自分好みの値で計算するつもりなので、始値、高値、安値、終値、売買高を抽出していきたいと思います。
このようにマウスの動きに対して、ページの表示が変わる場合には、Seleniumを使っていきます。こちらのサイトのような静的なページにはBeautifulSoupのみで良かったので、少し手間がかかります。
google chromeのデベロッパーツールを開き、⌘+Shift+c
で試しに日付の部分にカーソルを置きます。
どうやらここら辺から取れそうです。
もう一つ、Seleniumを使ってカーソルを動かす際に、座標が必要になります。下の写真のようにしてチャートの要素と横幅がわかる部分を探しました。
必要なものは揃いました。これを使って情報を抽出していきます。
URL
複数の企業を同時にスクレイピングしようとした場合、urlの構造も知っておいたほうが良いです。
日経新聞の株価のURL(楽天)は以下のようになっています。
https://www.nikkei.com/nkd/company/chart/?type=6month&scode=4755&ba=1
https://www.nikkei.com/nkd/company/chart/
以降のtype
にチャートのレンジ、scode
に企業のコードを指定すれば良さそうです。ba
に関してはよくわかりませんでした。
codeを得るための上場企業のリストに関しては、ここら辺から取得できます。
抽出プログラム
プログラムは以下のようになります。
pipでのドライバのインストール等はこちらを参考にしてください
from selenium import webdriver
import chromedriver_binary # Adds chromedriver binary to path
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
import re
import time
from datetime import datetime
from bs4 import BeautifulSoup
def extract_value(html):
"""
return [日付, 始値, 高値, 安値, 終値, 出来高(売買高)]
"""
soup = BeautifulSoup(html, "html.parser")
graph = soup.find("div", class_="highcharts-tooltip")
graph_td = graph.find_all("td")
values = [datetime.strptime(graph_td[0].text, '%Y/%m/%d').date().strftime('%Y-%m-%d')] # 日付格納
for v in graph_td[1:]:
if re.findall("始値|高値|安値|終値|売買高", v.text):
values.append(re.sub(r'\D', "", v.text))
return values
def scraping_stock_values(driver, url):
stock_values = []
driver.get(url)
# ページの表示に時間がかかり取得できない場合を想定
for _ in range(10):
try:
graph_xy = driver.find_elements_by_class_name("highcharts-grid")[1] # graphの情報を取得
except:
print("continue find elements")
continue
break
else:
print("サイト構造の変化の可能性")
raise
g_w = graph_xy.rect['width']
# 中心にいる→中心からグラフ幅の右半分移動(最新の株価)
actions = ActionChains(driver)
# NOTE: move_to_element_with_offsetだとうまくいかない
actions.move_to_element(graph_xy).perform()
actions.move_by_offset(g_w // 2, 0).perform()
html = driver.page_source.encode('utf-8')
stock_values.append(extract_value(html))
for _ in range(g_w-1):
actions = ActionChains(driver)
# 左に一つ移動
actions.move_by_offset(-1, 0).perform()
html = driver.page_source.encode('utf-8')
tmp_value = extract_value(html)
if tmp_value not in stock_values:
stock_values.append(extract_value(html))
return stock_values
if __name__ == "__main__":
type_ = "6month"
code = "4755"
url = f"https://www.nikkei.com/nkd/company/chart/?type={type_}&scode={code}"
options = Options()
# Headlessモードを有効にする(コメントアウトするとブラウザが実際に立ち上がります)
options.set_headless(True)
# ブラウザを起動する
driver = webdriver.Chrome(chrome_options=options)
start = time.time()
result = scraping_stock_values(driver, url)
print(f"scraping time:{time.time()-start}")
driver.close()
driver.quit()
結果(result)の抜粋
[['2020-02-21', '950', '997', '950', '987', '20693900'],
['2020-02-20', '950', '960', '944', '948', '10382000'],
['2020-02-19', '943', '949', '935', '942', '11191100'],
['2020-02-18', '927', '938', '916', '933', '11056200'],
['2020-02-17', '900', '933', '897', '930', '12856700'],
['2020-02-14', '876', '912', '876', '905', '16816300'],
...
['2019-09-13', '995', '1006', '989', '1000', '10201400'],
['2019-09-12', '1013', '1014', '982', '985', '12639000'],
['2019-09-11', '1026', '1033', '1007', '1014', '9178200'],
['2019-09-10', '1027', '1052', '1017', '1026', '11298600'],
['2019-09-09', '970', '1027', '958', '1025', '14368900'],
['2019-09-06', '1013', '1019', '966', '984', '28309900']]
うまくできてそうです。
プログラム解説
ドライバのパスやらの説明は省きます。
株価を取得する手順としては大まかに以下のようになります。
0. ページにアクセス
- チャートの要素を取得
- チャートに移動(中心座標)
- チャートの横幅の半分右に移動→値取得
- そこから左に1ずつ移動→値取得
- 4.をチャートの左端にくるまで繰り返す
1. チャートの要素を取得
# ページの表示に時間がかかり取得できない場合を想定
# ページの表示に時間がかかり取得できない場合を想定
for _ in range(10):
try:
graph_xy = driver.find_elements_by_class_name("highcharts-grid")[1] # graphの情報を取得
except:
print("continue find elements")
continue
break
else:
print("サイト構造の変化の可能性")
raise
チャートの座標を知るために下の要素を取得しています。
driver.find_elements_by_class_name("highcharts-grid")[1]
でclassがhighcharts-grid
のものを全て取得しています。[1]
としているのは、欲しいグリッドの情報が2番目のものだったためです。
elseに入った場合for文の処理を定数回試した後に入るので、サイト構造が変わった可能性があるので、抜け道を用意しました。
2. チャートに移動(中心座標)
g_w = graph_xy.rect['width']
# 中心にいる→中心からグラフ幅の右半分移動(最新の株価)
actions = ActionChains(driver)
# NOTE: move_to_element_with_offsetだとうまくいかない
actions.move_to_element(graph_xy).perform()
g_wはチャートの横幅(559)を取得しています。actions.move_to_element(graph_xy).perform()
で、指定したチャートの中心座標にマウスが移動します。
3. チャートの横幅の半分右に移動→値取得
actions.move_by_offset(g_w // 2, 0).perform()
html = driver.page_source.encode('utf-8')
stock_values.append(extract_value(html))
actions.move_by_offset(g_w // 2, 0).perform()
で中心から、チャートの半分の長さを右に移動します。以降、htmlを取得し、extract_value()で必要情報のみを取得しています。
def extract_value(html):
"""
return [日付, 始値, 高値, 安値, 終値, 出来高(売買高)]
"""
soup = BeautifulSoup(html, "html.parser")
graph = soup.find("div", class_="highcharts-tooltip")
graph_td = graph.find_all("td")
values = [datetime.strptime(graph_td[0].text, '%Y/%m/%d').date().strftime('%Y-%m-%d')] # 日付格納
for v in graph_td[1:]:
if re.findall("始値|高値|安値|終値|売買高", v.text):
values.append(re.sub(r'\D', "", v.text))
return values
graph_tdの中身は
[
<td width="16%">2020/2/21</td>,
<td width="16%">始値: 950</td>,
<td width="18%">高値: 997</td>,
<td width="18%">安値: 950</td>,
<td width="18%">終値: 987</td>,
<td width="14%"> </td>,
<td colspan="2"><span style="color:#5e7ab8;">━</span>25日移動平均: 895</td>,
<td colspan="2"><span style="color:#ce6170;">━</span>75日移動平均: 933</td>,
<td colspan="2"><span style="color:#b5c4cc;">■</span>売買高: 20,693,900</td>
]
これのtext部分を抽出して、文字列マッチングによって数字のみを取り出しています。日付の格納に関しては、以前取得していた日付の形式に合わせているだけですので気にしないでください。
4.そこから左に1ずつ移動→値取得
for _ in range(g_w-1):
actions = ActionChains(driver)
# 左に一つ移動
actions.move_by_offset(-1, 0).perform()
html = driver.page_source.encode('utf-8')
tmp_value = extract_value(html)
if tmp_value not in stock_values:
stock_values.append(extract_value(html))
for文に入る前の状態として、マウスが一番右にある状態なので、そこからチャートのwidth分、左にマウスを動かして、値を取得していきます。重複データがある場合、値をappendしないようにしています。
まとめ
- 日本経済新聞から各企業の日足データをスクレイピングしました。
- しかし、ブラウザを実際に操作するので遅いです。
- 上記プログラムだと
scraping time:251.08227729797363
- 上記プログラムだと
- 対策としてこんなのがあるそうです
- 高速化ができればまた記事を書きたいと思います。