スクレイピング に関する投稿を表示しています

PHP で本文抽出したいよね、という

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

前回の記事: ExtractContent を PHP で書き換えた | ごみばこいん Blog

「今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい」といいつつ、他のネタが挟まり、ようやくにして比較しました。

比較した結論

お試しにはこちらのリポジトリをどうぞ。
sters/compare-article-extractors: Compare web article extractors.

日本語環境下で難しい話

php-goose 、または php-web-article-extractor では、どちらも内部にストップワード辞書を持っていて、プログラム上で言語を指定、あるいは lang 属性や meta タグから読み取った言語名から、適切なストップワード辞書を選択しています。そうして決まったストップワードの一覧が、文字列にどれくらいあるかを確認して、本文かどうか?といった判断を進めているようです。

ストップワードとはよく使われがちな単語のことで、自然言語処理の前処理として行なうことでデータ量を減らしたり、精度を上げたり出来ます。

自然言語処理における前処理の種類とその威力 - Qiita

例えば英語では「This is a pen.」といったようにスペースで区切られているので「ストップワードの is と a の 2 文字がある!こいつはコンテンツだ!」と簡単に出来るのですが、日本語の場合はそう簡単な話ではありません。日本語はスペース区切りではありませんし、前後にある文字によって、まったく異なる意味合いになったりもしますので、一概に辞書でドバーッと指定することは適切な処理でない可能性が高いです。

じゃあどうするかというと分かち書きの出番なのですが、分かち書きも簡単なものではありません。 Chasen や Mecab といった既存の技術を利用すればお手軽にもできますが php-goose や php-web-article-extractor がそれを利用して日本語対応するかというとちょっと違いそうだなあ、という気持ちです。

php-goose に出したプルリクは諦めてドバーッと処理する形ではありますが、日本語だけ特殊化せざるを得ないので、そうやってみて出してみたものの、なんだかなあと。いやそもそも 元になってる Goose はそういう実装になっていないだろうし…。

そのほかのアプローチ方法

DOM構造や、画面上の位置情報も利用するのはめっちゃ有効的だと思います。
PuppeteerでWebページからメインコンテンツっぽいところを抽出してみる - Qiita

上記の記事の「まとめ」でも出てはいますが、機械学習して取るようにするのもよさそうです。
seomoz/dragnet: Just the facts -- web page content extraction

仕事スクレイピングを楽にした

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

この記事は 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 さんで「何か書きたい」です。こちらもお楽しみに!

Webスクレイピング Advent Calendar 2017 - Adventar

ExtractContent を PHP で書き換えた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

いや Ruby でええやん、 Python でええやん、みたいな話だとは思うのだが、やっぱり PHP でやりたいよね、という一定の需要がギョームで発生してしまったので、 Ruby のコードを見ながら PHP に書き換えた。 Packagist にも登録してあるので composer からどうぞ。

sters/extract-content - Packagist

$url = 'http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html';
$extractor = new \ExtractContent\ExtractContent(file_get_contents($url));
$result = $extractor->analyse();
file_put_contents(__DIR__ . '/result', $result);

// // 抽出結果
// Webページの自動カテゴライズ の続き。
// 前回書いたとおり、パストラックで行っている Web ページのカテゴライズでは、Web ページの本文抽出がひとつの鍵になっています。今回はその本文抽出モジュールを公開しつつ、使っている技法をざっくり解説などしてみます。
// 本モジュールの利用は至極簡単。require して analyse メソッドに解析したい html を与えるだけ。文字コードは UTF-8 です。
// ...

ExtractContent は Cybozu の Nakatani Shuyo さんが2009年に作成したもので、正規表現を主として Web 記事上の本文に関するちょっとの知見が加わることで成り立っている。以下の記事へ。

Webページの本文抽出 (nakatani @ cybozu labs)

今回、移す際に参考にしたのは、もともとのものではなく、Ruby 1.9 に対応したソース。というのもこっちの記事を見つけたのは後で、 Github 上で ExtractContent を先に見つけていたので、まあいっか、と。

mono0x/extractcontent: ExtractContent for Ruby 1.9+

Ruby はチョットヨメルので inject とかわからないメソッドだけドキュメント見つつ脳内補完して、同じような感じの処理になるよう PHP へ書き直した。工夫したところはとくになく、クラスで扱うようにした、チョットテスト書いたくらいで、ほぼそのまま移してきた。Wikipedia と Medium、はてなブログあたりで試してみたところで、おおよそうまくいっているように見えたので、たぶん大丈夫。

ただ、試していて、本文っぽいと判断されるのが 2 つ以上あるような 1 記事ページ(例えば記事中に section タグがあってーとか、そいういう)ではうまく取り出せず、スコアが高くなったほうのみ抽出されてしまう。本文しきい値のようなものを設けてそれを超えていたら、結合して出す、とかしないといけないなあと思う。とはいえ、正確に本文が欲しいのか、その記事中の重要な部分にフォーカスするのか、などなど要件にもよるので、とりあえずはいいんじゃないかなの気持ち。

ちなみに PHP による実装もいたのだが、おそらく上記のようなところで、元々のものには無いオプションが増えていたりでチョットわからなかったので、一から書いた次第。

aoiaoi/ExtractContent: extract content from HTML

なお記事解析、本文抽出について Packagist を調べると他にも 3 つのライブラリが出てくる。ざっくり紹介するとこんな感じ。

今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい。