🍉

playwright-pythonがもしもAWS LambdaやAlpine Linuxで動作したら最高なのに...?

4 min read

playwright-pythonはローカルマシンで動かす分には最高のブラウザ自動操作環境だ。
ただ、Djangoに組み込もうとしたり、AWS Lambdaなどのサーバレス環境で動かそうとしたりすると、割としんどい。

なぜかというと、理由は2つ。

  • playwright run-driver をシェル実行できる必要がある
    ので、Dockerコンテナを用意するところから......
  • ブラウザを実行するために、各種ライブラリが必要

Webサービスだったら、そもそもシェル実行なんて避けたいだろうし、WebサービスのコンテナにGUI系のライブラリが山ほど入っていると、脆弱性が...とかとかある。

AWS LambdaでPlaywrightを動かすには、以下の記事に書かれているように、わりと準備が大変だ。

https://tech.smartshopping.co.jp/lambda-container-playwright

実はピュアPythonで動かせる仕組みはすでにある

Playwrightはサーバー/クライアントアーキテクチャで、従来はパイプ(標準入力/標準出力)を通信路として、サーバー(Node.jsベースのPlaywrightドライバ)とクライアント(playwright-pythonで書いたスクリプト)が動作する。

もし通信路がWebSocketになったらどうだろう?

こんな感じで、サーバはどこかのPaaSで動かしておいて、クライアント部分だけをDjangoなりAWS Lambdaなりで動かせばいい構成になる。この構成ならブラウザ動作に必要な依存パッケージなども不要なので、Alpine Linuxでも動かせる。

Playwright 1.11から導入されたWebSocketTransport

※ _付きのパッケージなので、今後変わる可能性が大です!!

https://github.com/microsoft/playwright-python/commit/9d796623a104e082f57878d29d9aea20009b88c1

PipeTransportという従来の方式に加え、WebSocketTransportが追加された。

これにより、 sync_playwright() の中でPipeTransportを作ってConnectionを作っている処理を真似して、 `WebSocketTransportを作ってConnectionを作る版の sync_playwright_remote() みたいなのを作ればいい。

https://github.com/microsoft/playwright-python/blob/v1.12.1/playwright/sync_api/_context_manager.py

↑これをコピペして、こんな感じに。

class SyncPlaywrightRemoteContextManager:
    def __init__(self, ws_endpoint) -> None:
        self._playwright: SyncPlaywright
        self._ws_endpoint = ws_endpoint

    def __enter__(self) -> SyncPlaywright:
        loop: asyncio.AbstractEventLoop
        own_loop = None
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()
            own_loop = loop
        if loop.is_running():
            raise Error(
                """It looks like you are using Playwright Sync API inside the asyncio loop.
Please use the Async API instead."""
            )

        def greenlet_main() -> None:
            loop.run_until_complete(self._connection.run_as_sync())

            if own_loop:
                loop.run_until_complete(loop.shutdown_asyncgens())
                loop.close()

        dispatcher_fiber = greenlet(greenlet_main)
        self._connection = Connection(
            dispatcher_fiber,
            create_remote_object,
            WebSocketTransport(loop, self._ws_endpoint)
        )

        g_self = greenlet.getcurrent()

        def callback_wrapper(playwright_impl: Playwright) -> None:
            self._playwright = SyncPlaywright(playwright_impl)
            g_self.switch()

        self._connection.call_on_object_with_known_name(
            "Playwright", callback_wrapper)

        dispatcher_fiber.switch()
        playwright = self._playwright
        playwright.stop = self.__exit__  # type: ignore
        return playwright

    def start(self) -> SyncPlaywright:
        return self.__enter__()

    def __exit__(self, *args: Any) -> None:
        self._connection.stop_sync()


def sync_playwright_remote(ws_endpoint) -> SyncPlaywrightRemoteContextManager:
    return SyncPlaywrightRemoteContextManager(ws_endpoint)

あとは、

main.py

with sync_playwright_remote('ws://127.0.0.1:8080/ws') as playwright:
  playwright.chromium.launch() as browser:
    page = browser.new_page()
    page.goto('https://.....')

こんな感じで、スクレイピングスクリプトを書けば、WebSocket経由でPlaywrightプロトコルが流れて、自動操作ができる。

実際にHeroku上でPlaywrightサーバーを動かして、手元の python:3.9-alpine のDockerイメージを使って動かしたサンプルは、このスクラップ↓

https://zenn.dev/yusukeiwaki/scraps/3ad5f8ed536720

GitHubコードは↓

https://github.com/YusukeIwaki/playwright-python-playWithWebSocket/tree/main

Alpine LinuxへのPlaywrightのインストールは↓

https://github.com/YusukeIwaki/playwright-python-remote#execute-python-script-on-alpine-linux-environment

まとめ

Playwright 1.11以降であれば、隠しパッケージに含まれるWebSocketTransportを使って、実現できる。
隠しパッケージなので、いつかきっと壊れるだろうけど、1.12.3現在はまだ壊れていない。