この記事は Webスクレイピング Advent Calendar 2017 - Adventar の 5 日目の記事です。
お仕事都合による発生するスクレイピングについてと、それつらたん~~~なことになってきたので解消するようにブチアゲした話を書きます。
お仕事都合による発生するスクレイピング
お仕事都合、例えば営業や企画といった人達から「こういうこと出来ないかなあ…?」とかって話が来るとおよそ7,8割の確立でスクレイピングできないっすかというお願いです。それが 1 回だけならいいんですが、異なる人から、異なることを、ちょこちょことお願いが来たりします。 さらに厄介なのが「XX さんに先月お願いしたやつ、今月もよろしく」みたいなやつ。ぼくは XX さんじゃないし、その事があったことを知らないぞ??
そしてその “お願い” の中身としては、例えばこういう感じ。
- 指定する URL 群から XXX と YYY の情報を集めたい
- (これがいちばんおおい)
- 電車案内のサイトから駅名一覧を集めたい
- 求人系のサイトから職種一覧、募集企業の情報を集めたい
- IRまとめ系サイトから業種や会社名、連絡先を集めたい
- 特定のメディアサイトに入っている画像を集めたい
- 〇〇のメーカーの商品、型番の一覧を集めたい
ムフフなコンテンツ集めたい- etc...
そしてこういったお願いはかなりの速度感を求められることがあります。
とはいえ、どばーーーーってスクレイピングしたら、対象のサイトに対して、迷惑を掛けることにつながりますし、訴訟リスクも存在します。前提として 1 日目の vaaaaanquish さんが書かれているようなこと( PythonでWebスクレイピングする際の規約回りの読み込み - Stimulator ) を 確認しつつ、良心的な範囲でやらなければいけないので、その点については説明し納得をしてもらっています。
そんなこんなで、これまではまあいっかーという気持ちで、毎回 0 からコードを書いて、ぐるぐるしたりしてやっているのですが、いやいやちょっとそろそろ面倒になってきましたよ、ということでもうちょっとやることにしました。
「コレ使えばおおよそいいよ」セットを作る
毎回コードを書くのはまあいいとして、でも毎回 0 スタートはかなり面倒です。ボイラープレートというか、フレームワークというか、何かそういう「コレ使えばおおよそいいよ」セットを作ります。 このセットがあることによって、コードがざっくりと統一されるので、引き継ぎだったり、並行開発だったり(するか?)、そのあたりの勝手が良くなります(なりました)。
Guzzel 使えばいいじゃん、とかそういう話でもあるんですが、スクレイピングしたときってデータの入出力はもちろん、スクレイピングのやり方によってはキューイングしたり、一時データをもったりするじゃないですか。そういうところ全部扱いたいんですよ。で、後述するんですけど、速度感よくやっていこうとすると、あーだこーだ composer require するってよりは、ファイル 1 個読めばいいじゃん、という環境にしたいのです。
制約とか
お仕事の都合によって、言語の制約は PHP になります。他の言語でもいいのですが、周りの人の取り回しを考えるとこれがベストマッチです。
また composer や PEAR は利用できません。お願いを解決するにあたって速度感よくやるために「コレ使えばおおよそいいよ」セットは、何も考えずにとにかくサクッと使えることが必要です。なんなら PHP の設定も気にせずに使えるように、どこかのサーバにファイルを設置して eval(file_get_contents()) みたいに使えると最高です。
そうして出来たものがこちらです
easy scraping kit ※ CSS セレクタを扱う箇所がざっくり過ぎて正しくないです
コレを使って、例えば「Mapion を利用して、指定する都道府県・市区町村の駅名を集める」ものを書くとこんな用になります。
// プログラム本体
eval(file_get_contents('https://.../scraping_kit.txt'));
HTMLDoc::$waitMin = 5000;
HTMLDoc::$waitMax = 15000;
$baseUrl = 'https://www.mapion.co.jp';
foreach(Console::getListFile('list.csv') as $zip) {
Console::out("start {$zip[0]}");
// search pref
$topDoc = HTMLDoc::loadURL($baseUrl . '/station/');
$prefLinks = $topDoc->findCSSPath('.section.type-a a');
foreach($prefLinks as $prefLink) {
if ($prefLink->textContent !== $zip[0]) {
continue;
}
Console::out("found {$zip[0]}");
Console::out("start {$zip[1]}");
// search city
$prefDoc = HTMLDoc::loadURL($baseUrl . $prefLink->attributes['href']->value);
$cityLinks = $prefDoc->findCSSPath('.section.type-a a');
foreach($cityLinks as $cityLink) {
if (mb_strpos($cityLink->textContent, $zip[1]) === false) {
continue;
}
Console::out("found {$zip[1]}");
// search station
$cityDoc = HTMLDoc::loadURL($baseUrl . $cityLink->attributes['href']->value);
$h1 = $cityDoc->findCSSPath('h1.type-a-ttl');
$h1 = explode('の', $h1[0]->textContent);
$h1 = $h1[0];
$trainLinks = $cityDoc->findCSSPath('table.list-table tr');
foreach($trainLinks as $train) {
$item = [
$h1,
];
$tds = $cityDoc->findCSSPath('td', $train);
foreach($tds as $td) {
$item[] = str_replace('[MAP]', '', trim($td->textContent));
}
Console::outputResult($item);
}
}
Console::out("done {$zip[0]}{$zip[1]}");
}
}
// list.csv の例
神奈川県,川崎市
神奈川県,横浜市
今ままで 0 から書いて、1,2 時間かかってコードを書いていたのがサクッと終わるようになりました。 また、簡単にではありますが、キューやキャッシュも持てるようになるので、 cron などを使って定期的に回したり、 web サイト A の情報を元に web サイト B の情報を取得するといった複数のステップがあるようなスクレイピングもわりかし簡単にできるようになりました。
ごく簡単なスクレイピングはコードを書かずに済ませたい
「コレ使えばおおよそいいよ」が出来ましたが、そもそもとして、よく発生するお願いは「指定する URL から XXX と YYY の情報を集めたい」です。これってコードを書く必要はほとんどないはずで、入力として URL と CSS セレクタ( もしくは XPath セレクタ)さえあれば、あとはよろしく処理するような仕組みがあれば十分なはずです。
というわけで一旦雑に作って様子を見ています
11 月くらいに作って様子を見ようと思っていたのですが、特に何かあったわけでもないんですが、急にお願いが減っちゃって作らなくていいかなあと思ってきました。とはいえ怠けているとあとが大変になりそうな予感がするので、近いうちにやりたい気持ち。
おわりに
「コレ使えばおおよそいいよ」を作ってお仕事都合のスクレイピングを簡単にしたり、別の開発者が入るときも問題の起きにくい状況になりました。 しかし、もはやこの件についてコードを書くことをなるべくやめたいので、そういう「スクレイピングいい感じにできるツール」みたいなのあったら教えてください。
(Google スプレッドシートで出来ることは知っていますが、時間系のあたりをもうちょっと自由度が効く感じで…)
6 日目は _kjou さんで「何か書きたい」です。こちらもお楽しみに!