ヘッドレスブラウザでログインしたりパースしたり

## やりたいこと
図書館のWebサイトにログインし、貸出中の図書のタイトルと返却日を取得してみます。
(最終的にはそのデータをGoogleカレンダーに反映させたりリマインドメールを送信したりやSlackに通知したりする予定ですが、ここでは割愛します)

当初PHPでPostリクエストを投げてログインしようとしたのですが、セッションの扱いや動的に発行されるURLのルールがよくわからなかったため、ブラウザ操作を再現できるCasperJSを使うことにしました。

また一連のスクレイピング処理をNode.jsモジュールとして扱うために、CasperJSをラッパー操作できるSpookyJSも利用しています。Spookyの導入方法や簡単な使い方は以前の記事にまとめています。
[SpookyJSでJavaScript有効状態のページをスクレイピング](https://qiita.com/d-mato/items/014f4cf2e902f9c76f97)

## 基本的な流れ

最初のページのURLを読み込んだ後は、フォームの入力やクリックやスクロールをしながら適当に待機条件をはさみつつ、ブラウザ操作を繰り返していきます。

```js:雛形
// 初期URLをロード
spooky.start(url);
// ロード完了
spooky.then(function() {

  // フォーム入力
  this.fill('form', {name: 'Alice'}); 

  // クリック
  this.click('.login'); 

  // 次のthen に移る条件 (.username という要素が現れるまで次に進まない)
  this.waitForSelector('.username');
});


spooky.then(function() {
 
});

spooky.then(function() {
 
});
```

上記のソースを見て頂ければ一目瞭然ですが、CasperJSのAPIは非常に直感的にわかりやすいものになっています。function内の待ち条件 (ここでは`waitForSelector`) がクリアされていたら次のthenに進んでいくだけです。

待ち条件は`waitForSelector`以外にも、特定の文字列が表示されるまで待つ`waitForText`や、特定の要素がappendされるだけでなくvisibleになるまで待つ`waitUntilVisible`など、痒いところに手が届くAPIがいくつもありますし、無名関数で条件を独自に設定することもできます。

また`click`は要素の指定だけでなく座標も指定できるので、ブラウザゲームの自動化なんかもできそうです。

詳しくはCasperJSのAPIドキュメントページをご参照ください。
http://docs.casperjs.org/en/latest/modules/

## 実践

今回の対象は僕が普段よく利用している港区図書館です。
まず全体のソースはこちらです。

```js
import Spooky from 'spooky';
import cheerio from 'cheerio';

const options = {
  child: {
    transport: 'http'
  }
};
const spooky = new Spooky(options, function(err) {
  let url = 'https://www.lib.city.minato.tokyo.jp/licsxp-opac/WOpacTifSchCmpdExecAction.do?tifschcmpd=1';
  spooky.start(url);

  spooky.then(function() {
    this.click('#login');
    this.waitForSelector('form[name="LBForm"]');
  });

  spooky.then(function() {
    account = {
      username: '**user**',
      j_password: '**pass**'
    };
    this.fill('form[name="LBForm"]', account, false);
    this.click('.exec');
    this.waitForSelector('a#logout');
  });

  spooky.then(function() {
    this.click('#stat-lent');
    this.waitForSelector('table.list');
  });

  spooky.then(function() {
    this.emit('body', this.getHTML());
  });

  spooky.run();
});

spooky.on('body', function(body) {
  let $ = cheerio.load(body);
  let books = [];
  $('.list tbody tr').each(function(i, tr) {
    let book = {
      title: $('td', tr).eq(0).text().trim(),
      returnDate: $('td', tr).eq(4).text().trim()
    };
    books.push(book);
  });

  console.log(books);
});

```

``const spooky = new Spooky(options, function(err) { ``以下を順番に解説します。

### 初期ページ読み込み

```js
  let url = 'https://www.lib.city.minato.tokyo.jp/licsxp-opac/WOpacTifSchCmpdExecAction.do?tifschcmpd=1';
  spooky.start(url);
```

``spooky.start``で指定したURLが読み込まれます。ロードが完了すると次の``then``に進みます。
ちなみに、読み込んでいるページはログインページでは無く検索システムのトップページです。ログインページをいきなり開くと、セッション管理の関係でリダイレクトされてしまうという仕様らしいので、このようにしています。

### ログインページに移動
```js
  spooky.then(function() {
    this.click('#login');
    this.waitForSelector('form[name="LBForm"]');
  });
```
初期ページの読み込みが終わると上記が実行されます。
``#login``という要素をクリックするとページ遷移が発生しますが、``waitForSelector``によって``form[name="LBForm"]``という要素が現れるまで次の``then``に進まずに待機します。

### フォーム入力
```js
  spooky.then(function() {
    account = {
      username: '0095510665',
      j_password: '******'
    };
    this.fill('form[name="LBForm"]', account, false);
    this.click('.exec');
    this.waitForSelector('a#logout');
  });
```

フォームが表示されているはずなので、``fill``でアカウント情報を指定するフォームに入力します。更に``.exec``というボタンをクリックし、``a#logout``という要素が現れるまで待機しています。

### 貸出リストページに移動して貸出図書リストを取得

```js
  spooky.then(function() {
    this.click('#stat-lent');
    this.waitForSelector('table.list');
  });
  spooky.then(function() {
    this.emit('body', this.getHTML());
  });
```

ログイン後はマイページのトップにいるので、``this.click('#stat-lent')``で貸出図書リストページに移動し、``this.waitForSelector('table.list')``でリストテーブルが表示されるまで待った後、``this.emit``でHTML全体を外部に投げています。

あとはイベントリスナーでHTMLを受け取って``cheerio``でパースするだけなので省略します。

---

実質CasperJSの紹介になってますが、SpookyJSでラップしているのでこれをexportすれば他のNodeプログラムと連携できそうです。

## 参考ページ

API Documentation — CasperJS 1.1.0-DEV documentation
http://docs.casperjs.org/en/latest/modules/

SpookyJS/SpookyJS: Drive CasperJS from Node.js
https://github.com/SpookyJS/SpookyJS