AWS Lambda＋Seleniumでスクレイピングする話

# 導入

業務の一環で実行しているスクレイピング。
python + seleniumでツールを作って定時に社内サーバーから自動実行していた。

ある日、設定していた社内サーバーが計画停止。
とりあえずは自分の環境で手動実行してデータは取得したけれど・・・

「技術として汎用性もあるし、これを機にAWS Lambdaへ移動や！」

ということでやってみたのが今回のお話。

# 前提

* 実行環境：AWS Lambda
* 設定環境１：Windows10
* 設定環境２：debian10（権限設定用リモート環境・WSL2とかでもいけるかも）
* 言語：python3.7
    * 追加パッケージ：selenium

# コード

AWS Lambdaに配置したソースコードがこちら。

サンプルとしてYahooのプロ野球の順位表から、セ・リーグの順位を抜き出してみた。

https://baseball.yahoo.co.jp/npb/standings/

``` lambda_function.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--single-process')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--lang=ja-JP')
options.add_experimental_option('w3c', True)
options.binary_location = '/opt/headless-chromium'

def test_selenium():

    driver = webdriver.Chrome(executable_path="/opt/chromedriver", chrome_options=options)
    driver.get('https://baseball.yahoo.co.jp/npb/standings/')
    elm = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, ".bb-rankTable__row"))
    )
    elm = driver.find_element(By.CSS_SELECTOR, '.bb-rankTable')
    elm = elm.find_element(By.TAG_NAME, "tbody")
    elm = elm.find_elements(By.TAG_NAME, "tr")
    for elmitem in elm:
        team = elmitem.find_element(By.CSS_SELECTOR, '.bb-rankTable__data--team')
        print(team.text)

    driver.close()
    driver.quit()


def lambda_handler(event, context):
    print("セ・リーグ順位")
    test_selenium()
```

結果↓

```
セ・リーグ順位
ヤクルト
阪神
巨人
広島
中日
DeNA
```

# 実施手順

## ツールの用意

まず、大前提としてAWS Lambdaはもちろん通常のブラウザが使える環境ではない。
なので、UIが表示されないブラウザ「Headless Chrome」を利用する。


ちなみに「Headless Chrome」は以下の用途で用いられるらしい。

> ヘッドレスブラウザは、GUI を持つ必要のない自動テスト環境やサーバー環境にとてもよいツールです。例としては、実際のウェブページに対してなにかテストを実行する、そのページの PDF を生成する、またはただ、そのページがどう表示されるかを検証するなどが挙げられるでしょうか。

https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja

### ツールのDL

#### Headless Chrome

まずは上述の「Headless Chrome」を用意する。

以下のURLよりDLする。

```
https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-55/stable-headless-chromium-amazonlinux-2017-03.zip
```

#### ChromeDriver

プログラムでブラウザを動かす為にはドライバーが必要である。「ChromeDriver」を用意する。

以下のURLよりDLする。

```
https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip
```

この時、「Headless Chrome」に対応した「ChromeDriver」を選択する必要がある。(今回は2.37を選択した。)


### ツールの権限設定

WindowsでDLした場合、権限の設定が必要になる。
DLしたファイルにLinux上で以下の権限を与える。

```
$ chmod 755 chromedriver
$ chmod 755 headless-chromium
```


### パッケージの用意

AWS Lambda上でseleniumを動かすためにpythonパッケージを用意する。
(今回はAWS Lambda実行環境に合わせて、念のためLinux環境でパッケージを用意した。)

```
$ pip install selenium -t python/lib/python3.7/site-packages
```


### ツールのzip化

AWS Lambdaに設定するため、用意したツールをzipで固める。
改定頻度を考え以下の構成でツールを固めた。

