えんじにあのじゆうちょう

勉強したことを中心にアウトプットしていきます。

seleniumを落ちないように気をつけながら使ってみた

はじめに

AJAXを使ったWebページのスクレイピングをするためには、単純にHTMLを取得するだけではなく、実際に描画、JSの実行等諸々の処理をしてレンダリングが完了した上で必要なものを取得していく必要があります。
今回諸事情により色々試すことになったので、試したことを順を追って記録しておこうと思います。

お品書き

seleniumをdockerで利用する

pythonようにこちらのブログ*1を参考にDockerfileを作りました。

あとは

docker build -t YOUR_TAG .

でビルドして、

docker run -it -v LOCAL_SCRIPT_DIR_PATH:CONTAINER_SCRIPT_DIR_PATH --name selenium YOUR_TAG python3 CONTAINER_SCRIPT_DIR_PATH/SCRIPT_NAME.py

として実行していく感じです。

オプションの工夫

今回はページ遷移のあるサイトを次々*2にスクレイピングしていたのですが、主にメモリを食いつぶして落ちているのがわかったので、まずはこのあたり*3を参考にオプションを色々と変えました。

最終的に以下に落ち着いています。

o = Options()
o.add_argument('--headless')
o.add_argument("--start-maximized")
o.add_argument("--disable-infobars")
o.add_argument("--disable-extensions")
o.add_argument("--disable-gpu")
o.add_argument("--disable-dev-shm-usage")
o.add_argument("--no-sandbox")

要素の中身を取得する

要素の取得は簡単で、以下のようにすれば良いです。

from selenium.webdriver.common.by import By
el = d.find_element(By.CSS_SELECTOR, "YOUR_SELECTOR_STRING") # CSS Selectorの場合

しかし、確実に中身はある(ローカルのChromeでは取れている)ものがとれないということがありました。
seleniumでは描画領域外の要素は取得できないという制約があるようです。*4

el.text #elが描画領域外にあるとき、値は取得できない

以下のようにその位置にスクロールするようにコードを追加します。

d.execute_script("arguments[0].scrollIntoView(true);", el) # これで要素の位置までスクロールさせている
el.text #取得可能

1つのセッション内でページを遷移させていく

たまたまそのときは合計で100ページくらいのスクレイピングをする必要があったのですが、driver.getで遷移させていくとどんどんメモリを消費していって、最終的にはdockerコンテナに割り当てられたメモリ量を超えてしまい落ちてしまいました。

簡易化したコード(waitなどを除く)と以下のようなイメージです。

for url in [url1, url2, ...]:
    d.get(url)
    # いろいろな処理

そのため、私は1回のスクリプト実行で1つのコンテナを生成し、1ページのみ処理する、それを3並列くらいで実行していくという形にしました。
概ねこんな感じです。

呼び出し元

from concurrent.futures import ThreadPoolExecutor

import subprocess
import docker
client = docker.from_env()

def run_thread(url):
    c = client.containers.run(
        "YOUR_SELENIUM_TAG",
        command=["python3", "YOUR_SCRAPING_SCRIPT_PATH_ON_CONTAINER/YOUR_SCRAPING_SCRIPT.py", url],
        remove=True,
        volumes={
            'YOUR_SCRAPING_SCRIPT_PATH_ON_HOST': {
                'bind': 'YOUR_SCRAPING_SCRIPT_PATH_ON_CONTAINER',
                'mode': 'ro'
            }
        }
    )
    return c.decode('utf-8') # 今回はテキストのみを返すスクレイピングスクリプトだったためこうしました

# Threadプールの作成
tpe = ThreadPoolExecutor(max_workers=3)
futures = []
for url in [YOUR_TARGET_URL_LIST]:
    future = tpe.submit(run_thread, url)
    futures.append(future)

for future in futures:
    print(future.result())

動的なページの描画処理が終わるのを待つ

AJAXページの場合、描画が終わったことを確認してから値を取得しないといけません。
以下のように実装してみました。

d.get(url)

def get_element(by, query, max_iteration=5, interval=1):
    for i in range(max_iteration):
        try:
            el = d.find_element(by, query)
            break
        except:
            time.sleep(interval)
    else:
        return None
    return el

おわりに

今回はseleniumの自分がハマったところのまとめを書きました。
seleniumのコードそのものは書きやすく難しくもないのですが、意外と細かい使い方的なところが大変だなぁと思った次第でした。