【Selenium】半年分の日足チャートを日本経済新聞からスクレイピングした

# 動機
以前まで[こちらのサイト](https://kabuoji3.com/)のデータを使用していたのですが、年末から更新が滞ってしまったため、スクレイピングが禁止されていない日本経済新聞の株のページから日足の株価を取得しようと思いました。
日本経済新聞からその日の日経平均株価を取得するような記事はよく見かけるのですが、上場企業の株価を取得するような記事はさらっと見た感じなかったので今回作ることにしました。

# サイト構造
## ページ
このページの構造を見ながら、スクレイピングする方法を考えます。企業は僕が楽天モバイルを使っているので、楽天にしました。特に深い意味はないです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187535/9dbf1d2c-98bb-b981-f4d7-ac2d2c549aba.png)

日足に関しては6ヶ月までがチャートから得られるようなので、6ヶ月をクリックします。チャート下のスマートチャートで見るにするともう少し長い期間で得られるかもしれませんが、自分的に必要がなかったので、6ヶ月にします。

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187535/5a7d7de0-5d52-3ebf-6d21-921018504069.gif)

カーソルをgifのように動かすことで、その日の始値、高値、安値、終値、25日移動平均、75日移動平均、売買高が取れるようです。移動平均に関しては自分好みの値で計算するつもりなので、始値、高値、安値、終値、売買高を抽出していきたいと思います。
このようにマウスの動きに対して、ページの表示が変わる場合には、**Selenium**を使っていきます。[こちらのサイト](https://kabuoji3.com/)のような静的なページにはBeautifulSoupのみで良かったので、少し手間がかかります。

google chromeのデベロッパーツールを開き、`⌘+Shift+c`で試しに日付の部分にカーソルを置きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187535/e8c25b06-3a91-b95e-3f10-3e0b5fd1ff30.png)

どうやらここら辺から取れそうです。
もう一つ、Seleniumを使ってカーソルを動かす際に、座標が必要になります。下の写真のようにしてチャートの要素と横幅がわかる部分を探しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187535/a31bb0dc-5ee6-cb70-8a64-be98d2471b0c.png)

必要なものは揃いました。これを使って情報を抽出していきます。


## 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を得るための上場企業のリストに関しては、[ここら辺](https://www.jpx.co.jp/markets/statistics-equities/misc/)から取得できます。

# 抽出プログラム
プログラムは以下のようになります。
pipでのドライバのインストール等は[こちら](https://qiita.com/kinpira/items/383b0fbee6bf229ea03d)を参考にしてください

```python
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. チャートの要素を取得
2. チャートに移動(中心座標)
3. チャートの横幅の半分右に移動→値取得
4. そこから左に1ずつ移動→値取得
5. 4.をチャートの左端にくるまで繰り返す

### 1. チャートの要素を取得

```python
# ページの表示に時間がかかり取得できない場合を想定
    # ページの表示に時間がかかり取得できない場合を想定
    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文の処理を定数回試した後に入るので、サイト構造が変わった可能性があるので、抜け道を用意しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187535/a31bb0dc-5ee6-cb70-8a64-be98d2471b0c.png)

### 2. チャートに移動(中心座標)

```python
    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. チャートの横幅の半分右に移動→値取得

```python
    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()で必要情報のみを取得しています。

```python
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ずつ移動→値取得

```python
    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`
- 対策として[こんなの](https://www.quora.com/Why-is-Selenium-so-slow)があるそうです
- 高速化ができればまた記事を書きたいと思います。