読者です 読者をやめる 読者になる 読者になる

そんな今日この頃でして、、、

コード書いたり映画みたり。努力は苦手だから「楽しいこと」を探していきたい。

CasperJSで動的ページをスクレイピング、あるいは毎月100円ゲットする方法について

厳密にはクローリングが任意のサイト上からHTML等のデータを取得する方法、スクレイピングが取得されたデータから必要な情報を抽出する方法らしいので言葉の用法としては怪しいけど、まあそこは気にせず。


CasperJSとは、JavaScript APIにより操作できるヘッドレスなWebKitブラウザであるところのPhantomJSをより簡潔な記述により操作可能にしたユーティリティである。

スクレイピングといえば古くは各言語のウェブアクセスライブラリ(PerlでいうLWPとかMechanize系とか)を用いていたが、これらはリクエストされたページのHTMLを静的な文字列として取得するだけであり、Ajax全盛な昨今のJavascriptを用いた動的に生成される要素を解析することはできなかった。

(↓そういや前にLWP使ったスクレイピングしてたこっちでも、それと思しき問題にぶつかった)


そんなわけで各所でWebkit等のレタリングエンジンを内部的に使用して動的なページを解釈するライブラリやツールなんかが出てくるのだが、その一つがJavascriptにより記述が可能なPhantomJS。

PhantomJS自体は様々なことができる反面で、実際にページを巡る動作をさせるまでには色々と面倒な記述を要するのだが、CasperJSはそのあたりをラッピングして簡単な記述で使うことができるようになっている。

CasperJS, a navigation scripting and testing utility for PhantomJS and SlimerJS

本来はページのテストしたりとかするものなのだが、今回はこれを悪用使用して某書籍通販サイト(規約読む限りは問題なさそうだけど、ネタがネタなんで具体名は伏せておく)の「あしあと」ボタンを押させてみた


概要

今回対象としたサイトでは、毎日サイトにアクセスして「あしあと」ボタンを押すことで、購入時に使用できるポイント1ptが付与されるサービスがある。

そして連続日数に応じてボーナスがあり、一ヶ月皆勤なら100円分にあたる100pt付与される

毎月コンスタントに何冊か注文する身には小さい額面ながらも紙書籍で割引の効くありがたいサービスではあるが、これが毎日となると意外と押し忘れてしまう。

そこで今回、CasperJSによりログイン→あしあとページへ遷移→あしあとボタンをクリックまでの動作をするスクリプトを組んでみた。


CasperJS導入

Installation — CasperJS 1.1.0-DEV documentation

色々と導入方法はあるが、今時のnode.jsが入ってる環境ならnpmで入れるのが楽。

$ node install -g phantomjs
$ node install -g casperjs

CasperJSの使い方

公式のドキュメントが結構充実しているのがありがたい。

Quick Start(Quickstart — CasperJS 1.1.0-DEV documentation)やAPI Document(API Documentation — CasperJS 1.1.0-DEV documentation)を読めば容易に雰囲気はつかめると思う。 (英語くっそ苦手だけどこういうのは割と読めるのよね)


とりあえずは最低限

  • var casper = require("casper").create()で作成して使いまわす
  • casper.echo(STRING)で標準出力
  • casper.start(URL, FUNCTION)でページを開く
  • casper.fill(SELECTOR, {NAME1: VALUE1, NAME2: VALUE2…}, SUBMIT?)でフォームへの入力
  • casper.click(SELECTOR)で要素クリック
  • casper.evaluate(FUNCTION)でページ内のjavascriptを動作させる(スコープもページ内のものになる)
  • casper.capture(IMG_FILE)でスクリーンショット
  • 最後に.run()で実行

あたりを把握しておけば大丈夫。

コマンド体系もわかりやすくてthenOpenみたいにバリエーション系も容易に挙動が想像できるのが良い。


レッツトライ

事前調査

casperJSでは各種コマンドはCSSパスでの記述が可能。

そんなわけでChromeのディベロッパーツール(もはや完全にFirebugの立ち位置食っちゃいましたね)あたりを使って調べる。

今回のサイトの場合、

  1. ログインページにおけるフォームのCSSパス → #pbBlock33398 > form
  2. ログインフォームの入力要素のname → それぞれdy_lginIddy_pw
  3. あしあとページのボタンのCSSパス → div.stBoxBorder04 > p > a

であった。


とりあずベタにやってみる

(ちなみにサイトドメインは伏せてる)

//footmark.js
var casper = require("casper").create();

//ログイン画面
casper.start("https://*****/reg/login.html", function(){
  this.echo("ログイン画面");
  
  //ログイン
  this.fill('#pbBlock33398 > form', { dy_lginId: 'MY ACCOUNT', dy_pw: 'MY PASSWORD' }, true);
  
  this.capture('login.png');
});

