こんにちは、にょろりんこの備忘録的技術ブログです。
今日は、Node.js製のWebクローラーでシードURLからリンクをたどる際に役立つ、同一ドメイン限定のフィルタ関数について紹介します。
クローラーの処理は、最初の1件――いわゆるシードURLから始まります。そこから <a> タグをたどってリンクをどんどん収集していくのですが、そのままにしておくとTwitterや広告、他サイトのリンクまですべて含まれてしまい、無制限に外部ドメインへ飛んでしまう「リンク暴走」状態になってしまいます。
とはいえ、これはこれで「全てのサイトを網羅したい」といった総当たり型クローラーの発想としては確かに理にかなっています。実際、大手の検索エンジンなどはそうしたクロスドメインのリンクも含めて無制限にクロールできるだけの資源とポリシーを持っています。
でも、現実的にはそうはいきません。個人開発のクローラーや中小規模のボットでは、無秩序にリンクをたどるとすぐに処理が爆発しますし、意図しない外部サイトにリクエストを飛ばし続けるリスクも大きくなります。
そのため、「同一ドメインのみを対象にする」という制限は、手元で制御可能なスコープを維持しながらクローリングを行うための現実的な戦略でもあるのです。

今回紹介するのは、そういった事態を防ぐための小さなフィルター関数、filterSameDomainLinks() です。この関数を使うことで、シードURLと同じドメインに属するリンクだけを抽出し、際限のないフェッチングを回避することができます。
コードとしては以下になります。
function filterSameDomainLinks(links, baseUrl) {
const baseDomain = new URL(baseUrl).hostname;
return links.filter(link => {
try {
const linkDomain = new URL(link).hostname;
return linkDomain === baseDomain;
} catch (err) {
return false;
}
});
}
この関数の目的は、渡された links 配列の中から baseUrl と同じドメインを持つリンクだけを抽出する ことです。
const baseDomain = new URL(baseUrl).hostname;
まず、基準となるURL(たとえば https://example.com/page1)から、ホスト名(ドメイン)だけを取り出します。この例だと、baseDomain には "example.com" が入ります。
links.filter(link => { ... })
次に、与えられたリンク一覧(例:['https://example.com/page2', 'https://twitter.com/...'])を .filter() で走査します。
各リンクについては以下です。
const linkDomain = new URL(link).hostname;
return linkDomain === baseDomain;
link を new URL() でパースし、.hostname を取り出します。baseDomain と一致すれば true、そうでなければ false にしてフィルターから除外します。
try { ... } catch { return false; }
一部のリンク(たとえば javascript:void(0) や相対パス "/about" など)は new URL(link) の時点でエラーになります。そういった不正なリンクがあると .filter() 全体がクラッシュしてしまうので、try-catch で安全にスキップしています。
これらの効果によって「ドメイン単位でリンクを選別」「例外処理で落ちない堅牢性」「シンプルな構造での高い再利用性」という三拍子を達成しています。
次は、このfilterSameDomainLinks関数が、main.jsの方で実際にどのように使われているか見ていきます。
具体的には、以下ののように、ページを開いて <a> タグからリンクをすべて取得したあとに、以下のように同一ドメインのリンクだけを通すフィルタとして活用されています。
const sameDomainUrls = filterSameDomainLinks(rawUrls, targetUrl);
これによって、Twitterや広告などの外部リンクを自動的に除外できるようになり、シードURLからたどるリンクが、対象サイト内のページだけに限定されるようになります。
ちなみにこのフィルター関数は、example.com と m.example.com のようなサブドメインの違いも考慮せず、完全一致で判定しています。
サブドメインをどう扱うかは設計次第ですが、私のクローラーでは「別サイト扱いで再シードする」方針を取っています。というのも、サブドメインごとに構造や目的が異なるケースが多く、同一の構造として扱うと逆に混乱を招くからです。サブドメインに対しては、一括で扱わずに個別に巡回管理することで、クロール範囲を明確に保ちつつ、処理の安定性も確保するという方針を採用しています。
このあたりは、「超大規模にクローリングを行うか」「範囲を限定して管理型に行うか」といった、ウェブサービスの目的との整合性の問題かなと思います。「良い・悪い」ではなく、大切なのはそのクローラーの設計思想とどう整合しているか、という点なんじゃないかなと感じています。
それではみなさん、よい開発ライフを。
このコードが使われている、あなたを追跡しないアダルト動画の検索エンジン Sae-Porns はこちら。よかったら見ていってください。