特定のURLにある画像をまるっとzipでダウンロードするサイトを作ってみる


## 注意 - @2015-08-20(木)


今日の段階ではまだサイトは作れませんでした。
Naverまとめから画像を全ページ分引っ張ってくる所までしか出来てませんが、今週中に目的のサイトは完成させます！

## 背景

なんか、例えばこう・・2chまとめサイトとかみてて、画像のファイルがいっぱいあったら、それをDLしていつでも見れるようにしたいなーって時あるじゃないですか。
それを作ってみようと思いました。

## Goutteというライブラリを使ってみる

### まずはインストール

composerの基本的な使い方については過去の[クソみたいな記事](http://qiita.com/rikkey/items/4462124c475a0fc35f31)を見てください。

composertを使ってインストールします。

```json:composer.json
{
    "require": {
        "fabpot/goutte": "v3.1.0"
    }
}
```

packagistによると最新バージョンは3.1.0だったのでこれを使います。

```bash
./composer.phar install
```

これでOK.

### さっそく使ってみる

とりあえず、大好きな剛力彩芽ちゃんの画像をNaverまとめから引っ張ってきましょう。

`Crawler`オブジェクトは、filterメソッドにHTMLのタグの名前かCSSのセレクタを指定する。
で、この`filter`メソッドは、そのURLの中から指定したタグやセレクタの要素を持ってきて、そのDOMをもったcrawlerオブジェクトを返すっぽい。
で、`each`メソッドってのは↓みたいになってて、自分自身のオブジェクト(filterで一致したDOMのオブジェクトなのかな？)に、引数に渡したclosureを通して、通したあとのCrawlerオブジェクトを返すようになってる。

```php:Crawlerのeachメソッド
 343     public function each(\Closure $closure)
 344     {
 345         $data = array();
 346         foreach ($this as $i => $node) {
 347             $data[] = $closure(new static($node, $this->uri, $this->baseHref), $i);
 348         }
 349 
 350         return $data;
 351     }
```

さらに、今回はimgタグのsrc要素がほしいので、attrでsrcを指定してみました。


```php
<?php

require_once __DIR__."/../lib/vendor/autoload.php";

use \Goutte\Client;

$client = new Client();

// naverまとめに大好きな剛力彩芽のまとめページがあったので使ってみる
$crawler = $client->request('GET', 'http://matome.naver.jp/odai/2138493836638647001');

/**
 * filterメソッドの引数は下記のように書かれてるので、
 * CSSのセレクターを指定すればいいんだと思う
 *
 * @param string $selector A CSS selector
 */
$crawler->filter('img')->each(function ($node) {
    echo $node->attr('src')."\n";
});
```

```結果
続けるにはENTERを押すかコマンドを入力してください
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Ft1.gstatic.com%2Fimages%3Fq%3Dtbn%3AANd9GcR3Oh7t3PY5DYEA22NwTlQB0TaELgMHZI_FCElxkvdI71TBJBk3-6PihNY&twidth=95&theight=95&qlt=80&res_format=jpg&op=sc
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20140208%2F35%2F3012705%2F0%2F300x300x555a66adc7e01cd8213fbbbb.jpg%2F120%2F120&twidth=33&theight=33&qlt=80&res_format=jpg&op=sc
http://b.st-hatena.com/images/entry-button/button-only.gif
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20150802%2F35%2F3012705%2F224%2F500x505x54189f15878b8fb723bf6373.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20150802%2F35%2F3012705%2F226%2F500x352x08256d4a5cd44817fd1b4e5c.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20150707%2F35%2F3012705%2F23%2F800x566x8302f00e3db5b6246a6d28d0.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r
http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20150707%2F35%2F3012705%2F21%2F599x444xf2227f50661db381815f4182.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r

....
```

とれてはいるんだけど、横の広告の画像とかもとって来ちゃってる。
そんなのはいらない。俺は剛力ちゃんだけがほしいのだ！！
ということで、Naverまとめのページ構造をもうちょい見てみよう。

どうやら`div`の`LyMain`というクラスがメインの記事？っぽい部分になってて、その中の画像だけ持ってくれば良さそう。

#### 再挑戦


```php:filterの部分変えてみた

$crawler->filter('div.LyMain img')->each(function ($node) {
    echo $node->attr('src')."\n";
});
```

いい感じになったけど、まだ余計な画像入ってる。
すげーDOMをたどってみると、Naverの記事の中の画像は`MTMItemThumb`ってクラスがついてるっぽいので、これで絞り込んでみる。

```php:さらに変えた
$crawler->filter('div.LyMain img.MTMItemThumb')->each(function ($node) {
    echo $node->attr('src')."\n";
});
```

おぉ！！めっちゃいい感じ！！！！
でも、待ってこれじゃ1ページ分しか取得出来ないよね。
でもNaverって複数ページあるじゃない。
どうせなら全ページ持ってきたい。
しかもこのClientオブジェクト使えばなんかいけそう！！

### 2ページ目も取得してみる

ちなみに、Naverは`div.MdPagination03`って要素が、ページングになってるっぽいので、ここを解析して最後のページまで取得出来る気がする。

次のページもクロールするには、次のページのURLさえわかれば同じことの繰り返しでなんとか出来そう。

Naverの場合は、URLの最後に`?page=xx`ってつければページングするっぽい。

このページングは、strongタグが今表示してるページっぽいので、その次の要素のテキストが次ページ数のはず！
で、次のページのURLを生成してcrawlerオブジェクトを生成して、同じことを繰り返せば出来そう！！！

```php:完成版
<?php

require_once __DIR__."/../lib/vendor/autoload.php";

use \Goutte\Client;

$client = new Client();

// naverまとめに大好きな剛力彩芽のまとめページがあったので使ってみる
$crawler = $client->request('GET', 'http://matome.naver.jp/odai/2138493836638647001');

/**
 * filterメソッドの引数は下記のように書かれてるので、
 * CSSのセレクターを指定すればいいんだと思う
 *
 * @param string $selector A CSS selector
 */
$page = 1;
while(1) {
    $crawler->filter('div.LyMain img.MTMItemThumb')->each(function ($node) {
        echo $node->attr('src')."\n";
    });
    
    // 今表示してるページの次のページの数字を取得
    $nextPageNode = $crawler->filter('div.MdPagination03 strong')->nextAll();

    // 次のページがなければ終了
    // @TODO ここは空ではなくて、Crawlerオブジェクトが返ってくるくるからemptyで終了を判別できないから例外飛んじゃう
    if (empty($nextPageNode)) {
        break;
    }

    // 次ページのページ数を取得
    $nextPageNum = $nextPageNode->text();

    // 次ページのcrawlerオブジェクトをつくる:w
    $crawler = $client->request('GET', 'http://matome.naver.jp/odai/2138493836638647001?page='.$nextPageNum);

    // 一応1秒だけ置いとく
    sleep(1);
}
```

できたぁああああああ！！！よし！


## 番外編

Clientオブジェクトと、Crawlerオブジェクトがどんなもので、それぞれ何が出来そうなのかを紹介しておきます。

### Clientクラス

```php
$client = new Client();

$methods = get_class_methods($client);

var_dump($methods);
```

今回は`request`メソッドを使いましたが、Auth認証のサイトを見たり、サイト上でクリックしたりとかなんか色々出来そう。
なんかWEBページそのもの的な？


```php:Goutte:Client
array(29) {
  [0] =>
  string(9) "setClient"
  [1] =>
  string(9) "getClient"
  [2] =>
  string(9) "setHeader"
  [3] =>
  string(12) "removeHeader"
  [4] =>
  string(7) "setAuth"
  [5] =>
  string(9) "resetAuth"
  [6] =>
  string(13) "addPostFields"
  [7] =>
  string(11) "__construct"
  [8] =>
  string(15) "followRedirects"
  [9] =>
  string(15) "setMaxRedirects"
  [10] =>
  string(8) "insulate"
  [11] =>
  string(19) "setServerParameters"
  [12] =>
  string(18) "setServerParameter"
  [13] =>
  string(18) "getServerParameter"
  [14] =>
  string(10) "getHistory"
  [15] =>
  string(12) "getCookieJar"
  [16] =>
  string(10) "getCrawler"
  [17] =>
  string(19) "getInternalResponse"
  [18] =>
  string(11) "getResponse"
  [19] =>
  string(18) "getInternalRequest"
  [20] =>
  string(10) "getRequest"
  [21] =>
  string(5) "click"
  [22] =>
  string(6) "submit"
  [23] =>
  string(7) "request"
  [24] =>
  string(4) "back"
  [25] =>
  string(7) "forward"
  [26] =>
  string(6) "reload"
  [27] =>
  string(14) "followRedirect"
  [28] =>
  string(7) "restart"
}
```

### Crawler

Crawlerオブジェクトは、データ抽出に関する振る舞いをするっぽいです。


```php:Symfony\Component\DomCrawler\Crawler
array(58) {
  [0] =>
  string(11) "__construct"
  [1] =>
  string(5) "clear"
  [2] =>
  string(3) "add"
  [3] =>
  string(10) "addContent"
  [4] =>
  string(14) "addHtmlContent"
  [5] =>
  string(13) "addXmlContent"
  [6] =>
  string(11) "addDocument"
  [7] =>
  string(11) "addNodeList"
  [8] =>
  string(8) "addNodes"
  [9] =>
  string(7) "addNode"
  [10] =>
  string(2) "eq"
  [11] =>
  string(4) "each"
  [12] =>
  string(5) "slice"
  [13] =>
  string(6) "reduce"
  [14] =>
  string(5) "first"
  [15] =>
  string(4) "last"
  [16] =>
  string(8) "siblings"
  [17] =>
  string(7) "nextAll"
  [18] =>
  string(11) "previousAll"
  [19] =>
  string(7) "parents"
  [20] =>
  string(8) "children"
  [21] =>
  string(4) "attr"
  [22] =>
  string(8) "nodeName"
  [23] =>
  string(4) "text"
  [24] =>
  string(4) "html"
  [25] =>
  string(7) "extract"
  [26] =>
  string(11) "filterXPath"
  [27] =>
  string(6) "filter"
  [28] =>
  string(10) "selectLink"
  [29] =>
  string(12) "selectButton"
  [30] =>
  string(4) "link"
  [31] =>
  string(5) "links"
  [32] =>
  string(4) "form"
  [33] =>
  string(25) "setDefaultNamespacePrefix"
  [34] =>
  string(17) "registerNamespace"
  [35] =>
  string(12) "xpathLiteral"
  [36] =>
  string(7) "getNode"
  [37] =>
  string(6) "attach"
  [38] =>
  string(6) "detach"
  [39] =>
  string(8) "contains"
  [40] =>
  string(6) "addAll"
  [41] =>
  string(9) "removeAll"
  [42] =>
  string(15) "removeAllExcept"
  [43] =>
  string(7) "getInfo"
  [44] =>
  string(7) "setInfo"
  [45] =>
  string(7) "getHash"
  [46] =>
  string(5) "count"
  [47] =>
  string(6) "rewind"
  [48] =>
  string(5) "valid"
  [49] =>
  string(3) "key"
  [50] =>
  string(7) "current"
  [51] =>
  string(4) "next"
  [52] =>
  string(11) "unserialize"
  [53] =>
  string(9) "serialize"
  [54] =>
  string(12) "offsetExists"
  [55] =>
  string(9) "offsetSet"
  [56] =>
  string(11) "offsetUnset"
  [57] =>
  string(9) "offsetGet"
}
```