読者です 読者をやめる 読者になる 読者になる

新米PHPエンジニアdallPの日誌

元SEOコンサルタント、現初心者PHPエンジニアdallPの備忘録です。PHP、SEO、アフィリエイト、お金などについて役に立つ(?)情報を発信します。

【PHP】Goutteでスクレイピングプログラム(クローラー)を作ってみたので過程とともに晒す

お久しぶりです、dallPです。

ブログのネタは沢山メモっているのですが、最初に投稿するのはやっぱりPHPに関することじゃないと。
と思っていたら更新が遅くなりました。

タイトル通りなのですが、今回は初心者なりにPHPクローラーを書いてみたので、コードとか考えたこととか諸々を晒します。

利用したもの

今回は、個人的なタスクを進める上で必要な情報を取得したかったので、1からクローラーを書くようなことはしていません。
(というか基本しないと思いますが)

利用したのは"Goutte"というPHPのライブラリです。

github.com

上記からダウンロードできます。
超絶初心者なので、このライブラリの中で何やってるのか、とか正直全然分かってません。

だけど、これを使えばPHP歴一ヶ月未満の僕でも2時間でスクレイピングが出来た、というのは事実です。

ちなみにこの記事はMac向けです。

参考にさせて頂いたサイト群

PHP と Goutte ではじめる超絶簡単クローラー入門 - Qiita
Goutteをcomposerでインストール - どんどこすすむの日記
たった数行のコードでひたすらアイドル水着画像をあつめるのをGoutteで書いてみた - iakioの日記
「Goutte」で対象ページ中の全リンクURLを取得する - やったこと
【PHP】特定の文字列を含むかのチェック - Qiita

色々参考にさせていただきましたが、一番上のmojibakeoさんのQiitaの記事が一番簡潔で、コードの基本を真似させて頂きました。
PHP クローラー」でもSEOで一位ですね。素晴らしい。
こんな分かりやすい記事を書けるように、プログラマとして精進します。

前提

今回の僕の目的は、地球の歩き方から各国の主要な観光都市名を抽出することでした。

最初は、ビジネス職にありがちな感じで「気合で頑張ろう」と思ってたのですが。。。
2カ国目をやり始めた時点で思いました。

「辛。」

地球の歩き方のサイトには実に160の国・地域の情報が載っています。
さすがに200近いリンクをポチポチやるのは割に合わないですよね。

Macのエクセル遅いし。

まずやったこと: composerとGoutteのインストール

よくわからないですが、composerなるものが便利らしいです。
PHPのライブラリやパッケージを管理してくれるツールらしいですが、僕はまだ良くわかってませんw

定義を気にする方は下記の記事をどうぞ。
qiita.com

Homebrewが入っている方なら、ターミナルで下記コマンドを打つだけでインストールできます。

brew install homebrew/php/composer

ちなみに、

Homebrew(ホームブルー)とは、Mac OS X オペレーティングシステム上でソフトウェアの導入を単純化するパッケージ管理システムのひとつです。

パッケージ管理システム Homebrew

らしいです

ちなみにHomebrewのインストール方法はこちらが詳しそうです。

qiita.com



話が逸れましたが、composerがインストール出来たら次はGoutteです。

composer require fabpot/goutte

これでインストール完了。

早速クローラー書いてみた

いきなりですが、はじめて書いたコードがこちら。

<?php

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

$cli = new Goutte\Client();
$url = 'http://www.arukikata.co.jp/country/europe/GB/';
$crawler = $cli->request('GET',$url);

$crawler->filter('#cities_link .photo_list p')->each(function($name) {
echo $name->text() . "\n";
});

$urlにスクレイピングしたいURLを代入して、filter()の中には抽出したい部分のCSSセレクタをスペース区切りで入れる。
これだけで、抜ける。

すごい。

第二弾: 複数ページをクロールしてみた

では次。クロールしたいのは1ページだけではないので、まずは最終的に欲しいデータが載ってるページのURLを抽出する必要がある。

そこで書いたコードがこちら。

<?php

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

$cli = new Goutte\Client();
$url = 'http://www.arukikata.co.jp/country/US/';
$crawler = $cli->request('GET',$url);

$urls = $crawler->filter('.country_textlink .top_map .city_list a')->extract('href');
print_r($urls);

ここではじめて出てくるのがextract('href')。
詳細はよく分からないが(←)、これ入れずにprint_rしてもURLが返ってこない。
デフォルトではそのリンクに設定されてる文字の方を拾ってきてしまうようなので、「URLを取ってきてね」的な意味なのかなと思っている(想像)。

さて、これで目的のURLは手に入った。
しかし、ビジネス職の職業病が発動する。

ここで僕はなぜかExcelを起動。
手に入ったURLリストをおもむろにExcelにコピペし、次の瞬間下記のコードが出来上がっていた。

<?php

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

