ブラウザ自動操作のPlaywrightはRubyからでも使える?

Playwrightのリリースノートに、気になる記載がある。

f:id:YusukeIwaki:20210112235602p:plain

Release v1.4.0 · microsoft/playwright · GitHub

英語が苦手でも読めるよう、みんな大好きdeepl翻訳をかけておこう。

f:id:YusukeIwaki:20210113011154p:plain

爆速の多言語展開スピードを支える何かがあるらしいことはわかる。

ただ、具体的にどういう仕組みで動いているとか、どうやればクライアントが実装できるよとか、そういう情報はドキュメントには一切書かれていない。 さすがマイクロソフト。 なので、いろいろソースを読んで調べた&実際にRubyからPlaywrightを使うPoCを作ってみた。

simple_scraping

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なりパイプなりで、要求を投げる君。

f:id:YusukeIwaki:20210208002728p:plain

PythonやJavaやC#やGoのクライアントは、内部的にplaywright-cliを使ってサーバーを起動している

リリースノートに、「Nodeじゃなくても」動かせるようにした、と書いてあった部分の話。

In the last release, we introduced an internal protocol to support Playwright in the none-Node environments

タイトルでネタバレしてしまったが、それぞれのクライアントには

  • playwright-cli (v1.8以降はplaywright driver)をダウンロードする部分
  • playwright-cli run-driver を実行する部分

がある。

Pythonであれば

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 こんな感じのもので、実際にそこからダウンロードして中身を見てみた↓

f:id:YusukeIwaki:20210113001801p:plain

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で、 このへんでソース生成君がいて、実際に生成されたソースは

このあたりだ。

これとは別に、インターフェースの実装部分を、作っていけば、クライアントモジュールが出来上がる。

実際にRubyクライアント書いてみた

github.com

プロトコル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のほうを本腰入れて作っていこうかなと思う。