PHP
Goutte

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

More than 1 year has passed since last update.

はじめに


うれしくなりそうなひと

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

かいてないこと


よくあるつらみ

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

まじむり


せめてどうなってほしいか

  • エラーでた瞬間にどのページで異常が起きてるか知りたい
  • ただそれだけだ

おちついて考えてみよう

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

もしかして

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

やってみた


構造

$ 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ファイルに全部かくと追いかけるの大変だからページごとにクラス分けよう
  • これそのまま使いまわしてもいいよ自己責任でね