* python.zip・・・①
    * python/lib/python3.7/site-packages/*
    * headless-chromium
* driver.zip・・・②
    * chromedriver


## AWSでの設定

### レイヤーの設定

先ほど用意したツール群はレイヤーとして登録する。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html

レイヤーを作成することのメリットは多数あるが、一番恩恵を感じたのは
`ツール群のサイズが大きいため、コードに含んでしまうとAWS Lambda上のエディタでコード編集が出来なくなる。`
という状況を回避することが出来ることかもしれない。

つまり、レイヤーをうまく使うことで効率よく開発をすることが出来、メンテナンス性も向上することが出来るのである。

また、他の関数と共有することも可能であるため「別関数でスクレイピングしたい」という場合には
レイヤーを設定することで簡単に導入することも可能である。

![a.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198205/20d95d47-effc-b729-8356-da97a5218e72.png)

:::note warn
互換性のあるランタイムにはpython3.7を含めること
:::


### 関数の作成

さて、ここまで終わったらついに関数の作成である。

関数の作成方法は多数の記事があるので割愛させていただく。

* 関数名
    * 任意（selenium_testなど）
* ランタイム
    * python3.7
* アーキテクチャ
    * x86_64（デフォルト）
* アクセス権限等
    * （デフォルト）

:::note warn
ランタイムをpython3.7である理由は、現状python3.8以上では動作しないためである。
今後、動くようになった場合は本ドキュメントを更新する。
:::


### 関数にレイヤーを設定

先ほど作成したレイヤーを関数に設定する。

今時点のUIではコードタブの最下部にて設定が可能である。

![b.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198205/e1ea5c97-f2f1-2f00-fa3e-27240f32bad6.png)

:::note info
一度設定した後は、この設定を行うだけで他の関数でもseleniumを導入可能である。
:::

### 関数の設定

設定タブで「メモリ」と「タイムアウト」を調整する。

ヘッドレスブラウザ起動～スクレイピングを内部的に実行するので
普通の関数実行よりもちろん時間はかかるので、調整しなければ99％タイムアウトしてしまう。


### コードの実装

後は、最初に示したコードを実行すると同様の結果が得られるはずである。

動かすだけなら基本コードは以下である。

``` lambda_function.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--single-process')
options.add_argument('--disable-dev-shm-usage')
options.binary_location = '/opt/headless-chromium'

def lambda_handler(event, context):
    driver = webdriver.Chrome(executable_path="/opt/chromedriver", chrome_options=options)
    driver.get('https://baseball.yahoo.co.jp/npb/standings/')
    driver.close()
    driver.quit()
```

<dl>
  <dt>オプションについて</dt>
  <dd><code>options.add_argument</code>で追加しているオプションはヘッドレスブラウザで起動するためのものである。</dd>
  <dt>レイヤーの参照について</dt>
  <dd>レイヤーに設定したファイルは<code>/opt/*</code>で参照する。</dd>
</dl>


## ハマりポイント

### 日本語サイトの表示

ブラウザの設定言語で表示が変わるページは初期設定が英語となっている関係で
意図した結果が得られない場合がある。

その場合はオプションに以下を設定することで日本語で表示をしてもらうことが出来る。

```
options.add_argument('--lang=ja-JP')
```

### 解析時の注意

WebElementを解析する時、`w3c`が`False`となっていると
`driver.find_element(*)`等でページを解析した結果がdict型となっている現象が確認された。

これでは正直解析が出来ないので

```
options.add_experimental_option('w3c', True)
```

を明示的に設定し、`w3c`を`True`にする必要がある。

そうすることで、`selenium.webdriver.remote.webelement.WebElement`が解析結果として返却され
text等を参照することが出来るようになる。

# 終わりに

思いつきで始めた実装ではあったが
AWS Lambda上でスクレイピングを行うことが出来れば、
サーバーレスでのテストの自動化等につながると実感することが出来た。

# 参考文献

先駆者の力を多数借りましたので、この場を借りてお礼申し上げます。

* [AWS Lambda(Python)でSelenium + Headless Chromeの実行](https://masakimisawa.com/selenium_headless-chrome_python_on_lambda/)
* [【AWS Lambda】でseleniumを使ってスクレイピングをサーバーレスで行う～前編～](https://qiita.com/ichihara-development/items/5e61c3424b3176bc6096)
* [【Python】AWS Lambdaでheadless chrome+seleniumを実行する方法](https://note.com/kei198403/n/n24d8f0bfb39a)
* [AWS Lambda PythonでSeleniumとheadless-chromiumを使ってスクレイピングする方法](https://syachiku.net/aws-lambda-python-selenium/)
* [起動オプション](http://chrome.half-moon.org/43.html)
* [Chromeの--disable-dev-shm-usageオプションについて](https://qiita.com/yoshi10321/items/8b7e6ed2c2c15c3344c6)
* [stack overflow - Selenium...](https://stackoverflow.com/questions/68640373/selenium-chromedriver-dict-object-has-no-attribute-click-for-find-element#)