PhantomJSでmixiにログインしてブラウザ三国志mixi版の画面をキャプチャする

# 目的
ブラウザ三国志mixi版へアクセスし、ゲーム内地図 1601*1601マスのデータを収集し、MongoDBへ格納する。また、各マス1ピクセルとした全体画像(.PNG)を生成する。

※ タイトルにある「画面のキャプチャ」はエラー検出時にPhantomJSのrenderを使って保存する際に使われているだけで、この記事の本旨はブラウザゲームの画面からデータを採取するところにあります。

# 動作環境
- [さくらのVPS](http://vps.sakura.ad.jp/) (1G)
- [PhantomJS](https://github.com/ariya/phantomjs)
- [Node.js](https://github.com/joyent/node)
  - [canvas](https://github.com/Automattic/node-canvas) 描画ライブラリ
  - [mongoose](https://github.com/LearnBoost/mongoose) MongoDB オブジェクトモデル
  - [cron](https://github.com/ncb000gt/node-cron) 定時イベント発行
  - [async](https://github.com/caolan/async) 非同期処理のフロー制御サポート
  - [pm2](https://github.com/Unitech/pm2) プロセスマネージャ

以上のように何にも新規性がない。

[ブラウザ三国志](http://www.3gokushi.jp/)は、[株式会社マーベラス](http://www.marv.jp/)が運営しているオンラインゲームで、 開発はジー・モード(旧ONE-UP)でプログラム開発は[株式会社インフィニットループ](http://www.infiniteloop.co.jp/)。
Flashなどプラグインは使用されていない。
Web API等は無く、地図情報はゲーム内の地図ページ内のHTMLから取得するしかない。また、ブラウザ三国志はマーベラスのmoog、Yahooモバゲ、ハンゲーム、mixi等提供会社がそれぞれあり、今回はmixi版へアクセスするのでmixiへのログインが必要となる。ログイン後、ブラウザ三国志ゲームサーバの選択、運営からのメッセージポップアップ対応等、ゲーム画面に至るまでに幾つかのステップをこなさなければならない。その際クライアントサイドでJavaScriptを処理出来るPhantomJSを使うのが適切と考えられる。

# 処理内容
スクリプトは3本。
1. PhantomJS用ブラウザ操作スクリプト
2. 1を定期的に起動するcronをNode.jsスクリプトで記述
3. PhantomJSが出力する地図データファイルを読み込み、MongoDBを参照し前回データから更新のあった地点に対しては所有者情報など更新するスクリプト。同時にこれら情報を1枚のPNGファイルにプロットも行う。

PhantomJSはセッション情報などCookieを保持するなどの各種設定を([config.json](http://phantomjs.org/api/command-line.html))に 設定しておく。

以下、各スクリプト毎の処理内容を紹介する。

## PhantomJS スクリプト
起動後、まずブラウザ三国志ゲーム画面を開いてみて、ゲーム画面が表示されているのであれば処理を続行する。サーバの状態やユーザセッションの状態によって表示内容が異なるので処理を分岐させる。メンテナンス画面が表示されていれば抜ける、ゲームのセッションタイムアウトであれば再度サーバ選択画面に戻ってやり直す。また、そもそもmixiにログインしていない状態であるなら、mixiのログイン画面を開いて自動ログインからやり直す。

おおまかな流れは以下のとおり。

- 状態チェック用に仮のゲーム画面を開く
- ゲーム画面固有の要素をチェックし、存在するなら地図画面採取へ
  - 存在しなければ、mixiの[該当アプリ画面](http://mixi.jp/run_appli.pl?id=6598)を開く
  - ゲームサーバ一覧画面が開くので一覧を取得し、対象のゲームサーバボタンを特定しクリック
    - サーバ一覧画面が表示されていない場合は、そもそもmixiにログインしていないので、mixiのログインプロセスを行ってから最初からやり直し
- ゲームの地図画面が開いていることを確認し、HTML内から地図情報を入手
- 地図データを元に、各マスの情報を1行のJSON化してファイルに追加
- これを全地図範囲に対して行ったら終了

全マップは 1601*1601マスだが、地図の画面は一番大きな地図サイズでも51*51マス表示なので 1024回の地図取得が必要になる。地図1枚あたり500KBのテキストファイルが出力される。JSONなので大きめだが、後の工程でMongoDBに差分が収容されると消去される。地図1ページ毎のPhantomJS処理時間は3秒から時々6秒程度で、全地図を巡回すると1時間弱を要する。

mixi上でブラウザ三国志を起動する際に、mixiアプリに関する認証プロセスが行われており、そこではHTTPS通信が行われていてSSLエラーが発生する場合がある。これを無視する様PhantomJS 設定ファイルを記述しなければならない場合もある。

PhantomJSは、直接いじるのには難しいという意見もあったし、CasperJS等を通して使う方が楽だと言われている。最近はPhantomJSを紹介する記事もあまり見掛けない。しかし、今回のmixiアプリみたいな膨大なページ遷移が起きるサイトを扱っているとき、エラーに対処するのはPhantomJSを直接触った方が解決が速いと思われる。[onResourceError](http://phantomjs.org/api/webpage/handler/on-resource-error.html)、[onResourceReceived](http://phantomjs.org/api/webpage/handler/on-resource-received.html)、[onResourceRequested](http://phantomjs.org/api/webpage/handler/on-resource-requested.html)、[onResourceTimeout](http://phantomjs.org/api/webpage/handler/on-resource-timeout.html) が役に立つ。また、[Selenium](http://www.seleniumhq.org/) & WebDriverの様なテストフレームワークを ブラウザ三国志の地図採りに使うのも大袈裟に思える。というわけでPhantomJSをそのまま使っている。[GreaseMonkey](http://www.greasespot.net/)等のuser.scriptに慣れていればそれほど苦労はないと思うのだけれど。

## Node.js スクリプト
1本は cron的動作をしてPhantomJSをNode.jsのchild_processで呼び出す。シグナルSIGUSR1とSIGUSR2を受けたら次回の発動を抑制したり、即時発動したり出来るようになっている。

もぅ一本は、上掲の地図1枚辺り500KBのJSON行ファイルがtmpフォルダに生成されたらタスクが起動して、各行を読み込んでMongoDBに反映し、かつCanvasにプロットするスクリプト。PhantomJSの巡回速度よりも、こちらの処理の方が遅いので、PhantomJSのクロールが進むにつれ、次第に一時ファイルが溜まってゆき、巡回後もこちらの処理は続く。

この2本はpm2で管理されており、`pm2 logs` でログ出力が1画面で見られる。`pm2 ilogs` が最近追加されたけれど、あまり利用していない。

# 運用状況
2013年7月から、mixiの第1ワールドの地図採取を続けている。

ブラウザ三国志運営側の新機能追加等でHTML記述内容の変更に対応したり、mixiのログイン画面デザインの変更等に対応したりする以外では問題なくデータ採取が出来ている。ゲームは半年くらいで1フェーズを終了するので、運用も3期目になった。ゲーム期毎のMongoDBには2GB程のデータが保持されており、全てのマス毎に所有者の変遷が記録されている。

* * *
全図はこんな感じです。ゲーム自体は殆どやってないのですが、日々生成されるこの図を眺めるのが趣味みたいになってます。
![map_all.png](https://qiita-image-store.s3.amazonaws.com/0/20860/80c4e6e4-4fcb-8122-2a7a-833099a4f713.png)

MongoDBの蓄積データも紹介。
現時点で所有者の入れ替わりが最も多い土地を検索してみます。

```bash:mongo
> db.lands.aggregate([{$group:{_id:null, max:{$max: "$__v" }}}]);
{ "_id" : null, "max" : 54 }
> db.lands.find({__v:54},{_id:1, __v:1})
{ "_id" : "679,15", "__v" : 54 }
> db.lands.findOne({_id:"679,15"});
...
```
`__v`はレコードの更新版数で、レコードの更新毎にインクリメントされています。今回のDBの使い方ではこれが素直に土地の所有者更新回数に該当します。

更新回数の最大値が54である事がわかったので、`__v`が54である土地の`_id`を検索すると、1件該当がありました。その座標(679,15)、現在は空き地になっています。これは同盟間戦争の跡でしょうか。

```json:json
{
        "_id" : "679,15",
        "alliance" : "",
        "prevuser" : "タビー",
        "username" : "",
        "population" : 0,
        "caption" : "空き地",
        "changed" : ISODate("2014-10-11T15:50:51.050Z"),
        "history" : [
                {
                        "_id" : ObjectId("540ef75a97b6345b1800440c"),
                        "username" : "",
                        "alliance" : "",
                        "date" : ISODate("2014-09-04T23:32:22.570Z"),
                        "type" : "vacant_land"
                },
                {
                        "_id" : ObjectId("540f83d297b6345b1800b2e0"),
                        "username" : "翔勝",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-09T12:46:57.955Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5410107197b6345b1801067e"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-09T22:44:58.203Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541048df97b6345b18012ae3"),
                        "username" : "たまたまちゃん",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-10T08:44:08.852Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5412af1997b6345b18029e12"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-10T12:45:53.469Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5415d00697b6345b18046784"),
                        "username" : "ルチアーノ",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-12T08:28:47.994Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5416d107cc8e921e5c0073c0"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-14T17:24:51.511Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5418b2d99be8c1f10a004170"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-15T11:37:18.702Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5419c4629be8c1f10a0072f9"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-16T21:48:15.432Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5419fe47b51ba21017004454"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-17T17:24:27.559Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541add4dd55655ce5f0051f0"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-17T21:29:38.264Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541b4d05d55655ce5f008710"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-18T13:22:18.207Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541ba140d55655ce5f00a52c"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-18T21:20:45.645Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541e4768d55655ce5f01bfed"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-19T03:21:18.711Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541e7eb3d55655ce5f01d612"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-21T03:34:11.200Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("541f4128d55655ce5f022ba0"),
                        "username" : "tatsuyah",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-21T07:29:36.294Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54215aecd55655ce5f02eece"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-21T21:19:29.379Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542191d7d55655ce5f030a13"),
                        "username" : "tatsuyah",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-23T11:34:53.093Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5425878cb9e32d073d008b1b"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-23T15:28:25.216Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5425bdc4b9e32d073d00a5f1"),
                        "username" : "タンメン",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-26T15:34:34.289Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542843a7327380150a00552f"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-26T19:24:35.106Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5428ef9f327380150a007d31"),
                        "username" : "タンメン",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-28T17:20:43.015Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54296081327380150a009b75"),
                        "username" : "だつえばん",
                        "alliance" : "だつえばん@よりいち",
                        "date" : ISODate("2014-09-29T05:33:43.631Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5429d0bf327380150a00be3c"),
                        "username" : "タンメン",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-29T13:31:57.525Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542a7975327380150a00d602"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-29T21:21:34.745Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542acea7327380150a00f7da"),
                        "username" : "タンメン",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-09-30T09:33:24.312Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542bcad2327380150a0139a5"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-09-30T15:35:16.555Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542c3f0d327380150a015d37"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-01T09:33:06.790Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542c901b327380150a016ec5"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-01T17:43:11.428Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542cfd82327380150a01886a"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-01T23:32:19.160Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542d51ef327380150a019ebf"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-02T07:22:25.564Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542dc3c3327380150a01bc8d"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-02T13:22:10.572Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542de193327380150a01c1f7"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-02T21:27:57.969Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542e6dd4327380150a01e0da"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-02T23:33:48.446Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542ea3d0327380150a01ee8a"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-03T09:33:09.019Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542ef8b2327380150a020928"),
                        "username" : "tatsuyah",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-03T13:22:42.861Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542f4bf6327380150a021b6d"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-03T19:22:14.232Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542f6b2a327380150a022266"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-04T01:21:42.496Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("542fa097327380150a02322c"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-04T03:32:20.491Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5430652a327380150a026c4f"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-04T07:21:20.895Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5430bc62327380150a02806e"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-04T21:22:48.025Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54314666327380150a02aaab"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-05T03:34:46.249Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54316343327380150a02b472"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-05T13:23:47.272Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5431b6d7327380150a02c9d3"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-05T15:24:38.578Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5431f22d327380150a02d38a"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-05T21:22:20.537Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5432d156327380150a030c62"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-06T01:36:01.789Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("543326d5327380150a031d93"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-06T17:21:59.839Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5433ce5d327380150a0336f4"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-06T23:22:13.101Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5434093a327380150a0346ce"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-07T11:27:09.223Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5434789f327380150a0360d2"),
                        "username" : "tatsuyah",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-07T15:36:32.591Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("5434b042327380150a036bcf"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-07T23:33:40.213Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54352103327380150a0382db"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-08T03:31:29.250Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("54353e69327380150a0389b4"),
                        "username" : "舞舞",
                        "alliance" : "内田一家総本部",
                        "date" : ISODate("2014-10-08T11:29:12.118Z"),
                        "type" : "territory"
                },
                {
                        "_id" : ObjectId("543956e52a93de043e0050b4"),
                        "username" : "タビー",
                        "alliance" : "綺羅星♪",
                        "date" : ISODate("2014-10-08T13:34:52.775Z"),
                        "type" : "territory"
                }
        ],
        "nearFort" : "",
        "material" : [
                0,
                0,
                0,
                2
        ],
        "power" : 1,
        "loc" : [
                679,
                15
        ],
        "type" : "vacant_land",
        "__v" : 54
}
```