まえおき
(追記: 開発再開してます→ やっぱりPuppeteerをRubyから使えないと困るので、puppeteer-rubyを作ることにした - YusukeIwakiのブログ )
Puppeteerはすごい。「自動化はSeleniumで十分じゃね?」と思ってた自分だが、Puppeteerには完全に魅了されてしまった。
puppeteerはJavaScriptで書かれているライブラリなんだけど、Dartで動くようにしてるすごい人がいる。C#とかPythonで動くようにしている人もいる。
「・・・あれ、Ruby版は?」
そう、ないのだ。
「Rubyは日頃の業務で使ってるからPuppeteer使えたら何か便利になるんじゃね?」と安易な気持ちでpuppeteer-rubyを作り始めた。
あらかじめ書いておくと、この記事を書いている2020/01/31時点では未完成。たぶん1年以内に完成されることはないだろう。
とりあえず、挫折したなりに、勉強になったこととか苦労させられたところとかは共有しておこうと思う。
WebSocketをRubyで使う
多くの言語では標準でWebSocketを使うライブラリがあるが、Rubyには無い。
2019年末時点で、現実的なライブラリとしては
- faye-websocket GitHub - faye/faye-websocket-ruby: Standards-compliant WebSocket client and server
- その子分の websocket-driver GitHub - faye/websocket-driver-ruby: WebSocket protocol handler with pluggable I/O
- async-websocket GitHub - socketry/async-websocket: Asynchronous WebSocket client and server, supporting HTTP/1 and HTTP/2 for Ruby.
あたりだろう。
ソースコードが読みやすいのは圧倒的に async-websocket なんだけど、async-websocketは asyncっていうライブラリに依存していて、こいつがなかなかに難読だ。いっぽうwebsocket-driverはソースコードこそ若干いまいちながら、ベースは歴史あるEventMachineだ。
chrome-remoteっていうCDPクライアントのGemがwebsocket-driverを使っていたこともあり、今回はwebsocket-driverを使った。
async/awaitを攻略する
Puppeteerはasync/awaitをとてもヘビーに使っている。API仕様書 を見てもわかるように、ほとんどの主要なAPIはasync functionでPromiseを返すものだ。
いっぽうでRubyはというと、言わずもがな、並列処理の仕組みはそんなにリッチではない。
たとえば、browser.launchくらいなら、他に並列でやりたいことは無いのでRubyでも普通に def launch(...)
で同期な関数定義してしまえばいい。
ただ、困ったのが、WebSocketのメッセージを受けて動くところだ。ブラウザの準備完了を待ちつつ、タイムアウトを設定しつつ、みたいなのをPromiseを使いまくっているのがPuppeteerの元のコードだ。
自分の実装力低さゆえ、ページの読み込み完了を待ちつつ、いくつか準備のためのWebSocketメッセージをやり取りしないといけないところで、完全にデッドロック起こしてしまって死んだ。
# Alternative implementation of #lifecyclePromise. def wait_for_lifecycle @wait_for_lifecycle ||= (@wait_for_lifecycle_queue ||= Queue.new).pop end
十中八九、ここで Queueの popをしてスレッドをブロックしてしまってるのが原因。なんだけど、Async だったり concurrent-ruby だったりになんとなく頼ってしまうのも負けかなーなんておもって、結局身動き取れないまま、情熱が冷めて今に至る。
気分転換に puppeteer_firefox-dartを作ることにした
Dartがやっぱり最高なので、puppeteer-firefoxをDartに移植しようかなと思った。Firefoxでしか動かないレガシーなサービスが自分の身近にあるので、それをDartで自動操作するプログラム書けば、シングルバイナリで実行とかもできて便利だろうなー。そもそもDartだったら型がしっかりあるから書きやすいだろうなー。みたいなのを狙っている。
https://github.com/YusukeIwaki/puppeteer_firefox-dartgithub.com
とはいえ、そうこうしている間にMicrosoftが playwright とかいうOSSを出してきたり、Puppeteerが公式でFirefoxのサポートしたぞってアナウンスしていたり、
In collaboration with Mozilla, we’re proud to announce Puppeteer v2.1.0! 🔥
— Chrome DevTools (@ChromeDevTools) 2020年1月28日
The new version works with Chromium 80 and now supports official Firefox binaries as well.https://t.co/fq5FFvAFJS
puppeteer-firefoxにとっては逆風なできごとがここ1ヶ月で結構起きています。近未来にDeprecatedにされそうな勢いです。
とはいえ、Firefoxでしか動かない謎システムは直近1年間では大きく変わらないでしょうから、なんだかんだで古くなっても自分はpuppeteer-firefoxを作って使って行くことになると思います。
(2020.12.02 追記 : add Firefox support by YusukeIwaki · Pull Request #125 · xvrh/puppeteer-dart · GitHub puppeteer-dart にPulll Request出しました)
まとめ
結局のところ、いかに情熱をキープできるかが大事だということ。
(追記: 開発再開してます→ やっぱりPuppeteerをRubyから使えないと困るので、puppeteer-rubyを作ることにした - YusukeIwakiのブログ )
(追記2: 調子に乗ってplaywrightもRubyから触れるようにしてみてますw→ ブラウザ自動操作のPlaywrightはRubyからでも使える? - YusukeIwakiのブログ )