//あしあとページ開いてあしあとボタンクリック
casper.thenOpen(" https://*****/my/account/point/footmark.html', function(){
  this.echo('足あとページ');
  
  //ボタンを押す
  this.click('div.stBoxBorder04 > p > a');
  
  this.capture('before.png');
});

//比較用にクリック後のページも
casper.then(function(){
   this.capture('after.png');
});

casper.run();


さっそく実行!

$ casperjs footmark.js


ちゃんとechoされてるし終わってくれた。

そんなわけでブラウザから確認に行くと・・・

あれ?できてない


キャプチャを見た限りだとあしあとページまでのアクセスは正しく行われていそう。

ボタンのクリックに関しても、パス間違いならエラー出るはずなので、そうなるとクリック時の挙動の問題なのかもしれない。

該当のボタンは

  • aタグ
  • hrefにjavascriptのコマンドが記述されている
  • 該当のコマンド自体は外部のjsファイルを呼んでいるが、httpsでありブラウザ上からだと何をやっているか確認できない

という感じ。

aタグ&hrefにコマンド記述という形式自体は自宅サーバにテストページを立ち上げてcasperJSで叩いた限りではちゃんと動きそうなので、呼ばれるコマンドに何かしらの問題があるのだろうか?


試行錯誤

本来はスクリプトを読んで逆算してく所なのだが、呼ばれている外部のjsファイル自体は認証がかかっていてブラウザから直接読むことができないので、推測で対応していくしかない。

  1. ロード時間の問題なのではと考えwait()系を使ってみる
  2. UserAgentを一応設定してみる

等々やってみたが、ダメ。

もしかしたら何かしらの不正防止手法があるのかもしれない。


解決

そんなわけで色々やってみたが、結局evalute内で動作させることで解決できた。

きっとロード後に何かしらのJavascript動作があって初めて有効になるようなフラグでもあるのだろう。

パターン1: evalute内でクリックさせる

  ////あしあとページを開いた上で
  this.evaluate(function() {
    //クリックイベント作成
    var click_event = document.createEvent('MouseEvents');
    click_event.initEvent('click', false, true);
 
     //足あとボタン取得
     var footmark_btn = document.querySelector("#pbBlock71830 > div.stBoxBorder04 > p > a");
     
     //押下
     footmark_btn.dispatchEvent(click_event);
  });

パターン2: evalute内でクリックと同等のコマンドを叩く

  ////あしあとページを開いた上で
  this.evaluate(function() {
    //足あとボタンのaタグhref要素に設定されているコマンド文字列を取得
    var cmd = document.querySelector("div.stBoxBorder04 > p > a").getAttribute('href');
    
    //実行できるように「javascript:」を削る
    cmd = cmd.replace("javascript:", "");
    
    //取得したコマンド実行
    eval(cmd);
  });

好みの問題で後者を採用した。

前にコードのclickあたりを置き換えて実行すると・・・成功!

以上で要件を満たすことができた!



実は何日か運用して様子を見ているともう少し課題があって、たまにログイン失敗してたりボタン押下に失敗してたりするんだけど、これは恐らく完全にロードされる前に動いちゃってるんじゃないかと予想。

真面目にやるならwaitForとかで明示的にロードを待つとか、ちゃんとポイント数をスクレイピングして元の値と比較して再試行するとかやるべきなんだけど、今回のスクリプトは1回1回のリクエスト数もそこまで多くないので、とりあえずcronで日に数回余分に動かすことで対応している。


(今にしてみると若干恥ずかしい話ではあるけど)中学生時代にアングラ本やアングラサイトを読みあさってたタイプの人間としてはこういう目的があった方がワクワクするし、また防御側の手法も学べて面白い。

とはいえ本来はUIテストとかするものですけん。

Instant Testing with CasperJS

Instant Testing with CasperJS


アングラといえば↓のサイトの内容とか、今になってようやっとわかるようになってきた。

まだ更新されているようで、何か嬉しい。

なんかKindle本も出ているようで。


余談

過剰なまでに高機能な体重計 Withings「Smart Body Analyzer」を買ってみた - そんな今日この頃でして、、、

うちに来てから二週間経ったけど、毎日体重の推移記録をつけていると色々発見があって面白い。

  • 平日のカロリーフルな弁当がアカンのじゃないかと思っていたが、むしろ平日に体重減って休日に戻る傾向がみられる
  • 服装や直前の飲食による誤差も結構馬鹿にならないから、瞬間風速ばかりのオレオレ記録ってあんま意味なかったんだな・・・

あと、そういう記録が出ることで幾らか食欲が抑制される感じが良いなと思う。

割と高い買い物だったけど、今のところ満足感ある。

追記

対象サイトに変更があったようなので対応。

blue1st.hateblo.jp