Azure FunctionsでPlaywrightを使ったWebスクレイピング

この記事は、Serverless Advent Calendar 2020 1日目の記事です。

qiita.com

アドベントカレンダーの12月がはじまりましたが、みなさんどれくらい読んでますか?

私も情報収集に使っているモヒカンSlackでは、毎年 #advent-calendar-2020 などといったチャンネルがあり、気が向いた人がAdventarQiita Advent CalendarのカレンダーRSSフィードのURLを登録し、流れてくる記事を毎年楽しんでいます。

あ、モヒカンSlackというのは共有RSSリーダーのように使える公開Slackワークスペースで、Vulsでおなじみのサウナおじさんkotakanbeが運用しています。1万7千人くらいが登録していて、340のチャンネルでテーマ毎に様々なRSSフィードが流れてきます。無料プランということで1週間で10,000メッセージを使い切ってどんどん古い情報は見えなくなるので、古い情報は割り切りましょう!

qiita.com

さて、その昔はAdventarやQiitaのカレンダー一覧を見ながら、温かみのある手作業でRSSフィードを登録していたのですが、ちょうどこの記事が流れてきたので、Azure Functionsの上でPlaywrightを使ったバッチ処理を動かして、Webスクレイピングで全部のアドベントカレンダーを取ってきてSlackに登録するようにしました。

anthonychu.ca

ソースコードはここで公開しています。

github.com

Webスクレイピングには、URLから取得したHTMLをパースして欲しい情報を抽出するものと、ブラウザをHeadless(画面無し)で起動してAjaxなどを動かした結果からDOMなどを経由して抽出するものがあります。今回は、AdventarがgRPC APIを使っていて面倒くさい、SlackがレガシーAPIでないとスラッシュコマンドを投げられないなどいくつかの理由があり、Headlessブラウザを使うことにしました。

この手のHeadlessブラウザの操作ライブラリでは、Chromiumの開発者が作っているPuppeteerが定番かなと思いますが、FirefoxやWebKit(Safari)にも利用できるPlaywrightを今回は使ってみました。

つくりかた

この手のWebスクレイピングは、とにかくローカルで試行錯誤しながら組み立てていくことが多いかと思います。今回は、最初にベタなスクリプトとして動く部分を作り上げてから、それをAzure Functionsの関数アプリに変換するという手順を踏みました。開発環境としてはWSL2上のUbuntuにVSCodeで接続しています。VcXsrvを併用すると画面を使うアプリも動かせるので、一時的にHeadlessを解除して実際のブラウザ画面を見ながら試すこともできます。

動くスクリプトができてしまえば、host.json、functions.json、index.jsの三つを用意することで関数アプリとしてそのままデプロイできるようになります。対応するコミットがこれです。

github.com

いったんHTTP APIとして動作確認し、それからタイマートリガーに設定変更しました。これはfunction.jsonの変更だけでいけます。

github.com

これで10分に1回、スクレイピングして自動でSlackにRSSフィードのURLを足りない分だけ登録してくれます。

はまったところ

これだけ書くとさらっと動く感じですが、詰まるところが無いわけでは無いです。

1. Headlessを解除したときにダイアログが出ると操作ができない

動作確認のためにHeadlessを解除できる(lib/AdventCalendarCrawler.jsのL128あたりのheadless: false)のですが、Slackはブラウザで開くとデスクトップアプリを開くよう誘導され、Chromeのモーダルダイアログが開いてしまいます。この状態だとダイアログを閉じるまでウェブページ内の操作ができないため、後続の処理が失敗してしまいます。

dialogイベントをハンドリングしても上手くいかなかったので、とりあえず動作確認中はダイアログが出たらすぐに閉じるという運用でカバーしました。

2. Playwright内のchromeインストール場所

上の記事をよく読めばきちんと書いてあるのですが、Playwrightはnpm install中にChromeのバイナリをインストールしてくれます。してくれるのですが、普通に入れるとnode_modulesの下ではない場所にインストールしてしまうので、環境変数PLAYWRIGHT_BROWSERS_PATH0に設定する必要があります。

また、デプロイ時も、VSCodeからデプロイするなら.vscode/settings.jsonを、コマンドラインなら--build remoteを付けることでリモートでビルドが走り、その中でChromeのインストールが走ります。そのためデプロイに3分間ほどかかりますが待ってください。

3. Azure Functionsで動かしたらタイムアウトする

ローカルでは普通に動いていたんですが、じゃあいざAzure Functionsの関数として動かしてみたら見事にタイムアウトしました。まあ、こういうこともあるということで。

ちなみにAzure Functionsの従量課金プランでは一つの関数呼び出しの最大実行時間は10分間なので、それを超えると強制終了されます。試した範囲では5分ぐらいで終わっていたので、今回の範囲ではまあなんとかなるかなと。もしこれを超えるようであれば、処理を分解してDurable FunctionsでFan-outしたりする必要がありそうです。

4. Slackがクロールしてくれない

これは今回作ったものの問題ではないのですが、Slackに一度に大量の/feed subscribeを投げたせいか、今朝実際にフィードを登録したのですがアドベントカレンダーの初日の記事がまだSlackのチャンネルに流れてきていません。

設定では登録されているので大丈夫だとは思うのですが、最終的な確認ができていないのでまだ不安です……。

追記:日付変わったら来始めました。おそらくRSSフィードの時刻が0時になっていたために最初のエントリの検知に失敗していたように見えます。

まとめ

というわけで、Webスクレイピングができるとちょっとした自動化がささっとできるようになるので、手札として持っておくと便利です。またこういう雑なバッチ処理を適当に動かすのにFaaSは極めて強力なので、合わせ技で使っていくと良いかなと思います。