$cli = new Goutte\Client();
$urlArray = array("http://www.arukikata.co.jp/country/europe/GB/","http://www.arukikata.co.jp/country/europe/IT/","http://www.arukikata.co.jp/country/europe/ES/","http://www.arukikata.co.jp/country/europe/DE/","http://www.arukikata.co.jp/country/europe/FR/","http://www.arukikata.co.jp/country/asia/KR/","http://www.arukikata.co.jp/country/asia/SG/","http://www.arukikata.co.jp/country/asia/TH/","http://www.arukikata.co.jp/country/asia/TW/","http://www.arukikata.co.jp/country/asia/CN/","http://www.arukikata.co.jp/country/asia/VN/","http://www.arukikata.co.jp/country/namerica/US/","http://www.arukikata.co.jp/country/namerica/CA/","http://www.arukikata.co.jp/country/samerica/AR/","http://www.arukikata.co.jp/country/samerica/BR/","http://www.arukikata.co.jp/country/samerica/PE/","http://www.arukikata.co.jp/country/samerica/BO/","http://www.arukikata.co.jp/country/samerica/MX/","http://www.arukikata.co.jp/country/oceania/AU/","http://www.arukikata.co.jp/country/oceania/NZ/","http://www.arukikata.co.jp/country/hawaii/9H/","http://www.arukikata.co.jp/country/resort/GU/","http://www.arukikata.co.jp/country/resort/MP/","http://www.arukikata.co.jp/country/resort/PW/","http://www.arukikata.co.jp/country/resort/13/","http://www.arukikata.co.jp/country/resort/FJ/","http://www.arukikata.co.jp/country/meast/AE/","http://www.arukikata.co.jp/country/meast/IL/","http://www.arukikata.co.jp/country/meast/QA/","http://www.arukikata.co.jp/country/meast/TR/","http://www.arukikata.co.jp/country/africa/EG/","http://www.arukikata.co.jp/country/africa/KE/","http://www.arukikata.co.jp/country/africa/TZ/","http://www.arukikata.co.jp/country/africa/ZA/","http://www.arukikata.co.jp/country/africa/MA/");
$arrayNum = 0;

while($arrayNum < 34) {
	$crawler = $cli->request('GET',$urlArray[$arrayNum]);
	$crawler->filter('#cities_link .photo_list p')->each(function($name) {
		echo $name->text() . "\n";
	});
	$arrayNum++;
}

横スクロール推奨。
上手く動いたときに僕が喜んだのは言うまでもない。
これで、複数ページをクロールして特定のデータを取得する、という目的は取り敢えず達成された。

汚いコードだとは思いつつ、先輩に喜々としてコードを見せたところ、苦笑しながら手直しされた結果がこちら。

<?php

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

$cli = new Goutte\Client();
$url = 'http://www.arukikata.co.jp/country/US/';
$crawler = $cli->request('GET',$url);

$urls = $crawler->filter('.country_textlink .top_map .city_list a')->extract('href');

foreach ($urls as $url) {
	$crawler = $cli->request('GET',$url);
	$crawler->filter('#cities_link .photo_list p')->each(function($name) {
		echo $name->text() . "\n";
	});
}

お分かりだろうか・・・

修正ポイント1: URLリスト、わざわざExcelで頑張って加工しなくてよかった。二つのプログラムはマージできる。
修正ポイント2: 配列の各要素に何かしらの処理をぶん回すとき、whileは普通使わない (らしい)

修正入るのは全然嬉しいんだけど、僕は褒められて伸びる人間なのでもう少し褒めてほしかった。

最終的に完成したコード

紆余曲折あり、最終的に出来たコードがこちら。

<?php

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

$cli = new Goutte\Client();
$url = 'http://www.arukikata.co.jp/';
$crawler = $cli->request('GET',$url);

// グローバルナビからリンクを抽出
$urls = $crawler->filter('#header2016 #subnav a')->extract('href');

foreach($urls as $url) {
	// URLに'area'が含まれていたら地域ページ
	if (strpos($url,'area')) {
		$crawler = $cli->request('GET',$url);
		// 地域ページのエリアマップから、各国へのリンクを抽出
		$countryURLs = $crawler->filter('#area_Map a')->extract('href');

			foreach($countryURLs as $url) {
				// URLに'country'が含まれていたら国ページ
				if(strpos($url,'country')) {
					$crawler = $cli->request('GET',$url);
					// ターミナル出力時にどの国の都市か分かりやすいようにh1も抽出
					$crawler->filter('h1')->each(function($name) {
						echo $name->text() . "\n";
					});
					// 都市リストを抽出
					$crawler->filter('#cities_link .photo_list p')->each(function($name) {
						echo $name->text() . "\n";
					});
				}
				// ほんの少しの思いやり
				usleep(500000);
			}
	}
}

配列お化けを書いていた頃に比べれば、大幅な進化では無いだろうか(と、個人的に思っている)。

通算4時間かかってしまったけれど、学びは大いにあった。

学びをまとめると

  • プログラムで出来ること・出来ないことを決めつけない。どうしたら実現できるか?を深く考える
  • 仕様超大切 (input→outputへの道筋とoutputの形式をしっかりイメージするだけでコーディングのスピードが上がる)
  • ググり方めっちゃ大事
  • 解法は1つではない。これで無理ならあれでやる、の選択肢を沢山持ってたほうがいい

お腹がすいたので今日はここまでにします。
汚いコードを見てくれた皆様、ありがとうございました。

※こんなに簡単にスクレイピングできちゃうのですが、アホみたいにリクエスト投げまくるとか、悪用はダメ、絶対。