PuppeteerをRubyから直接触れるGemを作ろうとして挫折した

まえおき

(追記: 開発再開してます→ やっぱりPuppeteerをRubyから使えないと困るので、puppeteer-rubyを作ることにした - YusukeIwakiのブログ )

github.com

Puppeteerはすごい。「自動化はSeleniumで十分じゃね?」と思ってた自分だが、Puppeteerには完全に魅了されてしまった。

puppeteerはJavaScriptで書かれているライブラリなんだけど、Dartで動くようにしてるすごい人がいる。C#とかPythonで動くようにしている人もいる。

「・・・あれ、Ruby版は?」

そう、ないのだ。

Rubyは日頃の業務で使ってるからPuppeteer使えたら何か便利になるんじゃね?」と安易な気持ちでpuppeteer-rubyを作り始めた。

github.com

あらかじめ書いておくと、この記事を書いている2020/01/31時点では未完成。たぶん1年以内に完成されることはないだろう。

とりあえず、挫折したなりに、勉強になったこととか苦労させられたところとかは共有しておこうと思う。

WebSocketをRubyで使う

多くの言語では標準でWebSocketを使うライブラリがあるが、Rubyには無い。

2019年末時点で、現実的なライブラリとしては

あたりだろう。

ソースコードが読みやすいのは圧倒的に 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メッセージをやり取りしないといけないところで、完全にデッドロック起こしてしまって死んだ。

puppeteer-ruby/lifecycle_watcher.rb at 80a848417faf312f66a55410613ff3058c8f7d2f · YusukeIwaki/puppeteer-ruby · GitHub

  # 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-firefoxDartに移植しようかなと思った。Firefoxでしか動かないレガシーなサービスが自分の身近にあるので、それをDartで自動操作するプログラム書けば、シングルバイナリで実行とかもできて便利だろうなー。そもそもDartだったら型がしっかりあるから書きやすいだろうなー。みたいなのを狙っている。

https://github.com/YusukeIwaki/puppeteer_firefox-dartgithub.com

とはいえ、そうこうしている間にMicrosoftplaywright とかいうOSSを出してきたり、Puppeteerが公式でFirefoxのサポートしたぞってアナウンスしていたり、

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のブログ