いかにしてメイドさん画像をダウンロードするか~NAVERまとめ編
動機
NAVERまとめを「メイド」検索すると約100のまとめがヒットします。 もちろんすべてが画像サイトというわけではありませんが ここに少なからぬ宝が埋蔵されているわけで、これを発掘したいと思ったわけです。
全体の構成
アプローチですが、DBに「巡回リスト」を用意してこの台帳に登録されているURLを順に巡回します。 それぞれの巡回先では画像を(もしあれば)取り込みながら、さらにそこからリンクされているURLを新たな巡回先として リストに登録します。
次のような流れになります。
まずMySQLデータベースとそのユーザーを用意しておきます。 そして「巡回番号」と「URL」の2つの列だけの簡単な表を用意し、 巡回の起点となるNAVERまとめサイトの「メイド」検索結果のページを巡回番号1として登録しておきます。
メインループではループカウンタを1から順に増やしながら、カウンタ値を巡回番号とするURLを巡回リストから読み取ります。 読み取ったURLを子プロセスに渡して親プロセスは次の周にまわります。
それぞれの子プロセスは対象URLにアクセスして、
- Aタグのリンク先を読み取り条件にあえば巡回リストに加えます。
- Aタグのリンク先がjpg画像であればダウンロード処理をします。
巡回リスト
まず初期処理で巡回リストを作成します。 もしも既に一度スクリプトを実施済みでリストが存在したらDROP TABLEで一度クリアしてしまいます。 巡回リストのテーブル名はオプションで変更できるようにしておきました。 コードを抜粋すると次のようなものです。
sub init_db { my $h = shift; my $sql = "DROP TABLE IF EXISTS ".$opt->get_table_name; my $sth = $h->prepare($sql); my $result = $sth->execute(); $sql = "CREATE TABLE ".$opt->get_table_name. "(id INTEGER AUTO_INCREMENT PRIMARY KEY,". "url VARCHAR(255),". "CONSTRAINT UNIQUE cu_test (url))"; $sth = $h->prepare($sql); $result = $sth->execute(); $sth->finish(); insert_db($h, $opt->get_target_url); }
列idはAUTO_INCREMENTを指定していますので新たなレコードが追加されるたびに自動的に1ずつ増加した値が入ります。 列urlは巡回先で重複を避けるためにUNIQUE制約をかけています。
最後にサブルーチンinsert_dbで巡回起点URLを登録しています。 これも抜粋しておくと、
sub insert_db { my $h = shift; my $u = shift; my $sql = "INSERT INTO ".$opt->get_table_name." (id, url) VALUES (NULL, ?)"; my $sth = $h->prepare($sql); { local $sth->{PrintError} = 0; my $result = $sth->execute($u); if (not defined $result) { if ($sth->err == 1062) { ; # It is nothing } else { ERROR "code : ".$sth->err; ERROR "string : ".$sth->errstr; ERROR "state : ".$sth->state; } } } $sth->finish(); }
以前にこのブログに書いた一意キー制約違反を無視するしかけをしています。
親プロセスのメインループ
巡回リストが作成できたら、ひたすらリストを読み取って巡回することを繰り返します。 この部分をParallel::ForkManagerを使って並列処理化しました。 コードを抜粋すると、
my $dbh = connect_db() or die("Cannot connect DB".$DBI::errstr); init_db($dbh); my $pm = Parallel::ForkManager->new($opt->get_num_of_workers); for (my $c = 1; $c < $opt->get_max_pages; $c++) { my $u = get_url($c); if (not defined $u) { last; } my $pid = $pm->start and next; worker($u, $c); $pm->finish; } disconnect_db($dbh);
サブルーチンget_urlがDBから巡回先URLを取ってくる部分です。 少し困るのは子プロセスが次の巡回先をDBに登録する前にこのサブルーチンが呼ばれたときの動作なのですが、 そこは「1秒おきにリストを読みに行く」というポーリング動作をget_urlの中で書いて誤魔化しています。
sub get_url { my $id = shift; my $query = "SELECT url FROM ".$opt->get_table_name. " WHERE id = ?"; for (my $i = 0; $i < $opt->get_wait_count; $i++) { my $sth = $dbh->prepare_cached($query); $sth->execute($id); my @rs = $sth->fetchrow_array(); $sth->finish; if ($rs[0]) { return ($rs[0]); } sleep 1; } return undef; }
子プロセスの動作
起動された子プロセスは指定URLを巡回しますが、目的と無関係な画像のダウンロードを避けるために コンテンツに「メイド」の文字列が存在することをチェックすることにしました。 また「NAVERまとめ」の外のサイトは巡回リストに登録しないよう、フィルタをかけました。
コードは以下のような具合です。
sub worker { my $url = shift; my $c = shift; my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0, } ); my $target_url = URI->new($url); my $r = $ua->get($target_url); if (not ($r->is_success)) { return; } my $contents = $r->decoded_content; my $t = HTML::TreeBuilder->new; $t -> parse( $contents); $t -> eof(); if ($contents =~ /メイド/) { DEBUG "メイド found"; } else { return; } my $h = connect_db(); if ($h) { foreach my $a ($t->find("a") ) { my $tgt = $a->attr('href'); if ($tgt) { my $uri = URI->new_abs($tgt, $target_url); get_photo($ua, $tgt, $target_url); if ($uri =~ m{^http://matome.naver.jp/odai/}) { insert_db($h, $uri); } } } disconnect_db($h); } $t->delete; }
サブルーチン get_photo はリンク先がJPEG画像で100KB以上のサイズだったときにのみファイルにダウンロードする処理をします。
その成果
メインループの最大回数を1万回に設定して実行したところ、 600枚強の画像を取得できました。 今回はかなりの精度でメイドさん関連の画像をダウンロードできており 大満足です。