playwright-pythonがもしもAWS LambdaやAlpine Linuxで動作したら最高なのに...?
playwright-pythonはローカルマシンで動かす分には最高のブラウザ自動操作環境だ。
ただ、Djangoに組み込もうとしたり、AWS Lambdaなどのサーバレス環境で動かそうとしたりすると、割としんどい。
なぜかというと、理由は2つ。
- playwright run-driver をシェル実行できる必要がある
ので、Dockerコンテナを用意するところから...... - ブラウザを実行するために、各種ライブラリが必要
Webサービスだったら、そもそもシェル実行なんて避けたいだろうし、WebサービスのコンテナにGUI系のライブラリが山ほど入っていると、脆弱性が...とかとかある。
AWS LambdaでPlaywrightを動かすには、以下の記事に書かれているように、わりと準備が大変だ。
実はピュアPythonで動かせる仕組みはすでにある
Playwrightはサーバー/クライアントアーキテクチャで、従来はパイプ(標準入力/標準出力)を通信路として、サーバー(Node.jsベースのPlaywrightドライバ)とクライアント(playwright-pythonで書いたスクリプト)が動作する。
もし通信路がWebSocketになったらどうだろう?
こんな感じで、サーバはどこかのPaaSで動かしておいて、クライアント部分だけをDjangoなりAWS Lambdaなりで動かせばいい構成になる。この構成ならブラウザ動作に必要な依存パッケージなども不要なので、Alpine Linuxでも動かせる。
Playwright 1.11から導入されたWebSocketTransport
※ _付きのパッケージなので、今後変わる可能性が大です!!
PipeTransportという従来の方式に加え、WebSocketTransportが追加された。
これにより、 sync_playwright()
の中でPipeTransportを作ってConnectionを作っている処理を真似して、 `WebSocketTransportを作ってConnectionを作る版の sync_playwright_remote() みたいなのを作ればいい。
↑これをコピペして、こんな感じに。
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)
あとは、
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イメージを使って動かしたサンプルは、このスクラップ↓
GitHubコードは↓
Alpine LinuxへのPlaywrightのインストールは↓
まとめ
Playwright 1.11以降であれば、隠しパッケージに含まれるWebSocketTransportを使って、実現できる。
隠しパッケージなので、いつかきっと壊れるだろうけど、1.12.3現在はまだ壊れていない。
Discussion