Selenium + Chronium + BeautifulSoup でスクレイピング

# 前置き
これまで`Python`でJavaScriptが絡むウェブスクレイピングといえば、
`BeautifulSoup`+`Selenium`+`PhantomJS`が鉄板でした。
しかし、`PhantomJS`が開発終了したことでブラウザを`Chronium`等に移行する傾向にあります。
そこで今回は`Python 3.7`と`Chronium`を使ったウェブスクレイピングについて記事にします。
# 必要なもの
__`Python`関連__

```
Python 3.7 (or 3.x)
bs4 (BeautifulSoup)
selenium
lxml
```
__`Chromedriver`__
[ここ](https://chromedriver.chromium.org/downloads)から落としたあとに実行可能なパスに配置してください。
# 本稿で作るもの
[Hyper Collocation](https://hypcol.marutank.net/ja/)という便利なサイトがあります。
> Hyper Collocation とは？
> arXivに収録されている 811,761報の 英語論文から，例文を検索するための検索エンジンです．前後の語の頻度でソートして結果を返すので典型的な用法の例文を得ることができ，コロケーション辞書のように利用できます．
> (公式サイトより引用)

このサイトでキーワード検索することで、
例えば`「"calculated"の前置詞はなにが一般的なんだろう？」`といった疑問を解消してくれます。
![使用頻度が高い順に並べて表示してくれます](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/b3dd3419-981d-d238-77f2-c61377304ca9.png)
例文とキーワードが使用されているarXivのリンクまで載せてくれます。
![スクリーンショット 2019-08-15 10.46.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/dc8eba15-46fc-9ac3-bba5-e39bcafebfc7.png)

今回はターミナルからこの検索結果を持ってくるものを作ります (完成品は[こちら](https://gist.github.com/Scstechr/4b1e11f56ed51f181eebf77f7d57289c)）。
__静的なサイトからスクレイピングしたい場合は[ここ](#静的なサイト)までスキップ。__
# 実践
以後の説明ではファイル名を簡単のため`main.py`とします。
## モジュールをインポート
```python:main.py
from bs4 import BeautifulSoup

from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.keys import Keys
```
## `driver`をセットアップする

```python:main.py
options = ChromeOptions()
options.add_argument('--headless')
driver = Chrome(options=options)
```
2行目で`--headless`を引数に追加することでウィンドウが開かなくしています。
デバッグ中はコメントアウトしても良いかもしれません。
試しに2行目をコメントアウトした状態で走らせてみましょう。

```bash:Terminalで実行
python main.py
```
![立ち上がるブラウザ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/bd5945cf-51a4-ec63-b857-c8f7590136ce.png)
後に続く処理がないのですぐ閉じてしまうと思いますがブラウザが立ち上がります。
以後、`--headless`を追加しないままいくつか説明をします。
## サイトにアクセス
[Hyper Collocation](https://hypcol.marutank.net/ja/)に立ち上げたブラウザからアクセスします。
また、ブラウザがすぐ閉じないように`sleep()`を使って処理を一時停止します。

```python:main.py
from time import sleep
url = "https://hypcol.marutank.net/ja/"
driver.get(url)
sleep(200000)
```

```bash:Terminalで実行
python main.py
```
![ChroniumでHyper Collocationにアクセス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/d524ef68-1f79-9e99-dd17-981f07829778.png)
## 検索
実際にサイトを利用するとき、キーワードで検索するには
(a)検索ボックス選択、(b)キーワード入力、(c)決定の3つの手順が必要です。
これは今回のようにウェブスクレイピングをしている場合でも同じです。
まず、`Chrome`のデベロッパー（開発者）ツールで検索ボックスを指す`Element`を探します。
![エレメントを探す](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/e175127c-11c8-3612-e5d1-1db76c4315a9.png)
あとは手順(a), (b), (c)を順に行います。
今回は`Element`の指定に`find_element_by_class_name()`というメソッドを使います。
名前の通り`class`名で検索するものですが、他にも`id`や`div`などでも検索できます(詳しくは[こちら](https://kurozumi.github.io/selenium-python/locating-elements.html))。

```python:main.py
keyword = "calculated"
# (a) 検索ボックスを選択
input_element = driver.find_element_by_class_name("input")
# (b) キーワード入力
input_element.send_keys(keyword)
# (c) エンターキー押下
input_element.send_keys(Keys.RETURN)
```

```bash:Terminalで実行
python main.py
```
実行すると指定したキーワード（今回の例では`calculated`）で検索してくれるはずです。
![検索結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/8afc052e-d731-0bfc-bb22-96829a0bbcbd.png)
ここまでのスクリプトは以下のようになります。

```python:main.py

# モジュールのインポート
from bs4 import BeautifulSoup
import lxml
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.keys import Keys
from time import sleep

# ドライバのセットアップ
options = ChromeOptions()
# options.add_argument('--headless')
driver = Chrome(options=options)

# サイトにアクセス
url = "https://hypcol.marutank.net/ja/"
driver.get(url)

# 検索
keyword = "calculated"
input_element = driver.find_element_by_class_name("input")
input_element.send_keys(keyword)
input_element.send_keys(Keys.RETURN)

# 処理を一時停止
sleep(20000)
```

## `BeautifulSoup`でスクレイピング
まず`driver`からソースの`html`をもってきて、`BeautifulSoup()`で`bs4.BeautifulSoup`に変換します。
ここで、パーサには`lxml`を利用します。

```python
html = driver.page_source.encode('utf-8')
soup = BeautifulSoup(html, "lxml")
```

#### 静的なサイト
また、JavaScriptで検索などをしない静的なサイトであれば
`bs4.BeautifulSoup`を得るためには以下のコードで足ります。

```python

import requests
from bs4 import BeautifulSoup

# url指定
url = "https://www.google.com/"
# requestsでページをもってくる
r = requests.get(url)
# bs4.BeautifulSoupに変換
soup = BeautifulSoup(r.text, 'lxml')
```

つぎに、`Chrome`のデベロッパー（開発者）ツールを開いて検索結果の`Element`を探します。
![エレメント検索](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/494b4bc3-cfca-27d1-051c-50b5582aa688.png)
どうやら`div`で`class`が`"bar-list-box tooltip is-tooltip-top"`であるものを探せば良さそうです。

```python
slist = soup.find_all("div", class_="bar-list-box tooltip is-tooltip-top")
```

これで、`slist`の中身は以下のようになるはずです。

```html
[<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated by  — 82841 occurences (8.96%)" style="width: 656px; background-color: rgb(166, 206, 227);"></div>,
<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated from  — 80121 occurences (8.66%)" style="width: 634.461px; background-color: rgb(31, 120, 180);"></div>,
<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated the  — 74610 occurences (8.07%)" style="width: 590.82px; background-color: rgb(178, 223, 138);"></div>,
<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated in  — 67493 occurences (7.30%)" style="width: 534.463px; background-color: rgb(51, 160, 44);"></div>,
<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated using  — 66158 occurences (7.15%)" style="width: 523.891px; background-color: rgb(251, 154, 153);"></div>,
<div class="bar-list-box tooltip is-tooltip-top" data-tooltip="calculated for  — 49738 occurences (5.38%)" style="width: 393.864px; background-color: rgb(227, 26, 28);"></div>,
（以下省略)
```

ただ、実際にサイトを利用して分かる通り、検索結果を表示するまでには少し時間がかかります。  
このサイトの場合、検索がおわると`class="infohead"`の`Element`が出現することがわかっています。
そこで必要な情報が得られるまで繰り返しスクレイピングする文を書きます。

```python
infohead = []
while not len(infohead):
    html = driver.page_source.encode('utf-8')
    soup = BeautifulSoup(html, "lxml")
    slist = soup.find_all("div", class_="bar-list-box tooltip is-tooltip-top")
    infohead = soup.find_all('span', class_="infohead")
```

また、回線速度などの理由でいつまでたっても検索が終了しない場合を考えて、ループに上限を設けます。

```python
import sys

infohead = []
i = 0
while not len(infohead):
    html = driver.page_source.encode('utf-8')
    soup = BeautifulSoup(html, "lxml")
    slist = soup.find_all("div", class_="bar-list-box tooltip is-tooltip-top")
    infohead = soup.find_all('span', class_="infohead")
    i += 1
    if(i > 1000):
        print("TIME OUT")
        sys.exit(1)
```
また、検索してもなにもヒットしなかった場合を想定した処理を加えます。
`.text`は`html`のタグの部分を除去してくれる便利な機能です。

```python
infohead = '\n' + infohead[0].text + '\n'
if not len(slist):
    print(infohead)
    sys.exit(1)
```
あとはデータを整形したり、それを出力するための処理を追加します。
これについては本稿の主旨からは逸れるので割愛します。
# まとめ

- 引数でキーワードを指定可能にした
- 処理を関数に分割した
- 検索結果の表示件数を10件までとした
- データの整形・出力

以上を含めた実装が[こちら](https://gist.github.com/Scstechr/4b1e11f56ed51f181eebf77f7d57289c)になります。

スクレイピングのしやすさなどはサイトによりけりですが、
手順としては本稿で説明したものとほぼ差はないと思われます。
結局のところ、スクレイピングで大変なのはブラウザのセットアップやタグの検索ではなく
データの整形・出力方法を考えるところだと思います。