ブラウザ自動操作の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またはパイプ(標準入力/標準出力)をバインドして、そこで受けた命令をそっくりそのままChromeFirefoxSafariに(いい感じに変換して)投げる君。

クライアントモジュールというのは、ユーザが書くスクリプトで実際に使われるPageとかBrowserとかElementHandleとかそのへんのクラスで、サーバーに対して、WebSocketなりパイプなりで、要求を投げる君。

PythonJavaC#や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-pythonjavaがダウンロードしている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のほうを本腰入れて作っていこうかなと思う。

2020年のふりかえり

とくに書くことは決めていないが、2020年にあったことを書いとこう。

ちょっと昇給した

2019年の6月でクラウド○ークスをやめて、2019年7月から福岡が本社のIT企業で働き始めた。もともとCW社にいた頃もジリ貧生活していたものが、転職後しばらくはさらに貧乏生活になり、精神衛生上かなりしんどい時期が続いた。

どのくらい貧乏カツカツだったかというと、給与振込口座の残高が1万円を切った月が4回くらい続いたかな、という感じ。富士通時代の貯蓄がなかったら本当に詰んでた。

「エンジニア年収○○万円!」みたいな釣り広告が羨ましく思うことはあったが、そんなうまい話があるはずもないことは知っているので、地道に会社の役に立つことを自分なりに考えてやり続けた。本当にそれだけだ。結果、2020年の2月からちょっとだけ(かなり)お給料が上がった。

f:id:YusukeIwaki:20201231155421p:plain

CW社のころはなかなか自分の価値を見出しづらく4年半いて本当に給与が上がらなかったので、やっとか・・・・という感じ。

ついでにちょっと昇級した

2020年の4月から、"課長"っていう肩書が一応ついた。組織図だと、木の枝の分岐点みたいな場所に名前が載るんだけど、正直全く実体とあっていないのでどうにかしてほしいと日々思っているw

そんなことはともかく、転職するときに書いてた「組織の課題解決屋」ってのが、自分のなかでは結構いいキャッチフレーズになっている。

yusukeiwaki.hatenablog.com

技術のわからない窓際鎮座おじさんにはなりたくないので、日々それなりに技術的な勉強はやっているのだけど、昔のようにあれもこれも手を付けてみるというやりかたは最近あまりやっていない。「自分がそれなりに打ち込める」以外の尺度として「将来的に仕事場を楽しくできそう」というのが明らかなところを狙って勉強するようになった。

ちょっと前だとPuppeteer(Ruby用のGem作ったりしている)、最近だとNext.jsを少しやり始めた。

自分が好きなことをやるだけでは仕事は楽しくなっていかないので、 副業なり勉強なりを積んで、本業を楽しくしていこう という意識が芽生えたのがここ1年の大きな変化だ。

最近読み始めた「学習する組織」が結構ドハマリしていて、まだ読み終わってないけど、いいことが結構書いてあるので、実践まで持っていきたいなと思う。

yusukeiwaki.hatenablog.com

賃貸住宅からの脱却

2020年4月末に、住宅ローンを背負い、中古マンションを購入した。

昨今いろいろと不動産購入の批判記事はあるが、あれはほとんど関東でしか成り立たない批判の論理だろう。福岡市は賃貸の相場が高く、分譲マンションの相場が安い傾向にあるので、毎月同じ値段を払うにしても分譲マンションのほうが遥かに良い物件に住める。

もともと西新の便利な場所に住んでいたので、なるべくその近くで中古マンションがあれば、、と最初は思っていたのだけど、西新は非常に中古マンション相場が高い。地下鉄空港線で賃貸の相場がほぼ同じ姪浜と比べてみても、西新は1.5倍くらい高い。

半年くらい探したけど、結局、西新をやめて姪浜の駅近くに住むことにした。

完全に偶然ではあるけど、ちょうどコロナの影響で通勤する頻度が激減したので、天神から近い場所に住む必要がなくなってしまい、西新よりも買い物の便がよい姪浜にして正解だった。

