2018年2月12日月曜日

lambda + PhantomJS(webdriverIO)でamazon.co.jpをスクレイピング

概要


子ども用でfireタブレットを買ってあげたのは良いけど、いくら注意してもやりすぎてしまう。
あと何かしら言い訳・誤魔化ししようとしているところがムカつくようになった。

調べたところ、amazon.co.jpに購入したfireタブレットを登録するとwebから使用状況が確認できることが分かった。
ただし、毎回確認して子どもに注意するのも面倒なので
定期的にスクレイピングして、家族のLINEグループに通知する仕組みを作ってみた。




要件


・サーバーまで運用したくないことで、lambda限定で実装
・バージョン管理はgithub + serverlessフレームワーク使用
・結果をLINEグループへ通知する。そのために事前に作った通知用のbotアカウントをグループに追加する必要があった。
・fireタブレットは2台。どちらか使用時間が3時間超えたら通知を開始、以後はどちらかが1時間超えたら通知
・1回目は前日の使用時間を表示
・amazon.co.jpへのスクレイピング11:00〜21:00まで1時間1回行う
・勉強など使用しても問題ない履歴まで集計されてしまうので、明細まで表示されるようにする
・webサイトの明細までは表示しない。(息子が見たエロサイトまで表示されてしまうので)


構成図とフロー





課題と解決まで


1. amazon.co.jpサイトのスクレイピングが難しかった。

・始めはcheerio-httpcliで試してみたが、時間を置いて動的に表示されるcssセレクタがあった為、waitを掛けれるheadlessブラウザではないとダメだった。
・lambdaでできる候補としてchromelessとphantomJSに絞った。
・chromelessの方が実装的に楽だが、原因不明のsocketタイムアウトになったため使えなかった。あと、もう少し上手いやりかたがあるかも知れないが、公式に乗っているremote実装だとlambdaを2つに分けて制御部のlambdaからchromeless本体lambdaを呼ぶような大掛かりになりそうなのが嫌だった。
・phantomjsをlambdaで動かせる為にはphantomjs-prebuiltというパッケージが必要だった。幸いサンプルがあったので使わせてもらった。

Node.jsでウェブスクレイピングする色々な方法

ブラウザの自動操作で手軽さを追求したらAWS LambdaとPhantomJSの組み合わせにたどり着いた


2. デプロイしてもphantomjsが含まれない。

phantomjs-prebuiltをlambdaで動かせるためにはphantomjs-prebuiltのinstall.jsを実行して、そのバイナリファイルを含ませる必要があった。ちなみに英語だがマニュアルにもちゃんと乗っていたが見落としたのが痛かった。
install.jsを実行してない場合は既に開発環境でパス指定されているphantomjsのバイナリが実行されることで正常に動くように見えてるが、lambdaにデプロイするとバイナリなしのが実行される為、同然動かない。あと、デプロイしたs3上のzipファイルのサイズもバイナリあり・なしでだいぶ違っていた。

3. phantomJSの作りがいまいち理解できない。webdriverIOを挟むことに。

ネットで調べると、例は乗っていたがchromelessみたいに直感的ではない。webdriverIOを挟むことで、だいぶ似たような作りができた。もしかしたらwebdriverIOを排除することでオーバヘットを減らすことができるかも知れないが、今のままで満足。

AWS Lambda (Node.js 4.3) で PhantomJS + WebdriverIO を使用してヘッドレスWebブラウザを操作する

4. amazon.co.jpではphantomJSブラウザでログインができない。

chromelessだとamazon.co.jpでログイン→スクレイピングができたが、phantomJSだとwebdriverIOでほぼ同じ書き方ができるにも関わらず、ログイン画面で止まって、うまくできない。
試しで止まった時点の画像を取ってみたところ、見慣れないブラウザの画面だった。
もしかしたらamazon.co.jpでは変なブラウザだと弾く仕組みになっているかも?と思い、userAgentをfirefox(Mac)に設定したら上手く進めるようになった。
だが、場所によってはdomの認識ができず、cssセレクタでのwaitForExistなどが使えなかったりするので
格好悪いがwaitで数秒、描画を待つようにした。

5. botアカウントが入っているLINEグループIDを特定する必要があった。

LINEのbotにメッセージを送る際にはbotのLINE IDが必要で、同じ流れでグループになるとLINE GROUP IDが必要。ただし、これは普通に公開してくれるものではなく、特定する為には
botがグループに招待・脱退する際に返されるレスポンスに含まれているIDを見る必要があった。
これもlamdaで作ったが、面倒だった。そもそも、curlでもできたかも。




6. DynamoDB処理が非同期

仕様だから仕方ないけどログイン情報など、必要な情報を取得する為DynamoDBからのget/put処理を全部callbackで実装した。格好悪いけど、このまま放置。余裕がある時にpromisなどでキレイにしたい。

7. DynamoDBではNumber型でもstringに扱うべきらしい

カラム定義をNumberにしたのにも関わらず、数値をputすると下記のエラーになってしまう。

message: 'Expected params.Item[\'usage_minute\'].N to be a string'
 code: 'InvalidParameterType'

しかたなく、stringにcastしている。なかなか納得できない仕様。

DynamoDBでNumber型をputItemするときのエラー

8. lambdaの環境変数へ書き込みができない

認証情報などをlambda実行する度にDBから取得するのも非効率だったので、環境変数を利用することに。
ただし、githubにpushするserverless.ymlに残す訳にもいかないので、初回だけDBから取得して→環境変数へ書き込みをしようとしたが、nodejsからlambdaの環境変数へ書き込みができない。仕様ならしかたないけど、やり方が悪かったかも。

9. lambdaの日付がUTCだった。

nodejsで日付を取得すると+9:00のUTCになってしまう。環境変数にタイムゾーンを設定しないとダメらしい。

TZ: 'Asia/Tokyo'

東京リージョンって意味があるのか?

AWS Lambdaのタイムゾーン変更

10. ReadCapacityUnitsLimit / WriteCapacityUnitsLimitの意味がわからない。

取り敢えず正常に動くようにはなったけど、cloudwatchから見慣れない警告が出るようになった。
調べたところ、キャパシティを計測するためのデータが不足しているせいで出ているぽい?
そもそも、そんなに頻繁にDBアクセスをする訳でもないので、測定データがないのは同然?
という自分なりの推測した。

アラームの設定の「欠落データの処理方法」に何も指定されてなかったので「適正」に変えたら
取り敢えず、警告にはなれなくなった。これで良いのかは分からないが。



結果イメージ




今後の課題

・やる気になったら、callbackにした非同期実装をpromis化する。


その他

・amazon.co.jpでは「コンテンツと端末の管理」情報取得のAPIを提供してほしい。
・LINEは簡単にグループIDを特定できる方法を提供してほしい。
・dynamoDBからテーブル定義をjsonに抽出するコマンド。IAMポリシにあるdynamoDBのDescribeTable権限を付ける必要がある。

table_name=users
aws dynamodb describe-table --table-name ${table_name} > ${table_name}.json


既存のDynamoDBテーブルからテーブル定義を作成する

・fireタブレットのweb使用明細はslikブラウザを開いた時しか取れないらしい。
取り敢えず使用時間だけ知りたいだけだったので問題ないが、あまりにも子どもの利用時間が改善されなかったら家のルータからアクセス履歴を収集して公開する仕組みも考えてみる。


github

https://github.com/mwookpark/amazonJpConsole