Playwrightのリリースノートに、気になる記載がある。
Release v1.4.0 · microsoft/playwright · GitHub
英語が苦手でも読めるよう、みんな大好きdeepl翻訳をかけておこう。
爆速の多言語展開スピードを支える何かがあるらしいことはわかる。
ただ、具体的にどういう仕組みで動いているとか、どうやればクライアントが実装できるよとか、そういう情報はドキュメントには一切書かれていない。 さすがマイクロソフト。 なので、いろいろソースを読んで調べた&実際にRubyからPlaywrightを使うPoCを作ってみた。
PlaywrightはServer/Clientモジュールに分かれている
結論から言うと、
- microsoft/playwright (TypeScript版)にはServerモジュールとClientモジュールの両方が含まれている
- ただ、READMEなどに書かれている動作では、サーバー・クライアント動作は特にせず、1つのプロセスで自動化が動く
- microsoft/playwright-python, microsoft/playwright-java, microsoft/playwright-sharp, mxschmitt/playwright-go はいずれも、PlaywrightのClientモジュールである
サーバーモジュールというのは何かというと、Playwright自身が特定のWebSocketまたはパイプ(標準入力/標準出力)をバインドして、そこで受けた命令をそっくりそのままChromeやFirefoxやSafariに(いい感じに変換して)投げる君。
クライアントモジュールというのは、ユーザが書くスクリプトで実際に使われるPageとかBrowserとかElementHandleとかそのへんのクラスで、サーバーに対して、WebSocketなりパイプなりで、要求を投げる君。
- https://github.com/microsoft/playwright/tree/v1.7.1/src/server
- https://github.com/microsoft/playwright/tree/v1.7.1/src/client
PythonやJavaやC#やGoのクライアントは、内部的にplaywright-cliを使ってサーバーを起動している
リリースノートに、「Nodeじゃなくても」動かせるようにした、と書いてあった部分の話。
In the last release, we introduced an internal protocol to support Playwright in the none-Node environments
タイトルでネタバレしてしまったが、それぞれのクライアントには
がある。
Pythonであれば
- cliをダウンロードする部分→ https://github.com/microsoft/playwright-python/blob/v0.171.1/setup.py
- cli run-driverする部分→ https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/_transport.py
Goであれば
- cliをダウンロード/run-driverする部分→ https://github.com/mxschmitt/playwright-go/blob/v0.171.1/run.go
試しに、手元で npx playwright-cli run-driver
してみたら、なんとそれだけでPlaywrightサーバーが立ち上がるのがわかる。
ただ、playwright-pythonやplaywright-javaなどでダウンロードされているplaywright-cliはNode環境がなくても動くようにシングルバイナリ?っぽい形で配布されたもののようだ。 playwright-pythonやjavaがダウンロードしているURLは https://playwright.azureedge.net/builds/cli/next/playwright-cli-0.180.0-next.1608746109749-cbc13bd-mac.zip
こんな感じのもので、実際にそこからダウンロードして中身を見てみた↓
playwright-cli run-driverはPlaywrightサーバーモジュールを起動するだけ!
playwright-cli run-driverの内部実装も一応メモっておく。
実に単純で、
// Implement driver command. if (process.argv[2] === 'run-driver') runServer(); else if (process.argv[2] === 'print-api-json') printApiJson();
playwright-cli/cli.ts at v0.171.0 · microsoft/playwright-cli · GitHub
const { Playwright } = require('playwright/lib/server/playwright'); (中略) export function runServer() { installDebugController(); installTracer(); const dispatcherConnection = new DispatcherConnection(); const transport = new Transport(process.stdout, process.stdin); transport.onclose = async () => { // Force exit after 30 seconds. setTimeout(() => process.exit(0), 30000); // Meanwhile, try to gracefully close all browsers. await gracefullyCloseAll(); process.exit(0); }; transport.onmessage = (message: string) => dispatcherConnection.dispatch(JSON.parse(message)); dispatcherConnection.onmessage = (message: string) => transport.send(JSON.stringify(message)); const playwright = new Playwright(__dirname, require('playwright/browsers.json')['browsers']); (playwright as any).electron = new Electron(); new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); }
Playwrightサーバーモジュールを直接インクルードして起動し、通信路としてパイプ(stdin/stdout)を指定しているだけ。
あとは、Playwrightプロトコルをしゃべるクライアントを書けばいい
サーバーの立ち上げ方がわかったところで、あとは標準入力/標準出力を介してPlaywrightのプロトコルをしゃべるクライアントを書けばいいだけ、ということになる。
ただ、playwright-pythonもplaywright-javaもソースを見てみるとちょっと変わった作りをしていて、プロトコルのJSONをもとにAPIクライアントインターフェースを自動生成するようになっている。
npx playwright-cli print-api-json | jq .
こうすると、どっっばーーーーーっとAPIインターフェースを定義したJSONが降ってくる。playwright-pythonもplaywright-javaもこれを頑張って解析してAPIクライアントインターフェースを生成している。
わかりやすいのはPythonで、 このへんでソース生成君がいて、実際に生成されたソースは
- https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/async_api.py
- https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/sync_api.py
このあたりだ。
これとは別に、インターフェースの実装部分を、作っていけば、クライアントモジュールが出来上がる。
実際にRubyクライアント書いてみた
プロトコルJSONを読んでコード生成する部分が地味に大変だったけど、そのぶん実装部分はめっちゃ楽。なにこれ。ってかんじ。
require 'playwright' Playwright.create(playwright_cli_executable_path: '/path/to/playwright-cli') do |playwright| playwright.chromium.launch(headless: false) do |browser| page = browser.new_page page.goto('https://github.com/YusukeIwaki') page.screenshot(path: './YusukeIwaki.png') end end
このくらいの簡単なスクリプトを動かすだけなら、Browser, BrowserType, Page, Frame などの主要なクラスにいくつかのメソッドを実装するだけで、動くようになる。
puppeteer-rubyのときに散々苦しんだ、CDPSessionまわりの並行処理の順序変わっちゃう問題などは一切なく、(そのへんはサーバーモジュールがやってくれるので!)本当に素直にクライアント書くだけだ。
puppeteer-ruby のほうはしばらく開発をゆるめにして、Playwrightのほうを本腰入れて作っていこうかなと思う。