休日に糸島方面にドライブや登山しやすくなったこともあり、生活の充実度がかなり上がった。

あれ、2020年あまり変わってない?

コロナの影響で、旅行もほとんど行かなかったし、仕事も家で黙々とやってたので、あんまり思い出がない。

来年は登山に行きまくるかな。

「学習する組織」を読んでいる

まじでこれはいい本だ

読み始めたきっかけは、クラウドワークスエンジニアブログにあったtmknomさんの 「無人化システム」を駆逐する組織マネジメントとエンジニアリング の記事だ。組織論の話なのかな?と思わせるタイトルだが、「システム思考」などの理論を説明している部分が主だ。

(まだ全部読み切ってない段階でこんな記事書くなよ!って感じだけど、とりあえず書くw)

自分は、↑「なぜあの人の解決策はいつもうまくいくのか?」を読んでから、学習する組織を読み始めている。いきなり学習する組織を読むと、かなり面喰らうんじゃないだろうか。どちらの本も似たようなことを書いてるんだけど、「なぜあの人の・・・」のほうが具体例が身近でわかりやすい。

読んでいて感じたこと

国語力が皆無な自分ながらも、感じたことをいくつか書いておく。

スナップショットで判断するのではなく、時間をかけてシステム理解をする

暫定対処を重ねたうちに、本当にやりたいことをやりづらくなり、身動きがとれなくなった。

みたいなことはまれによくある。

  • 動きがよくわからないコードをベースとする機能改修で「とりあえずコピペ」してリリースした。
  • しばらくしたらバグったので、if文を追加して今回の機能改修部分だけを特別扱いした。
  • しばらくして別の機能を追加するときに・・・(以下略

ソフトウェア開発を仕事でやっていると、よほど意識が高い集団でもない限り、こういう現場を目にすることは多い。

この問題は多くの場合、要件をあらためて理解して、合理的な設計部分を少しずつ増やしていき、啓蒙を重ねていくことで解決に向かう。しかしながら、多くの現場ではそれがされないまま、コピペプログラミングとif文追加による複雑化という悪循環が10周20周と繰り返される。

本には色々書いてあったが、まず問題が複雑化するという1循環を認識できているかが1つ大きなポイントなのかなと思った。

たとえば、設計に無関心でも、とにかく動いてテストが通ればリリースができる某スマホメーカーのようなやりかたでは、なんとなく期を重ねるごとに開発効率が落ちているという感触は得ることができても、それに対する問題認識ができないだろう。

根本原因の本対処と暫定対処はセットで考える

自分がこれまで8年くらいソフトウェア開発をしていて、どうしても時間的制約などから暫定対処を考えないといけないことは多かった。そのときに、経験したのはだいたい以下のどちらかだ

  • 実は時間的制約はあまりなく、暫定対処をやめて最初から本対処した
  • 本対処を後回しにした結果、暫定対処のコードで3年以上運用されることになってしまった

「暫定対処したあと、後になって本対処をした」ということは、残念ながら数えるくらいしか無い。

本には書いてあるが、暫定対処は本対処に対する副作用を持っている。ソフトウェア開発であれば「暫定対処で乗り切った」→「本対処をし直す・・?まじで?」という心理的な障壁が、些細なものではなく割と大きい。つまり、暫定対処からの本対処という線形的な流れになることは非常にまれで、暫定対処と本対処の"二者択一" となってしまうケースが圧倒的に多い。

課題が認識できていて、本対処と暫定対処の両方が明確である場合にはまだいい。

問題は、「原因があまり認識できていないまま、本対処もよくわからないまま、暫定対処で乗り切る」というケースなのかなと。自分はわりと場当たり的な対応をしてしまうことが多いので、今後気をつけていかなければ、と改めて感じた。

進捗だめです...

f:id:YusukeIwaki:20201227154157p:plain

なかなかのボリュームゆえ、まだ27%

お正月休み中に読み終われるかな...