まず「URL 正規化」で何をしたいのかイメージする
「URL 正規化」と聞くと難しく感じますが、やりたいことは意外とシンプルです。
"https://Example.com//path/../path2/?a=1"
"http://example.com/path2"
"https://example.com/path2/"
これらが「全部同じ場所を指している」のに、文字列としてはバラバラです。
業務で困るのは、例えばこんなときです。
- 同じページなのに「別の URL」としてログに記録されてしまう
- キャッシュキーや DB のユニークキーとして URL を使うときに、重複判定がうまくいかない
- 「ホワイトリストに登録した URL と一致するか」を判定したいのに、表記ゆれで判定がズレる
そこで、
「同じ場所を指す URL は、できるだけ同じ文字列になるように整える」
これが「URL 正規化」です。
URL 正規化でよくやる「整え方」のパーツ
ホスト名を小文字にそろえる
URL のホスト名(Example.com の部分)は、大文字小文字を区別しません。
https://Example.com/path
![]()
Example Domain
これらは同じ場所を指します。
なので、正規化では「ホスト名を小文字にする」のが定番です。
余計なスラッシュや . .. を整理する
パス部分には、こんなゆらぎがあります。
https://example.com//a//b///c
![]()
Example Domain
![]()
Example Domain
これらは、全部「/a/b/c」と同じ場所を指します。
正規化では、
- 連続する
/を 1 個にする .を削除する..を解決して一つ上の階層に戻す
といった処理で、「最短のパス」に整えます。
デフォルトポートを消す
HTTP/HTTPS には「デフォルトポート」があります。
httpのデフォルトポートは80httpsのデフォルトポートは443
なので、
https://example.com:443/path
![]()
Example Domain
は同じ場所です。
正規化では、「スキームに対するデフォルトポートが明示されていたら消す」というのもよくやります。
末尾のスラッシュをどう扱うかを決める
https://example.com/path
![]()
Example Domain
これを「同じ」とみなすか、「別」とみなすかは、システム設計次第です。
正規化のルールとして、
- 「ディレクトリ扱いの URL は末尾
/を付ける」 - 「API のエンドポイントは末尾
/を付けない」
など、プロジェクトごとに方針を決めておくとよいです。
ここは「正解が一つではない」ので、ユーティリティ側で勝手に変えすぎない方が安全なことも多いです。
PHP で URL を分解・再構築する基本:parse_url と http_build_query
parse_url で URL を分解する
PHP には、URL を構造的に分解してくれる parse_url があります。
$url = "https://Example.com:443/a/../b//c/?q=php#top";
$parts = parse_url($url);
var_dump($parts);
PHP$parts には、だいたいこんな配列が入ります。
[
'scheme' => 'https',
'host' => 'Example.com',
'port' => 443,
'path' => '/a/../b//c/',
'query' => 'q=php',
'fragment' => 'top',
]
PHPこれを一つ一つ「整えて」から、また文字列に戻す、というのが正規化の基本パターンです。
クエリ部分は「とりあえずそのまま」でいいことが多い
クエリ(?q=php&sort=desc の部分)も、本気でやるなら
- パラメータの順序をソートする
- 重複パラメータをまとめる
などの正規化もありえますが、
業務アプリでは「そこまでやらない」ことも多いです。
初心者向けの段階では、
「クエリ文字列は、今回はいじらずそのまま」
くらいにしておく方が、安全で分かりやすいです。
シンプルな「URL 正規化」ユーティリティを作ってみる
パスの . .. と連続スラッシュを整理する関数
まずは、パス部分だけをきれいにする小さな関数から。
/**
* URL のパス部分を正規化する
* - 連続する / を 1 個に
* - . を削除
* - .. を解決
*/
function normalizePath(string $path): string
{
// 連続する / を 1 個に
$path = preg_replace('#/+#', '/', $path);
$segments = explode('/', $path);
$stack = [];
foreach ($segments as $seg) {
if ($seg === '' || $seg === '.') {
// 何もしない(スキップ)
continue;
}
if ($seg === '..') {
// 一つ上の階層に戻る
array_pop($stack);
continue;
}
$stack[] = $seg;
}
return '/' . implode('/', $stack);
}
PHP動きのイメージです。
echo normalizePath('/a/../b//c/'); // /b/c
echo normalizePath('//x///y/./z'); // /x/y/z
PHPここでのポイントは、
- 「パスだけ」を対象にしている
.と..をちゃんと解決している
というところです。
URL 全体を正規化する関数
これを使って、URL 全体を整える関数を作ります。
/**
* URL をシンプルに正規化する
* - ホスト名を小文字に
* - デフォルトポートを削除(http:80, https:443)
* - パスを normalizePath で整える
* - クエリ・フラグメントはそのまま
*/
function normalizeUrl(string $url): ?string
{
$parts = parse_url($url);
if ($parts === false || !isset($parts['scheme'], $parts['host'])) {
// URL として解釈できない場合は null を返す
return null;
}
$scheme = strtolower($parts['scheme']);
$host = strtolower($parts['host']);
$port = $parts['port'] ?? null;
$path = $parts['path'] ?? '/';
$query = $parts['query'] ?? null;
$frag = $parts['fragment'] ?? null;
// デフォルトポートなら削除
if (($scheme === 'http' && $port === 80) ||
($scheme === 'https' && $port === 443)) {
$port = null;
}
// パスを正規化
$path = normalizePath($path);
// URL を再構築
$normalized = $scheme . '://' . $host;
if ($port !== null) {
$normalized .= ':' . $port;
}
$normalized .= $path;
if ($query !== null && $query !== '') {
$normalized .= '?' . $query;
}
if ($frag !== null && $frag !== '') {
$normalized .= '#' . $frag;
}
return $normalized;
}
PHP使い方の例です。
echo normalizeUrl("https://Example.com:443/a/../b//c/?q=php#top");
// https://example.com/b/c?q=php#top
echo normalizeUrl("HTTP://EXAMPLE.COM//path");
// http://example.com/path
PHPここまでできれば、かなり「実務で使える」レベルです。
実務での使いどころのイメージ
ログやキャッシュキーの「URL をそろえたい」とき
アクセスログやキャッシュキーに URL を使うとき、
同じページなのに URL の表記ゆれで別物扱いされると、分析やキャッシュ効率が悪くなります。
$rawUrl1 = "https://Example.com:443/a/../b//c/";
$rawUrl2 = "https://example.com/b/c";
$norm1 = normalizeUrl($rawUrl1);
$norm2 = normalizeUrl($rawUrl2);
var_dump($norm1 === $norm2); // true
PHPこうしておけば、「同じ場所」は同じ文字列として扱えるようになります。
ホワイトリストチェックなどの前処理として
「この URL が、許可されたドメイン配下かどうか」をチェックしたいときも、
まず正規化してから判定すると安全です。
$allowedHost = 'example.com';
$url = $_GET['url'] ?? '';
$norm = normalizeUrl($url);
if ($norm === null) {
// そもそも URL としておかしい
}
$host = parse_url($norm, PHP_URL_HOST);
if ($host !== $allowedHost) {
// 許可されていないドメイン
}
PHPホスト名を小文字にそろえておけば、Example.com と example.com の違いで判定がズレることもありません。
まとめ:今日からの「URL 正規化」ユーティリティ
押さえておきたいポイントをコンパクトにまとめると、こうです。
- URL 正規化は、「同じ場所を指す URL を、できるだけ同じ文字列にそろえる」ための処理。
- よくやるのは「ホスト名を小文字に」「デフォルトポートを消す」「パスの
...や連続/を整理する」。 - PHP では
parse_urlで分解し、パスは自前で整えてから文字列に戻す、という形が扱いやすい。 - クエリやフラグメントは、最初のうちは「いじらずそのまま」にしておく方が安全。
まずは、あなたのコードの中で「URL をそのまま文字列として比較しているところ」を一箇所思い出してみてください。
そこを normalizeUrl() 前提に書き換えると、どれくらい判定が安定するか—その感覚を一度つかむと、「URL は正規化してから扱う」が自然な習慣になっていきます。

