ぽっぺんしゃんにょろりんこ

匿名・非追跡型アダルト動画検索エンジンの設計ノート

Node.jsでURL正規化!末尾スラッシュやアンカーを除去する「normalizeUrl」関数の話

こんにちは、にょろりんこの備忘録的技術ブログです。

今日は、クローリングやリンク収集処理の地味だけど超重要なパーツ、「URLの正規化」についてのお話です。

私が運用しているURLスクレイピング系のURL収集スクリプトでは、同じページを重複して処理しないために「URLの見た目」を正規化しています。たとえば、以下のようなケースです。

https://example.com/page/https://example.com/page → 同一

https://example.com/page#section1https://example.com/page → 同一

DB容量をちゃんと節約するためには、こうした「実質同じだけどURL文字列が違う」ケースは統一しておきたいところです。特にWebクローラーの世界では、「同じように見えるけど違うURL」が大量に出現します。アンカー(#以降の部分)付きURLや、末尾にスラッシュがある・ないの違いだけで、同じページを複数回クロールしてしまうことがよくあります。

ティアラ_食事

そこで活躍するのが、今回紹介する normalizeUrl() という関数です。この関数は、URLの末尾スラッシュやアンカーを取り除き、見た目の揺れをなくしてくれるシンプルなユーティリティです。

具体的なコードは以下です。

■normalize.js

function normalizeUrl(rawUrl) {
  try {
    const url = new URL(rawUrl);
    url.hash = ''; // アンカー除去
    let cleaned = url.toString();
    if (cleaned.length > 1 && cleaned.endsWith('/')) {
      cleaned = cleaned.slice(0, -1); // 末尾スラッシュ除去
    }
    return cleaned;
  } catch (e) {
    return null; // 無効URLは null
  }
}

やっていることはとてもシンプルですが、効果は抜群です。まず、以下のコードです。

const url = new URL(rawUrl);

上記コードでURL文字列をパースし、そもそも無効なURLだった場合には try...catch によって null を返すようにしています。これにより、明らかにおかしな文字列(例: "not a url")はスキップ対象になります。

次に、以下のコードです。

url.hash = '';

上記コードで #section1 のようなアンカー部分を丸ごと除去します。アンカーはクローリング上まったく意味を持たないので、ここで統一してしまうのが得策です。

さらに、次はこんな感じ。

if (cleaned.length > 1 && cleaned.endsWith('/')) {
  cleaned = cleaned.slice(0, -1);
}

上記コードで末尾の / を除去しています。これにより、https://example.com/page/https://example.com/page のようなURLの揺れをなくせます。

このように一つひとつは些細な処理ですが、積み重ねることで「無駄な重複クロール」「DBの同一URL登録」などの無駄を一気に解消できます。

特にSae-pornsのように、数万〜数十万件単位でURLを処理している場合、こうした“見た目の違いだけの重複”をどれだけ排除できるかが、パフォーマンスにもデータ品質にも大きく影響します。

たとえば、正規化せずにクロールを続けると、以下のようなURLが別物として登録されてしまうことがあります。

いずれも実体は同じページですが、見た目の違いだけで別エントリとして扱われてしまう──というのは避けたいですよね。

こうした問題を回避するためにも、normalizeUrl() のような関数を最初に通しておくことは、URL収集やクローリング処理の「第一歩」とも言えます。

実際の運用では、正規化されたURLを使って、すでに登録済みかどうかをチェックし、未登録のものだけをデータベースに追加するようにしています。

その処理は以下のようなコードで実装しています。

■main.js

for (const raw of sameDomainUrls) {
  const norm = normalizeUrl(raw);
  if (!norm) continue; // 正規化できなければスキップ

  const existing = await pool.query('SELECT id FROM urls WHERE url = ?', [norm]);
  if (existing.length === 0) {
    await pool.query('INSERT INTO urls (url) VALUES (?)', [norm]);
    insertCount++;
    console.log(`新規URL追加: ${norm}`);
  }
}

ここでは、同一ドメインから抽出された sameDomainUrls のリストに対して、normalizeUrl() を使って正規化し、すでにDBに存在するかをクエリで確認し、なければ INSERT する──という流れです。

normalizeUrl() を通すことで、見た目の揺れによる重複登録を未然に防げているわけですね。クローリング対象を効率よく管理したいなら、こうした“ひと手間”は欠かせません。このように「ユーティリティ関数を作って終わり」ではなく、実際にどこでどう使うのか まで設計することで、スクリプト全体の質もぐっと上がります。

このnormalize処理は、まさに全体クローラーの入口に置くべきフィルター的役割。手を抜かず、堅実に実装しておくのが正解と思います。

それではみなさんよい開発ライフを。

このコードとかをつかって作られている、あなたを追跡しないアダルト動画の検索エンジンSae-Pornsはこちら。よかったら見て行ってください。

※18歳未満の方はご利用いただけません。

sae-porns.org