JavaScriptを書ける人であれば誰でもスクレイピングはできるよっていう話

## この記事に書かれていること
- Seleniumの`execute_script`が最高だよという熱い思い。
- PythonやSeleniumが不慣れでも`execute_script`+`JavaScript`で大体いけるのではという提案。

## この記事の対象
- Webスクレイピングに興味がある人
- JavaScriptが書ける人
- Pythonはよくわからないけど環境構築までなら頑張れそうかなという人
- 私の同僚

※ 普段からWebスクレイピングをされていたり、すでにPythonをバリバリ使ってる人にはあまり収穫はないと思います。

# 本題に入る前にスクレイピングについておさらい
WebスクレイピングとはWebサイトから情報を抽出する技術のことです。
テキストや画像を取得したり、ブラウザを操作してファイルをダウンロードするなど実装次第でいろんなことができます。

スクレイピング自体は様々な言語で可能ですが、私は**Python**を使っています。
基礎文法がシンプルで学びやすく、スクレイピングに関するライブラリが豊富だからです。

環境構築は選択肢が多くややこしいですが私はAnacondaで入れています。
・[pythonの環境構築戦争にイラストで終止符をどうやら打てない](https://qiita.com/ganariya/items/1bf870275bad7b5ab506)
・[Anaconda で Python 環境をインストールする](https://qiita.com/t2y/items/2a3eb58103e85d8064b6)

そしてスクレイピングする上でWebサイトについて理解しなければいけないことがあります。
Webサイトはざっくり「静的ページ」と「動的ページ」で分けることができます。

## 静的ページと動的ページ
### 静的ページ
静的ページはいつどこでアクセスしても毎回同じものが表示されるページのことです。
もっと言うと**取得したい情報がHTMLでベタ書きされている**ページです。

```static.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>静的ページ</title>
  </head>
  <body>
    <h1>静的ページとは</h1>
    <p>このように取得したい情報がベタ書きされているページです。</p>
  </body>
</html>
```

### 動的ページ
動的ページとはアクセスしたときの状況に応じて異なる内容が表示されるWebページのことです。
もっと言うと**アクセスしないと何が表示されるかわからない**ページです。

```dynamic.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>動的ページ</title>
  </head>
  <body>
    <h1>動的ページとは</h1>
  </body>
  <script>
    window.onload = () => {
      let ele = document.createElement('p');
      ele.textContent = 'このページが開かれたタイミングで生成されます。';
      let body = document.querySelector('body');
      body.appendChild(ele);
    }
  </script>
</html>
```

## 静的/動的 それぞれの一般的なスクレイピング方法
### 静的ページのスクレイピング
静的ページをスクレイピングする場合は一般的に[Beautiful Soup](https://qiita.com/Chanmoro/items/db51658b073acddea4ac)が利用されます。
HTMLをまるごと解析するので、ベタ書きされている情報は大体取得できます。

逆に動的ページはスクレイピングできません。
例えば上で例に出した動的ページの場合、HTMLでベタ書きされている`<h1>`は取得できますが、
`<p>`は取得できません。HTMLに無いからです。

### 動的ページのスクレイピング

動的ページをスクレイピングする場合は一般的に[Selenium](https://qiita.com/Chanmoro/items/9a3c86bb465c1cce738a)が利用されます。
自動でブラウザを起動し、実際にそのページにアクセスして必要な情報を取得します。
要するに**「アクセスしないと取得できないならアクセスすればいいじゃない」**という発想です。

ちなみにSeleniumは静的ページも当然にスクレイピングできますが、
実際にブラウザを開いたりするのでBeautiful Soupよりも時間がかかります。

ただ本題につながってくる話なのでもう一度言いますが、
**Seleniumは静的/動的関係なくどちらでも使えます。**

# 本題 : なぜ 「JavaScript でいけるじゃん」 と思ったのか

それを語るにはまずSeleniumの基本的な書き方について説明する必要があります。
例として`Yahoo ニュース`のサイトをSeleniumを使って開いてみます。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')
```
![スクリーンショット 2021-04-18 20.41.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/45c96a3b-7776-6d2c-986c-71335ccd5ed4.png)


たった4行でYahooニュースを開けました。
開くサイトを変えたい場合は`driver.get`の引数を変えるだけです。

次に見出しのテキストを取得してみたいと思います。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

headlines = driver.find_elements_by_css_selector('#uamods-topics > div > div > div > ul a')

for headline in headlines:
    print(headline.text)
```

![スクリーンショット 2021-04-18 21.11.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/324710ed-7766-baf3-5f03-aa08bab432f8.png)

取得できました。

まず`driver.find_elements_by_css_selector`で取得したいCSSセレクタを指定しています。
普段JavaScriptを書いている人であれば親しみやすいメソッドだと思います。

`for headline in headlines:`がPythonのfor文です。
`for [仮引数] in [引数]:`という書き方をします。

`print`はJavaScriptでいう`console.log`みたいなものです。

まとめると
`driver.find_elements_by_css_selector`で取得した要素の一覧を`for`で回して
`print`でテキストをコンソール画面に表示させる。という流れです。

今回は driver.find_elements_by_css_selector を利用しましたが、
他にもSeleniumには便利なメソッドが豊富に用意されています。

https://qiita.com/mochio/items/dc9935ee607895420186

ただ、ここまで読んで思った人もいるかもしれません。

**「なんだよ！JavaScriptを書ければ良いって言っといて、結局Pythonの話じゃないか！」**

大丈夫です。ここからです。

## 万能メソッド : execute_script

`execute_script` とは
**してほしい処理をJavaScriptで自由に書ける**メソッドです。

例えば先ほどの処理を`execute_script`で置き換えて書いてみます。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

driver.execute_script('''
    let headlines = document.querySelectorAll('#uamods-topics > div > div > div > ul a');
    headlines.forEach(headline => {
	    console.log(headline.textContent)
    });
''')
```

どうでしょう **書いているのは思いっきりJavaScriptです！
pythonファイルにJavaScriptを書いています！**

エディタからは文字列として認識されるのでシンタックスハイライトが効かないのですが、
書いているのは間違いなくJavaScriptです。

では実行してみます。

![スクリーンショット 2021-04-18 21.56.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/c735678f-897d-900d-889a-4e9a87aef35a.png)


デベロッパーツールでConsoleを開いてみると、ちゃんと見出しの一覧が表示されています！
JavaScriptのコードが実行されました！

### return で値を渡す - 配列

上の例ではconsole.logでそのまま表示させましたが
`return`でPython側に値を渡すこともできます。

ということで次は`return`でPythonに値を渡した後にPythonで`print`してみます。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

headlines = driver.execute_script('''
    let headlines = [...document.querySelectorAll('#uamods-topics > div > div > div > ul a')].map(a => a.textContent);
    return headlines;
''')

for headline in headlines:
    print(headline)
```
![スクリーンショット 2021-04-18 21.11.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/c41f91da-de29-0ff6-9bf9-4701354c4dd5.png)

プリントできました！

このようにJavaScriptで作成した配列は、Pythonでも同じく配列として扱えます。

### return で値を渡す - オブジェクト（連想配列）

配列だけでなくJavaScriptで作成したオブジェクト（連想配列）もreturnでPythonに渡せます。
※ ちなみにPythonでは連想配列を "辞書型" と呼びます。

ではJavaScriptを使ってYahooニューストップのナビバーから
テキストとリンク先を取得し、それをオブジェクトにまとめてreturnしてみたいと思います。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

# JavaScriptでナビバーのaタグからhrefとtextContentを取得しオブジェクトにしてreturn
categories = driver.execute_script('''
    let categories = {};
    let elements = document.querySelectorAll('#snavi > ul.sc-uJMKN.gXWNbF.yjnHeader_sub_cat a');
    elements.forEach(ele => {
        categories[ele.textContent] = ele.href;
    });
    return categories;
''')

# 辞書型のfor文。.items()を付けることで仮引数にkey,valueを分けて持てる。
for text, link in categories.items():
    print(text, link)
```

![スクリーンショット 2021-04-19 23.18.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/c8626270-7704-e2b4-a525-20a15a9eb584.png)

JavaScriptで作成したオブジェクトをPythonでプリントできました。

せっかくなのでそれぞれのリンク先からニュースの見出しを取得してみたいと思います。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

categories = driver.execute_script('''
    let categories = {};
    let elements = document.querySelectorAll('#snavi > ul.sc-uJMKN.gXWNbF.yjnHeader_sub_cat a');
    elements.forEach(ele => {
        categories[ele.textContent] = ele.href;
    });
    return categories;
''')

for text, link in categories.items():
    # それぞれのリンク先を順番に開く
    driver.get(link)
    headlines = driver.execute_script('''
        let headlines = [...document.querySelectorAll('#uamods-topics > div > div > div > ul a')].map(a => a.textContent);
        return headlines;
    ''')

    for headline in headlines:
        print(text, headline)
```

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3633313630302f61313466653135662d343730392d663561372d303636622d6564646337376233653332382e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/8f74af7c-0546-b5ab-d38b-8f8b326b27e6.png)


それぞれのリンク先からニュースの見出しを取得できました。

### せっかくなので csv に書き出す

Python側でcsvに書き出す用の関数を用意して、あとはprintと入れ替えるだけです。
（ここもJavaScriptで書くのもアリかもと一瞬よぎりましたが逆にしんどくなりそうでやめました笑）

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
import csv

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

categories = driver.execute_script('''
    let categories = {};
    let elements = document.querySelectorAll('#snavi > ul.sc-uJMKN.gXWNbF.yjnHeader_sub_cat a');
    elements.forEach(ele => {
        categories[ele.textContent] = ele.href;
    });
    return categories;
''')

# csv に書き出す用の関数
def export_to_csv(category, headline):
    with open('scraping.csv', 'a', encoding='utf-8_sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerow([category, headline])


for text, link in categories.items():
    driver.get(link)
    headlines = driver.execute_script('''
        let headlines = [...document.querySelectorAll('#uamods-topics > div > div > div > ul a')].map(a => a.textContent);
        return headlines;
    ''')

    for headline in headlines:
        export_to_csv(text, headline)
```
![スクリーンショット 2021-04-19 23.47.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/dcc54359-83ed-314a-d38f-c2e99beb398b.png)

csvに書き出せました。


### json に書き出す

取得したデータを最終的にどのような形に加工するかって様々あると思いますが、
必要なデータが決まっていて、かつjsonにさえ書き出せれば後からどうにでもなると思っています。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
import json

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://news.yahoo.co.jp/')

categories = driver.execute_script('''
    let categories = {};
    let elements = document.querySelectorAll('#snavi > ul.sc-uJMKN.gXWNbF.yjnHeader_sub_cat a');
    elements.forEach(ele => {
        categories[ele.textContent] = ele.href;
    });
    return categories;
''')

obj_list = []
for text, link in categories.items():
    driver.get(link)
    headlines = driver.execute_script('''
        let headlines = [...document.querySelectorAll('#uamods-topics > div > div > div > ul a')].map(a => a.textContent);
        return headlines;
    ''')
    # 一つ一つ連想配列を作ってobj_listに追加
    obj_list.append({text: headlines})

# obj_listをjsonに変換
with open('scraping.json', 'a', encoding='utf-8') as f:
    json.dump(obj_list, f, ensure_ascii=False)
```

![スクリーンショット 2021-04-20 0.50.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/e17192a0-39d9-0d36-637e-bbcd851767ce.png)

jsonに書き出せました。

## 要件が複雑であればあるほど威力を発揮する execute_script


seleniumには豊富なメソッドが用意されていることは先に記した通りですが、
逆に言うと用意されたメソッドでやりくりする必要があるということです。

実際、ちょっと複雑な条件分岐を求められただけで途端に難易度があがります。

例えばこのような条件があるとします。

>*Aの値がTrueの場合、Aの兄弟要素であるBを評価し、Trueであればその兄弟要素であるCを返す*

用意されたメソッドでやれないことはないですが、ちょっとめんどくさいです。
なにより「うーん、どうやって取ろうかなー」と考える時間がもったいなく感じてしまいます。

ただ普段からJavaScriptを書いている人であれば深く考えずともイメージがつくはずです。

### 条件分岐は execute_script 内で完結させる

[セントラル・リーグ 個人打撃成績（規定打席以上）](https://npb.jp/bis/2020/stats/bat_c.html)

例としてプロ野球のサイトを用意しました。
**過去10年で3割30本100打点を達成した選手**を取得してみたいと思います。

```scraping.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())

results_list = []
for year in range(2010, 2021):
    driver.get('https://npb.jp/bis/{}/stats/bat_c.html'.format(year))
    this_year_results = driver.execute_script('''
        // バッターの打率の一覧を取得
        let batters = document.querySelectorAll('tr.ststats > td:nth-child(4)');

        let resultsList = [];
        batters.forEach((battar) => {
        // 打率が3割以上か判定
        if (Number(battar.textContent) < 0.3) {
            return;
        }

        // 本塁打が30本以上か判定
        if (Number(battar.parentNode.childNodes[11].textContent) < 30) {
            return;
        }

        // 打点が100以上か判定
        if (Number(battar.parentNode.childNodes[13].textContent) < 100) {
            return;
        }

        let year = location.pathname.replace(/[^0-9]/g, '');
        let name = battar.parentNode.childNodes[1].textContent.replace(/　/g,' ');;
        let ba = Number(battar.textContent).toFixed(3);
        let hr = Number(battar.parentNode.childNodes[11].textContent);
        let rbi = Number(battar.parentNode.childNodes[13].textContent);

        let results = [year, name, ba, hr, rbi];
        resultsList.push(results);
        });

        return resultsList;
    ''')
    results_list.extend(this_year_results)


for results in results_list:
    print(results)
```
![スクリーンショット 2021-04-21 0.46.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/86b68eb1-9aa8-0240-0802-cfd9ea6410e2.png)

取得できました。

このように execute_script を使うことによって、条件分岐が必要な場合でも
難しいことを考えずにJavaScriptで事足りるのです。

## スクレイピングだけでなく自動化ツールも作れる

JavaScriptが書けるということは

`click()`でクリックをしたり
`.checked = true`でチェックボックスにチェックを入れたり
`.value = 'hoge'`で値を入れたりなど、
自由にDOM操作ができるということであり、

つまりそれは**複数のページを横断的に決まった処理を走らせる自動化ツールを作成できる**ということです。

もちろんこれはPython+Seleniumだけで実現できるのですが、
上で例に出したような条件分岐が入る場合はJavaScriptのほうが簡単に書けると思います。
（私自身JavaScriptのほうが慣れてるだけっていう可能性も否めないですが汗）

# まとめ
「Webスクレイピングをしてみたいな」という人で、すでにJavaScriptを書ける場合、
execute_script + JavaScript で大体いけるのではというお話でした。

冷静に考えればブラウザを操作するわけですから、JavaScriptが一番やりやすいに決まってるわけです。

別途Pythonの環境構築やライブラリのインストール方法、ちょっとしたfor文などの使い方などを覚える必要はありますが、すでにJavaScriptを書けている人であればそこまで難しいものではないと思います。

ちょっとWebスクレイピングに興味あるなと考えている方の参考になれば幸いです。

### 補足:
■ 記事紹介: この記事では触れていませんが読んでおいた方が良い内容です。
・[Webスクレイピングの注意事項一覧](https://qiita.com/nezuq/items/c5e827e1827e7cb29011)
・[Seleniumで待機処理するお話](https://qiita.com/uguisuheiankyo/items/cec03891a86dfda12c9a)
・[Selenium WebDriverderでプロセス終了させメモリ枯渇を防ぐ方法](https://shimi-dai.com/selenium-webdriver-close-quit/)

■ 内容について
今回のJavaScriptでいいじゃんっていうお話はあくまでスクレイピングに限った話であり、
例えばデータフレームを取得してグラフ化して分析どうこうなどの話に発展する場合は別途Pythonの勉強が必要です。