こんにちは、にょろりんこの備忘録的技術ブログです。
今日は、クローリングやリンク収集処理の地味だけど超重要なパーツ、「URLの正規化」についてのお話です。
私が運用しているURLスクレイピング系のURL収集スクリプトでは、同じページを重複して処理しないために「URLの見た目」を正規化しています。たとえば、以下のようなケースです。
https://example.com/page/ と https://example.com/page → 同一
https://example.com/page#section1 と https://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が別物として登録されてしまうことがあります。
- https://example.com/page
- https://example.com/page/
- https://example.com/page/#top
- https://example.com/page#bottom
いずれも実体は同じページですが、見た目の違いだけで別エントリとして扱われてしまう──というのは避けたいですよね。
こうした問題を回避するためにも、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歳未満の方はご利用いただけません。