PHP と Goutte ではじめる超絶簡単クローラー入門 ログイン編

前回の[PHP と Goutte ではじめる超絶簡単クローラー入門](http://qiita.com/mojibakeo/items/ba38e6709356562b284a)の記事が予想の35倍くらいストックされたので続編を。

今回は Cookie を引き回してログインしたページの情報を~~かっさｒ~~取得してみましょう。

題材はかの有名な SNS プラットフォーム、mixi なんていかがでしょうか。
API が公開されてない(たぶん)足あと一覧を攻略してみましょう。ニックネームでも集めてみましょうか。

# はじめに

前回同様、composer つかってサクッと下地を作りましょう。

```zsh
composer require fabpot/goutte
touch app.php
```

```php:app.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Goutte\Client;

$cli = new Client();

```

このへんまで来たら準備完了ですね。
みなさんバッチリ mixi にログインしてるかと思うのでログアウトしておいてください。

# ねらいをさだめる
ログアウトすると https://mixi.jp/ つまりトップページにリダイレクトされたかと思います。

![スクリーンショット 2015-08-23 7.34.34.png](https://qiita-image-store.s3.amazonaws.com/0/28612/a0f4a734-afc3-e598-743a-718cc0dd70de.png)

(LINE の友達募集コミュニティって時代を感じますね！)

たいせつなのはこちらです。
![スクリーンショット 2015-08-23 7.34.15.png](https://qiita-image-store.s3.amazonaws.com/0/28612/0e52545d-c157-d0e0-5aaa-fb4a5128f17a.png)

ログインフォーム。

こちらにメールアドレスとパスワードを挿入して submit したいわけです。

```php:app.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Goutte\Client;

$cli = new Client();
$top = $cli->request('GET', 'https://mixi.jp');
$loginForm = $top->filter('form')->form();
$loginForm['email']    = 'mojibakeo@example.com';
$loginForm['password'] = 'PASSSWOOOORDDD';
$cli->submit($loginForm);
```

フォームに狙いを定めて form() すると [Symfony\Component\DomCrawler\Form](http://api.symfony.com/master/Symfony/Component/DomCrawler/Form.html) オブジェクトが取得できるのであとは簡単。
email と password の値をセットして `$cli->submit()` の引数に渡すだけ。
これだけで `$cli` にログイン状態が有効な Cookie がセットされるので、こちらを使い回せばログインユーザーとしてアクセスできます。なにこれちょうべんり。

# ためしに自分のニックネームを取得してみる

ログイン済みページ右上にあるこれが手っ取り早そうです。

![スクリーンショット 2015-08-23 7.51.46.png](https://qiita-image-store.s3.amazonaws.com/0/28612/e7e6558b-df56-679e-0455-49fbb8aad5e7.png)

一番最初の `.nickname` がこの要素にあたるようなので以下のように書けますね。

```php:app.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Goutte\Client;

$cli = getLoggedInClient();
$home = $cli->request('GET', 'http://mixi.jp/home.pl');
echo $home->filter('.nickname')->first()->text();

// 見やすさのためにログイン部分を function 化
function getLoggedInClient() {
    $topUrl = 'https://mixi.jp';
    $cli = new Client();
    $top = $cli->request('GET', $topUrl);
    $loginForm = $top->filter('form')->form();
    $loginForm['email']    = 'mojibakeo@example.com';
    $loginForm['password'] = 'PASSSWOOOORDDD';
    $cli->submit($loginForm);
    return $cli;
}
```

どうでしょう。ニックネームが表示されたのではないでしょうか。

# 足あとページにアクセス

そしてついに本題です。ニックネームなんぞ API 経由で取得するべきですが、足あとに関しては公開されていません(ぱっと見)。

![スクリーンショット_2015-08-23_7_59_23.png](https://qiita-image-store.s3.amazonaws.com/0/28612/4d369863-8c26-1817-1779-e1d69502555b.png)


**総アクセス数が切ないという衝撃事実** はさておき、このようなページ構成になっております。

Chrome Developer Tools で確認したところ、このような DOM 構造になっていました。
![スクリーンショット_2015-08-23_8_01_20.png](https://qiita-image-store.s3.amazonaws.com/0/28612/4096531b-b443-e0f5-1c95-5ebc9621cb58.png)

足あと表示部分が .logListAre.visitorList で、ニックネームが .nickname なのでこのように絞り込めばさくっとニックネームを集められそうですね。

```php:app.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Goutte\Client;

$cli = getLoggedInClient();
$visitor = $cli->request('GET', 'http://mixi.jp/list_visitor.pl');
$visitorList = $visitor->filter('.visitorList');

$visitUsers = [];
$visitorList->filter('.nickname')->each(function($nickname) use (&$visitUsers) {
    $user = trim($nickname->text());
    // $visitUsers に存在しないニックネームだけ追加
    if (!in_array($user, $visitUsers)) {
        $visitUsers[] = $user;
    }
});

print_r($visitUsers);

function getLoggedInClient() {
    $topUrl = 'https://mixi.jp';
    $cli = new Client();
    $top = $cli->request('GET', $topUrl);
    $loginForm = $top->filter('form')->form();
    $loginForm['email']    = 'mojibakeo@example.com';
    $loginForm['password'] = 'PASSSWOOOORDDD';
    $cli->submit($loginForm);
    $cli->getCookieJar()->all();
    sleep(2);
    return $cli;
}
```

こんな感じで足あとつけたひとのニックネーム一覧が取得できるはずです。

# 2ページ目(mixi プレミアムなユーザーならそれ以上)も取得したい

普通に for でまわして

```php
// 抜粋
$visitor = $cli->request('GET', 'http://mixi.jp/list_visitor.pl?page=$i');
```
のような形でも実現できるのですが、これだと getLoggedInClient が2回も呼ばれてしまいます。
それはつまり1ページから取得したいだけなのに2ページ分のアクセス、10ページなら20アクセスになってしまいますね。
裏口からこそっと行ってるなりに、お行儀よくしたいところです。

## Cookie 情報をキャッシュする
現時点でも相当に都合の良いオンナ感ある Goutte ですが、以下の一行だけで Cookie を取り出すことができます。

```php
$cookies = $cli->getCookieJar()->all();
```

そして以下の1行で取り出した Cookie をセットすることができます。

```php
$cli->getCookieJar()->updateFromSetCookie($cookies);
```

なんてつごうのいい子…(言いたいだけ)

## Cookie を使いまわして省エネアクセスする

まずこれまで使ってた `getLoggedInClient` を超雑に Cookie 使い回し対応します。

```php
function getLoggedInClient(&$cookies = null) {
    $cli = new Client();
    if (!is_null($cookies)) {
        $cli->getCookieJar()->updateFromSetCookie($cookies);
        return $cli;
    }
    $topUrl = 'https://mixi.jp';
    $top = $cli->request('GET', $topUrl);
    $loginForm = $top->filter('form')->form();
    $loginForm['email']    = 'mojibakeo@example.com';
    $loginForm['password'] = 'PASSSWOOOORDDD';
    $cli->submit($loginForm);
    $cookies = $cli->getCookieJar()->all();
    sleep(2);
    return $cli;
}
// 参照渡しやめろっていうのはごもっともで、普通にやるならクラス化して protected/private な $cookies プロパティを定義して使いたいですが本題からそれるので今回はこれで勘弁してください
```

これで関数側の準備ができましたね。
あとは1ページ目だけを取得するコードをちょろっと書き換えるだけです。

```php:app.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Goutte\Client;

$cookies = null;
$visitUsers = [];
for ($i = 1, $last = 2; $i <= $last; $i++) {
    $cli = getLoggedInClient($cookies);
    $url = sprintf('http://mixi.jp/list_visitor.pl?page=%s', $i);
    $visitor = $cli->request('GET', $url);
    $visitorList = $visitor->filter('.visitorList');

    $visitorList->filter('.nickname')->each(function($nickname) use (&$visitUsers) {
        $user = trim($nickname->text());
        // $visitUsers に存在しないニックネームだけ追加
        if (!in_array($user, $visitUsers)) {
            $visitUsers[] = $user;
        }
    });
    sleep(2); // 高速連続アクセスとか迷惑なので sleep しましょう
}

print_r($visitUsers);

function getLoggedInClient(&$cookies = null) {
    $cli = new Client();
    if (!is_null($cookies)) {
        $cli->getCookieJar()->updateFromSetCookie($cookies);
        return $cli;
    }
    $topUrl = 'https://mixi.jp';
    $top = $cli->request('GET', $topUrl);
    $loginForm = $top->filter('form')->form();
    $loginForm['email']    = 'mojibakeo@example.com';
    $loginForm['password'] = 'PASSSWOOOORDDD';
    $cli->submit($loginForm);
    $cookies = $cli->getCookieJar()->all();
    sleep(2)
    return $cli;
}

```

mixi プレミアムなひとが一体何ページ目までアクセスできるのか知りませんが、`$last` の値を変えてあげれば好きなページ数までクロールできそうですね。
でも高速連続アクセスとか超迷惑なので sleep しましょう。お行儀よく。


# さいごに
Facebook が好きです。でも、mixi の方がもっと好きです。

だからみんなもっと mixi つかおう(提案)