「Webクローラーを設計している場合、無限ループに陥らないようにするにはどうすればよいですか?」というインタビューの質問に出くわしました。私はそれに答えようとしています。
どのようにすべてが最初から始まりますか。 Googleがいくつかのハブページから始めたとすると、何百と言う(最初にこれらのハブページがどのように見つかったかは、別のサブ質問です)。 Googleはページなどからのリンクをたどるので、ハッシュテーブルを作成し続けて、以前にアクセスしたページをたどらないようにします。
URL短縮サービスなどがある最近の同じページに2つの名前(URL)があるとしたらどうでしょう。
Googleを例にとりました。 GoogleはWebクローラーアルゴリズムやページランキングなどの仕組みを漏らしませんが、推測はありますか?
詳細な回答が必要な場合は、 セクション3.8このペーパー を参照してください。これは、現代のスクレーパーのURLで見たテストについて説明しています。
リンクを抽出する過程で、Webクローラーは同じドキュメントへの複数のリンクに遭遇します。ドキュメントを複数回ダウンロードして処理することを避けるために、URLフロンティアに追加する前に、抽出された各リンクでURL-seenテストを実行する必要があります。 (代替設計では、代わりにURLがフロンティアから削除されたときにURL-seenテストを実行しますが、このアプローチではより大きなフロンティアになります。)
URL-seenテストを実行するために、Mercatorによって表示されるすべてのURLをURLセットと呼ばれる大きなテーブルに標準形式で保存します。繰り返しますが、エントリが多すぎてメモリに収まらないため、ドキュメントフィンガープリントセットのように、URLセットはほとんどディスクに保存されます。
スペースを節約するために、各URLのテキスト表現をURLセットに保存するのではなく、固定サイズのチェックサムを保存します。コンテンツ表示テストのドキュメントフィンガープリントセットに提示されるフィンガープリントとは異なり、URLセットに対してテストされるURLのストリームには、重要な局所性があります。したがって、バッキングディスクファイルに対する操作の数を減らすために、一般的なURLのメモリ内キャッシュを保持します。このキャッシュの直感では、一部のURLへのリンクは非常に一般的であるため、人気のあるURLをメモリにキャッシュすると、メモリ内のヒット率が高くなります。
実際、2 ^ 18エントリのメモリ内キャッシュとLRUのようなクロック置換ポリシーを使用すると、メモリ内キャッシュの全体的なヒット率は66.2%、テーブルのヒット率は9.5%になります。ネットヒット率75.7%の最近追加されたURL。さらに、人気のあるURLのキャッシュと最近追加されたURLのテーブルの両方でミスするリクエストの24.3%のうち、約1 = 3は、ユーザー空間にあるランダムアクセスファイル実装のバッファーでヒットを生成します。このすべてのバッファリングの最終的な結果は、URLセットで実行する各メンバーシップテストの結果、平均0.16シークと0.17の読み取りカーネルコールが発生することです(その一部はカーネルのファイルシステムバッファーから提供されます)。そのため、各URLセットメンバーシップテストでは、ドキュメントフィンガープリントセットのメンバーシップテストの1/6のカーネル呼び出しが行われます。これらの節約は、クロール中に遭遇するURLのストリームに固有のURLの局所性(つまり、人気のあるURLの繰り返し)に完全に起因しています。
基本的に、各URLの一意のハッシュを保証するハッシュ関数ですべてのURLをハッシュします。URLの局所性により、URLを見つけるのは非常に簡単になります。グーグルはハッシュ関数をオープンソース化しました: CityHash
警告!
彼らはボットトラップについても話しているかもしれません!!!ボットトラップは、一意のURLを持つ新しいリンクを生成し続けるページのセクションであり、そのページが提供しているリンクをたどることにより、「無限ループ」に本質的に閉じ込められます。ループは同じURLにアクセスした結果であるため、これは正確にはループではありませんが、クロールを回避する必要があるURLの無限のチェーンです。
Fr0zenFyrのコメントごと:ページを選択するために [〜#〜] aopic [〜#〜] アルゴリズムを使用する場合、無限ループの種類のボットトラップを避けることはかなり簡単です。 AOPICの仕組みの概要は次のとおりです。
Lambdaページは継続的に税金を徴収するため、最終的にはクレジットが最大のページになり、「クロール」する必要があります。引用符で「クロール」と言います。実際には、Lambdaページに対してHTTPリクエストを行うのではなく、そのクレジットを取得してallデータベース内のページ。
ボットトラップは内部リンクのクレジットのみを提供し、外部からクレジットを取得することはほとんどないため、(課税から)クレジットをLambdaページに継続的にリークします。 Lambdaページは、そのクレジットをデータベース内のすべてのページに均等に分配し、各サイクルでボットトラップページはクレジットを失い、クレジットがほとんどなくなるので、ほとんどクロールされなくなります。良いページでは、他のページにあるバックリンクからクレジットを受け取ることが多いため、これは起こりません。これにより、動的なページランクも得られます。データベースのスナップショットを作成するたびに、クレジットの量でページを並べると、ほとんどの場合、true page rank。
これは無限ループの種類のボットトラップのみを回避しますが、 他の多くのボットトラップ があり、注意する必要があり、それらを回避する方法もあります。
ここにいる皆さんは既にウェブクローラーの作成方法を提案していますが、Googleがページをランク付けする方法は次のとおりです。
Googleは、コールバックリンクの数(特定のWebサイト/ページを指す他のWebサイト上のリンクの数)に基づいて各ページにランクを付けます。これは、関連性スコアと呼ばれます。これは、ページに他の多くのページがリンクしている場合、おそらく重要なページであるという事実に基づいています。
各サイト/ページは、グラフ内のノードとして表示されます。他のページへのリンクは、有向エッジです。頂点の次数は、入ってくるエッジの数として定義されます。着信エッジの数が多いノードほどランクが高くなります。
PageRankの決定方法は次のとおりです。ページPjにLjリンクがあるとします。それらのリンクの1つがページPiにある場合、Pjはその重要性の1/LjをPiに渡します。 Piの重要度ランキングは、Piにリンクしているページによって行われたすべての貢献の合計です。したがって、PiにリンクしているページのセットをBiで表すと、次の式が得られます。
Importance(Pi)= sum( Importance(Pj)/Lj ) for all links from Pi to Bi
ランクは、ハイパーリンクマトリックスと呼ばれるマトリックスに配置されます。H[i、j]
この行列の行は0、またはPiからBiへのリンクがある場合は1/Ljです。この行列のもう1つの特性は、列のすべての行を合計すると1になることです。
ここで、次のようなI(固有値1)という名前の固有ベクトルをこの行列に乗算する必要があります。
I = H*I
繰り返しを開始します:IH、IIH、II [〜#〜] i [〜#〜]H .... I ^ k * H解が収束するまで。つまり、ステップkとk + 1のマトリックスでほぼ同じ数を取得します。
Iベクトルに残っているのは、各ページの重要性です。
簡単なクラスの宿題の例については、 http://www.math.cornell.edu/~mec/Winter2009/RalucaRemus/Lecture3/lecture3.html を参照してください。
インタビューの質問の重複する問題を解決するには、ページ全体でチェックサムを実行し、それまたはチェックサムのバッシュをマップのキーとして使用して、訪問したページを追跡します。
質問の意図の深さに依存します。同じリンクを前後にたどらないようにしようとしている場合は、URLをハッシュするだけで十分です。
文字通り何千ものURLが同じコンテンツにつながるコンテンツについてはどうでしょうか? QueryStringパラメーターのように、何にも影響を与えませんが、無限の反復回数を持つことができます。ページのコンテンツもハッシュし、URLを比較して、複数のURLで識別されるコンテンツをキャッチするのに似ているかどうかを確認できると思います。たとえば、@ Lirikの投稿で言及されているボットトラップを参照してください。
ここでの問題は、URLから取得したハッシュを使用するインデックスによって解決される重複したURLをクロールしないことです。問題は、重複したコンテンツをクロールすることです。 「クローラートラップ」の各URLは異なります(年、日、セッションID ...)。
「完璧な」解決策はありません...しかし、この戦略のいくつかを使用できます:
•URLがWebサイト内にあるレベルのフィールドを保持します。ページからURLを取得するたびに、レベルを上げます。それは木のようになります。 10などの特定のレベルでクロールを停止できます(Googleがこれを使用すると思います)。
•データベース内の各ドキュメントと比較できないため、類似のドキュメントを見つけるために比較できる一種のHASHを作成しようとすることができます。 GoogleのSimHashがありますが、使用する実装が見つかりませんでした。その後、自分で作成しました。私のハッシュはhtmlコード内の低頻度および高頻度の文字をカウントし、20バイトのハッシュを生成します。これは、ある程度の許容範囲(約2)のNearNeighbors検索でAVLTree内の最後のクロールされたページの小さなキャッシュと比較されます。このハッシュの文字の場所への参照を使用することはできません。トラップを「認識」した後、重複したコンテンツのURLパターンを記録し、それを含むページも無視し始めることができます。
•Googleと同様に、各Webサイトのランキングを作成し、他のWebサイトよりも1つのWebサイトでより多くの「信頼」を行うことができます。
結果を保存するためのハッシュテーブルを用意する必要があり、各ページを読み込む前にチェックする必要があります。
また、クローラーを使用する必要があり、自分の要件に合った適切なものを見つけることができないため、単純な要件を実装するために基本的なクローラーライブラリを開発しました。しかし、クローラーのほぼすべての原則を満たすことができます。 DotnetCrawler github repo をチェックして、Sql Serverにデータを保存するために、Entity Framework Coreを使用した既定の実装でDownloader-Processor-Pipelineモジュールを独自に実装します。
クローラーは、クロールされるすべてのURLを含むURLプールを保持します。 「無限ループ」を回避するための基本的な考え方は、プールに追加する前に各URLの存在を確認することです。
ただし、システムが特定のレベルに拡張された場合、これを実装するのは簡単ではありません。素朴なアプローチは、すべてのURLをハッシュセットに保持し、新しい各URLの存在を確認することです。 URLが多すぎてメモリに収まらない場合、これは機能しません。
ここにはいくつかの解決策があります。たとえば、すべてのURLをメモリに保存する代わりに、ディスクに保存する必要があります。スペースを節約するには、生URLの代わりにURLハッシュを使用する必要があります。また、元のURLではなく正規の形式のURLを保持する必要があることにも注意してください。したがって、bit.lyなどのサービスによってURLが短縮されている場合は、最終URLを取得することをお勧めします。チェックプロセスを高速化するために、キャッシュレイヤーを構築できます。または、別のトピックである分散キャッシュシステムとして見ることができます。
投稿 Build a Web Crawler には、この問題の詳細な分析があります。
Webクローラーは、特定のWebサイトのURLから次のキー値(HREFリンク、画像リンク、メタデータなど)を収集/クロールするために使用されるコンピュータープログラムです。以前のURLから既に取得された異なるHREFリンクをたどるようにインテリジェントに設計されているため、このようにして、Crawlerは1つのWebサイトから他のWebサイトにジャンプできます。通常、WebスパイダーまたはWebボットと呼ばれます。このメカニズムは、常にWeb検索エンジンのバックボーンとして機能します。
私の技術ブログからソースコードを見つけてください- http://www.algonuts.info/how-to-built-a-simple-web-crawler-in-php.html
<?php
class webCrawler
{
public $siteURL;
public $error;
function __construct()
{
$this->siteURL = "";
$this->error = "";
}
function parser()
{
global $hrefTag,$hrefTagCountStart,$hrefTagCountFinal,$hrefTagLengthStart,$hrefTagLengthFinal,$hrefTagPointer;
global $imgTag,$imgTagCountStart,$imgTagCountFinal,$imgTagLengthStart,$imgTagLengthFinal,$imgTagPointer;
global $Url_Extensions,$Document_Extensions,$Image_Extensions,$crawlOptions;
$dotCount = 0;
$slashCount = 0;
$singleSlashCount = 0;
$doubleSlashCount = 0;
$parentDirectoryCount = 0;
$linkBuffer = array();
if(($url = trim($this->siteURL)) != "")
{
$crawlURL = rtrim($url,"/");
if(($directoryURL = dirname($crawlURL)) == "http:")
{ $directoryURL = $crawlURL; }
$urlParser = preg_split("/\//",$crawlURL);
//-- Curl Start --
$curlObject = curl_init($crawlURL);
curl_setopt_array($curlObject,$crawlOptions);
$webPageContent = curl_exec($curlObject);
$errorNumber = curl_errno($curlObject);
curl_close($curlObject);
//-- Curl End --
if($errorNumber == 0)
{
$webPageCounter = 0;
$webPageLength = strlen($webPageContent);
while($webPageCounter < $webPageLength)
{
$character = $webPageContent[$webPageCounter];
if($character == "")
{
$webPageCounter++;
continue;
}
$character = strtolower($character);
//-- Href Filter Start --
if($hrefTagPointer[$hrefTagLengthStart] == $character)
{
$hrefTagLengthStart++;
if($hrefTagLengthStart == $hrefTagLengthFinal)
{
$hrefTagCountStart++;
if($hrefTagCountStart == $hrefTagCountFinal)
{
if($hrefURL != "")
{
if($parentDirectoryCount >= 1 || $singleSlashCount >= 1 || $doubleSlashCount >= 1)
{
if($doubleSlashCount >= 1)
{ $hrefURL = "http://".$hrefURL; }
else if($parentDirectoryCount >= 1)
{
$tempData = 0;
$tempString = "";
$tempTotal = count($urlParser) - $parentDirectoryCount;
while($tempData < $tempTotal)
{
$tempString .= $urlParser[$tempData]."/";
$tempData++;
}
$hrefURL = $tempString."".$hrefURL;
}
else if($singleSlashCount >= 1)
{ $hrefURL = $urlParser[0]."/".$urlParser[1]."/".$urlParser[2]."/".$hrefURL; }
}
$Host = "";
$hrefURL = urldecode($hrefURL);
$hrefURL = rtrim($hrefURL,"/");
if(filter_var($hrefURL,FILTER_VALIDATE_URL) == true)
{
$dump = parse_url($hrefURL);
if(isset($dump["Host"]))
{ $Host = trim(strtolower($dump["Host"])); }
}
else
{
$hrefURL = $directoryURL."/".$hrefURL;
if(filter_var($hrefURL,FILTER_VALIDATE_URL) == true)
{
$dump = parse_url($hrefURL);
if(isset($dump["Host"]))
{ $Host = trim(strtolower($dump["Host"])); }
}
}
if($Host != "")
{
$extension = pathinfo($hrefURL,PATHINFO_EXTENSION);
if($extension != "")
{
$tempBuffer ="";
$extensionlength = strlen($extension);
for($tempData = 0; $tempData < $extensionlength; $tempData++)
{
if($extension[$tempData] != "?")
{
$tempBuffer = $tempBuffer.$extension[$tempData];
continue;
}
else
{
$extension = trim($tempBuffer);
break;
}
}
if(in_array($extension,$Url_Extensions))
{ $type = "domain"; }
else if(in_array($extension,$Image_Extensions))
{ $type = "image"; }
else if(in_array($extension,$Document_Extensions))
{ $type = "document"; }
else
{ $type = "unknown"; }
}
else
{ $type = "domain"; }
if($hrefURL != "")
{
if($type == "domain" && !in_array($hrefURL,$this->linkBuffer["domain"]))
{ $this->linkBuffer["domain"][] = $hrefURL; }
if($type == "image" && !in_array($hrefURL,$this->linkBuffer["image"]))
{ $this->linkBuffer["image"][] = $hrefURL; }
if($type == "document" && !in_array($hrefURL,$this->linkBuffer["document"]))
{ $this->linkBuffer["document"][] = $hrefURL; }
if($type == "unknown" && !in_array($hrefURL,$this->linkBuffer["unknown"]))
{ $this->linkBuffer["unknown"][] = $hrefURL; }
}
}
}
$hrefTagCountStart = 0;
}
if($hrefTagCountStart == 3)
{
$hrefURL = "";
$dotCount = 0;
$slashCount = 0;
$singleSlashCount = 0;
$doubleSlashCount = 0;
$parentDirectoryCount = 0;
$webPageCounter++;
while($webPageCounter < $webPageLength)
{
$character = $webPageContent[$webPageCounter];
if($character == "")
{
$webPageCounter++;
continue;
}
if($character == "\"" || $character == "'")
{
$webPageCounter++;
while($webPageCounter < $webPageLength)
{
$character = $webPageContent[$webPageCounter];
if($character == "")
{
$webPageCounter++;
continue;
}
if($character == "\"" || $character == "'" || $character == "#")
{
$webPageCounter--;
break;
}
else if($hrefURL != "")
{ $hrefURL .= $character; }
else if($character == "." || $character == "/")
{
if($character == ".")
{
$dotCount++;
$slashCount = 0;
}
else if($character == "/")
{
$slashCount++;
if($dotCount == 2 && $slashCount == 1)
$parentDirectoryCount++;
else if($dotCount == 0 && $slashCount == 1)
$singleSlashCount++;
else if($dotCount == 0 && $slashCount == 2)
$doubleSlashCount++;
$dotCount = 0;
}
}
else
{ $hrefURL .= $character; }
$webPageCounter++;
}
break;
}
$webPageCounter++;
}
}
$hrefTagLengthStart = 0;
$hrefTagLengthFinal = strlen($hrefTag[$hrefTagCountStart]);
$hrefTagPointer =& $hrefTag[$hrefTagCountStart];
}
}
else
{ $hrefTagLengthStart = 0; }
//-- Href Filter End --
//-- Image Filter Start --
if($imgTagPointer[$imgTagLengthStart] == $character)
{
$imgTagLengthStart++;
if($imgTagLengthStart == $imgTagLengthFinal)
{
$imgTagCountStart++;
if($imgTagCountStart == $imgTagCountFinal)
{
if($imgURL != "")
{
if($parentDirectoryCount >= 1 || $singleSlashCount >= 1 || $doubleSlashCount >= 1)
{
if($doubleSlashCount >= 1)
{ $imgURL = "http://".$imgURL; }
else if($parentDirectoryCount >= 1)
{
$tempData = 0;
$tempString = "";
$tempTotal = count($urlParser) - $parentDirectoryCount;
while($tempData < $tempTotal)
{
$tempString .= $urlParser[$tempData]."/";
$tempData++;
}
$imgURL = $tempString."".$imgURL;
}
else if($singleSlashCount >= 1)
{ $imgURL = $urlParser[0]."/".$urlParser[1]."/".$urlParser[2]."/".$imgURL; }
}
$Host = "";
$imgURL = urldecode($imgURL);
$imgURL = rtrim($imgURL,"/");
if(filter_var($imgURL,FILTER_VALIDATE_URL) == true)
{
$dump = parse_url($imgURL);
$Host = trim(strtolower($dump["Host"]));
}
else
{
$imgURL = $directoryURL."/".$imgURL;
if(filter_var($imgURL,FILTER_VALIDATE_URL) == true)
{
$dump = parse_url($imgURL);
$Host = trim(strtolower($dump["Host"]));
}
}
if($Host != "")
{
$extension = pathinfo($imgURL,PATHINFO_EXTENSION);
if($extension != "")
{
$tempBuffer ="";
$extensionlength = strlen($extension);
for($tempData = 0; $tempData < $extensionlength; $tempData++)
{
if($extension[$tempData] != "?")
{
$tempBuffer = $tempBuffer.$extension[$tempData];
continue;
}
else
{
$extension = trim($tempBuffer);
break;
}
}
if(in_array($extension,$Url_Extensions))
{ $type = "domain"; }
else if(in_array($extension,$Image_Extensions))
{ $type = "image"; }
else if(in_array($extension,$Document_Extensions))
{ $type = "document"; }
else
{ $type = "unknown"; }
}
else
{ $type = "domain"; }
if($imgURL != "")
{
if($type == "domain" && !in_array($imgURL,$this->linkBuffer["domain"]))
{ $this->linkBuffer["domain"][] = $imgURL; }
if($type == "image" && !in_array($imgURL,$this->linkBuffer["image"]))
{ $this->linkBuffer["image"][] = $imgURL; }
if($type == "document" && !in_array($imgURL,$this->linkBuffer["document"]))
{ $this->linkBuffer["document"][] = $imgURL; }
if($type == "unknown" && !in_array($imgURL,$this->linkBuffer["unknown"]))
{ $this->linkBuffer["unknown"][] = $imgURL; }
}
}
}
$imgTagCountStart = 0;
}
if($imgTagCountStart == 3)
{
$imgURL = "";
$dotCount = 0;
$slashCount = 0;
$singleSlashCount = 0;
$doubleSlashCount = 0;
$parentDirectoryCount = 0;
$webPageCounter++;
while($webPageCounter < $webPageLength)
{
$character = $webPageContent[$webPageCounter];
if($character == "")
{
$webPageCounter++;
continue;
}
if($character == "\"" || $character == "'")
{
$webPageCounter++;
while($webPageCounter < $webPageLength)
{
$character = $webPageContent[$webPageCounter];
if($character == "")
{
$webPageCounter++;
continue;
}
if($character == "\"" || $character == "'" || $character == "#")
{
$webPageCounter--;
break;
}
else if($imgURL != "")
{ $imgURL .= $character; }
else if($character == "." || $character == "/")
{
if($character == ".")
{
$dotCount++;
$slashCount = 0;
}
else if($character == "/")
{
$slashCount++;
if($dotCount == 2 && $slashCount == 1)
$parentDirectoryCount++;
else if($dotCount == 0 && $slashCount == 1)
$singleSlashCount++;
else if($dotCount == 0 && $slashCount == 2)
$doubleSlashCount++;
$dotCount = 0;
}
}
else
{ $imgURL .= $character; }
$webPageCounter++;
}
break;
}
$webPageCounter++;
}
}
$imgTagLengthStart = 0;
$imgTagLengthFinal = strlen($imgTag[$imgTagCountStart]);
$imgTagPointer =& $imgTag[$imgTagCountStart];
}
}
else
{ $imgTagLengthStart = 0; }
//-- Image Filter End --
$webPageCounter++;
}
}
else
{ $this->error = "Unable to proceed, permission denied"; }
}
else
{ $this->error = "Please enter url"; }
if($this->error != "")
{ $this->linkBuffer["error"] = $this->error; }
return $this->linkBuffer;
}
}
?>