PHP と Goutte で攻める実用的なクローラアプリケーションの作り方

# はじめに

---


# うれしくなりそうなひと
- クローラかいてるひと
    - 今回は PHP & Goutte だけどあんまり言語関係ない話
- 一度だけじゃなくて継続的にデータを取得し続けたいひと
- 複数人でも困らない程度の清潔さを作っておきたいひと

--- 

# かいてないこと
- こっちよんでください。。
    - [PHP と Goutte ではじめる超絶簡単クローラー入門](https://qiita.com/mojibakeo/items/ba38e6709356562b284a)
    - [PHP と Goutte ではじめる超絶簡単クローラー入門 ログイン編](https://qiita.com/mojibakeo/items/be68498764909a66f126)

--- 

# よくあるつらみ
- なんか動いてない
- 動いてないのどこ。。。
    - エラーログみたけど `hoge.php` のN行目 ってどのページのどの部分だよ
- (おいかける)
- 「ああ、ここね、はいはい確かに構造かわってますね」

--- 

# まじむり

--- 

# せめてどうなってほしいか
- エラーでた瞬間にどのページで異常が起きてるか知りたい
- ***ただそれだけだ***

---

# おちついて考えてみよう

- なぜおれは1ファイル(or 1クラス)にすべて書こうとしたのか
- クローラのクラスに保存とか出力させてるから汚いんじゃないのか

---

# もしかして

- 複数クラスに分けてあげるとクラス名わかるからストレスへるんじゃないのか
- クローラのクラスにはあくまでデータぬくだけの方がいいんじゃないのか
- サービスクラス的なところにデータのハンドリングまかせてみたら全体を俯瞰できそうなんじゃないのか

---

# やってみた
- [mojibakeo/php-crawler-app-sample](https://github.com/mojibakeo/php-crawler-app-sample)
- 書いたもの
    - TOPページのニュース一覧をひろう
    - ログインしてタイムラインをひろう

---

# 構造
```
$ tree app/
app/
├── Crawlers
│   ├── AbstractCrawler.php
│   ├── AbstractLoginClient.php
│   └── Mixi
│       ├── Login.php
│       ├── My.php
│       └── Top.php
└── Services
    └── Mixi
        ├── FetchNews.php
        └── FetchRecentTimeLine.php
```

---

# 使い方
- 必要に応じて `AbstractLoginClient` を継承したログインロジックを書きます
- `AbstractCrawler` に渡すと cookie を引き回してアクセスできます
- `Crawlers` はあくまで取得する部分だけ覚えさせます
- `Services` 以下でハンドリングします

---

# サンプルをさっと眺めましょう

--- 
# ログイン
- ふつうですね

```Login.php
    /**
     * @param Crawler $crawler
     * @return Form
     */
    protected function logic(Crawler $crawler): Form
    {
        $form = $crawler->filter('form[name="login_form"]')->form();
        $form['email'] = $this->user['email'];
        $form['password'] = $this->user['password'];
        return $form;
    }


```

---

# タイムライン取得

```My.php
    protected function logic(array $parameters): array
    {
        $crawler = $this->createCrawler($parameters);
        $newsListNodes = $crawler->filter('.newsList li');
        if ($newsListNodes->count() === 0) {
            return [];
        }
        $news = [];
        $newsListNodes->each(function (Crawler $newListNode) use (&$news) {
            $linkNodes = $newListNode->filter('a');
            if ($linkNodes->count() === 0) {
                return;
            }
            $linkNode = $linkNodes->first();
            $title = trim($linkNode->text());
            $url = $linkNode->attr('href');
            $news[] = compact('title', 'url');
        });
        return $news;
    }
```

---

# ログインしてからタイムラインをとりにいく

- シンプルすぎて保存とかの処理書かれても見通しよさそう

```FetchRecentTimeLine.php
    /**
     * FetchRecentTimeLine constructor.
     * @param array $user
     */
    public function __construct(array $user)
    {
        $this->user = $user;
    }

    /**
     * @return array
     */
    public function fetch(): array
    {
        $login = new Login($this->user);
        $my = new My($login);
        return $my->crawl();
    }
```

---

# 運用してみて
- *node list empty* っていわれても StackTrace のクラス名の部分みるだけですぐにあたりつけられて楽
- いまのところこれ使ってクローラ運用するにあたって苦労してない
- 本気だすなら React でいうところの Containers/Components みたいな単位で分割するともっと調査時間短くなりそう
    - 過剰な気もするけど
- 型にまもられてる気持ちだけでだいぶ精神が安定する
    - PHP >= 7.1.0 さいこうですね
- 全然関係ないけどページネーション必要そうなページも対応できそうな感じで実装してみた
    - 動作確認は***していない***

--- 

# 結論
- 1ファイルに全部かくと追いかけるの大変だからページごとにクラス分けよう
- これそのまま使いまわしてもいいよ自己責任でね