URLの解剖:あらゆるURLを構成要素に瞬時に分解する
📷 Jordan Harrison / PexelsURLの解剖:あらゆるURLを構成要素に瞬時に分解する
プロトコル、ホスト名、ポート、パス、クエリパラメータ、フラグメント — URLのすべての要素を理解し、無料のオンラインツールで解析する方法を学ぼう。
URLは見かけより複雑
開発者は毎日URLを扱います。コピーして、貼り付けて、ログに記録して、デバッグして — たいていの場合、URLを見れば何を指しているかだいたい分かります。だからURLは理解できていると思いがちです。
でもある日、こんなものに出会うことがあります:
https://user:p%40ss@api.example.com:8443/v2/search?q=hello+world&filter%5Bstatus%5D=active&sort=desc#results
...そうなると、URL解析は決して簡単ではないと実感します。
そこで適切なURLパーサーが役に立ちます。ブラウザのDevTools、CLIツール、スクリプト、あるいはtoolboxhubs.com/en/tools/url-parserのようなツールを使っても、URLを個別のパーツに分解する方法があれば、目を細めて確認する時間を大幅に節約できます。
このガイドでは、URLのすべての構成要素を解説し、解析が重要となる現実のシナリオを取り上げ、予期しないと痛い目に遭うエッジケースも紹介します。
URLの構造
この投稿全体を通じて、次のURLを例として使います:
https://admin:secret@api.example.com:8080/v1/users/42?format=json&include=profile#contact
主要な構成要素のほとんどをカバーしています。一つずつ見ていきましょう。
プロトコル(スキーム)
https://
プロトコル(スキームとも呼ばれる)は、リソースをどのように取得するかを定義します。httpsはTLS暗号化されたHTTPを意味します。他にもhttp、ftp、ws(WebSocket)、wss(セキュアWebSocket)、mailto、file、そしてモバイルのディープリンクで使われるmyapp://のようなカスタムスキームも使われます。
注意点として:://の区切り文字はスキーム自体の一部ではありません。スキームはhttpsであり、https://ではありません。手動で文字列を解析しようとするとここで躓きます。
ユーザー名とパスワード
admin:secret@
URLに埋め込まれた認証情報です。現代のウェブアプリでは珍しいですが、内部ツール、レガシーシステム、一部のAPI設定では今でもよく使われています。://とホスト名の間に、コロンで区切られ、末尾に@記号が付く形で配置されます。
認証情報が含まれたURLをログに記録することは絶対に避けるべきです。認証に関わるコードを書いていてフルURLをログに残している場合、ここは必ず除去すべき箇所です。ほとんどのURLパースライブラリはusernameとpasswordを別々のプロパティとして提供しているので、保存前にそれらを削除できます。
ホスト名
api.example.com
ホスト名はDNSで解決されます。ドメイン名、サブドメイン、ベアIPアドレス、そして[2001:db8::1]のようなIPv6アドレスになることもあります。IPv6のブラケットはURL仕様で必須であり、:での単純な文字列分割はIPv6ホストに出会った瞬間に完全に失敗します。エッジケースについては後ほど詳しく説明します。
ポート
:8080
ポートはオプションです。指定されていない場合、ブラウザ(またはクライアント)はスキームのデフォルトを想定します — httpではポート80、httpsではポート443です。デフォルトポートを明示的に指定した場合(https://example.com:443/など)、良いURLパーサーは通常それを正規化するか、少なくとも冗長であることを知らせてくれます。
ポート8080と3000は開発者の定番です。開発用HTTPSでは8443が使われます。ステージング環境やローカル環境でデバッグしていて何かが解決しない場合、ポートが正しく取得されているか、どこかで飲み込まれていないかを確認する価値があります。
パス名
/v1/users/42
パスはホスト(とポート)の後から?または#までの部分です。サーバー上の特定のリソースを識別します。REST APIでは、パスにリソースタイプとIDがこのようにエンコードされることが多いです — /v1/users/42は:APIバージョン1、usersコレクション、ID42のレコードを意味します。
パスにはパーセントエンコードされた文字が含まれる場合があります。/search/hello%20worldと/search/hello world(リテラルのスペース)は技術的には異なります — たとえ実際にはしばしば同じように扱われても。パスを比較する場合は、常に一貫してデコードされた値を比較するようにしましょう。
クエリ文字列
?format=json&include=profile
クエリ文字列はおそらく日常業務でURL中で最もよく解析される部分です。?で始まり、&で区切られたキーと値のペアを含みます。各ペアはkey=valueの形式です。
値の種類:
- プレーンな文字列:
?name=John - URLエンコード:
?q=hello%20world(スペースを%20としてエンコード) - スペースに
+を使用(フォームエンコーディング):?q=hello+world - 配列(非標準だが一般的):
?ids[]=1&ids[]=2または?ids=1&ids=2 - ネストされたオブジェクト(PHPスタイル):
?filter[status]=active
最後の例 — filter%5Bstatus%5D=active — はfilter[status]=activeでブラケットがエンコードされたものです。基本的なキーと値の分割しか行わないURLパーサーはfilter%5Bstatus%5Dをキーとして返し、別途デコードする必要があります。注意が必要です。
フラグメント(ハッシュ)
#contact
フラグメントは#以降のすべてです。重要なことに、フラグメントはサーバーに送信されることはありません。ブラウザが完全にクライアントサイドで処理します。つまり、サーバーログからユーザーのURLにどんなフラグメントが含まれていたかを調べようとしても — 不可能です。サーバーはそれを見ません。
フラグメントはページ内ナビゲーション(アンカー要素へのジャンプ)、シングルページアプリのルーティング、時には安易な状態ストア(最近は少なくなりましたが)に使われます。OAuthの暗黙的フローや一部のトークン受け渡しパターンにも使われます。これはフラグメントが「見えない」ように感じられても機密データを含む可能性があることを思い出させてくれます。
コードでURLを解析する
JavaScript — 組み込みURL API
現代のJavaScriptには解析を非常にうまく処理する組み込みのURLコンストラクタがあります。ライブラリは不要です。
const raw = 'https://admin:secret@api.example.com:8080/v1/users/42?format=json&include=profile#contact';
const url = new URL(raw);
console.log(url.protocol); // 'https:'
console.log(url.username); // 'admin'
console.log(url.password); // 'secret'
console.log(url.hostname); // 'api.example.com'
console.log(url.port); // '8080'
console.log(url.pathname); // '/v1/users/42'
console.log(url.search); // '?format=json&include=profile'
console.log(url.hash); // '#contact'
// クエリパラメータをイテラブルオブジェクトとして
const params = url.searchParams;
console.log(params.get('format')); // 'json'
console.log(params.get('include')); // 'profile'
// 全パラメータを反復処理
for (const [key, value] of params) {
console.log(`${key}: ${value}`);
}
searchParamsプロパティはURLSearchParamsオブジェクトです — エンコードとデコードを自動的に処理します。URLに?q=hello+worldが含まれていれば、params.get('q')はプラスをデコードして'hello world'を返します。これが望ましい動作です。
注意点:URLコンストラクタは入力が有効な絶対URLでない場合にTypeErrorをスローします。ユーザー入力を解析する場合は、try/catchで囲みます:
function parseURL(input) {
try {
return new URL(input);
} catch {
return null;
}
}
相対URLの場合、ベースを渡す必要があります:
const url = new URL('/v1/users/42', 'https://api.example.com');
// 解決後: https://api.example.com/v1/users/42
Python — urllib.parse
Pythonの標準ライブラリにはurllib.parseにしっかりしたURL解析機能があります:
from urllib.parse import urlparse, parse_qs, urlencode, quote, unquote
raw = 'https://admin:secret@api.example.com:8080/v1/users/42?format=json&include=profile#contact'
parsed = urlparse(raw)
print(parsed.scheme) # 'https'
print(parsed.netloc) # 'admin:secret@api.example.com:8080'
print(parsed.hostname) # 'api.example.com'
print(parsed.port) # 8080 (文字列ではなく整数)
print(parsed.username) # 'admin'
print(parsed.password) # 'secret'
print(parsed.path) # '/v1/users/42'
print(parsed.query) # 'format=json&include=profile'
print(parsed.fragment) # 'contact'
# クエリ文字列を辞書に解析
params = parse_qs(parsed.query)
print(params) # {'format': ['json'], 'include': ['profile']}
# parse_qsは各値のリストを返す(マルチバリューパラメータをサポート)
# 空の値を保持するには parse_qs(qs, keep_blank_values=True) を使用
parse_qsはリストを返すことに注意してください — クエリパラメータは複数回出現する可能性があるためです。つまりparams['format']は'json'ではなく['json']です。単一の値が欲しい場合は、parse_qsにstrict_parsing=Falseを指定して[0]でインデックスアクセスするか、タプルのリストを返すurllib.parse.parse_qslを使用します。
一般的なユースケース
APIコールのデバッグ
おそらくURLパーサーに手を伸ばす最大の理由です。400エラーが発生し、リクエストURLを見て、実際に何が送信されているかを把握する必要があります。
例えばGitHub APIのURLを見てみましょう:
https://api.github.com/repos/facebook/react/commits?sha=main&per_page=50&page=3&since=2024-01-01T00%3A00%3A00Z
これを解析すると、すぐに分かります:facebook/reactリポジトリのmainブランチのコミットをフェッチしていて、1ページ50件、3ページ目、2024年1月1日以降 — since値はパーセントエンコード(%3Aは:)されています。これをプログラムで構築していて正しく動作しない場合、デコードされた値を一目で見られると問題が一目瞭然になります。
UTMパラメータの抽出
マーケティングチームはUTMパラメータが大好きです。アナリティクスダッシュボードにはこんなURLがあふれています:
https://example.com/landing?utm_source=newsletter&utm_medium=email&utm_campaign=spring_sale_2026&utm_content=cta_button
レポーティング、アトリビューション、またはファネルを通じてこれらを引き渡す必要がある場合:
const url = new URL(window.location.href);
const utm = {};
for (const [key, value] of url.searchParams) {
if (key.startsWith('utm_')) {
utm[key] = value;
}
}
console.log(utm);
// { utm_source: 'newsletter', utm_medium: 'email', utm_campaign: 'spring_sale_2026', utm_content: 'cta_button' }
シンプルで明快。正規表現は不要です。
リダイレクトチェーンの追跡
リダイレクトループのデバッグや短縮URLがどこに行き着くかを追跡する必要がある場合、一連のLocationヘッダー値を調べることになります。それぞれは絶対URLまたは相対URLになります。URLパーサーを使えば現在のベースに対して相対リダイレクトを解決でき、チェーンを正しく辿ることができます。
import urllib.request
from urllib.parse import urljoin
def trace_redirects(start_url, max_hops=10):
url = start_url
chain = [url]
for _ in range(max_hops):
try:
req = urllib.request.Request(url, method='HEAD')
# 自動リダイレクトをフォローしない
opener = urllib.request.build_opener(
urllib.request.HTTPRedirectHandler()
)
resp = opener.open(req)
break # 最終目的地に到達
except urllib.error.HTTPError as e:
if e.code in (301, 302, 303, 307, 308):
location = e.headers.get('Location', '')
url = urljoin(url, location) # 相対リダイレクトを処理
chain.append(url)
else:
break
return chain
urljoinの呼び出しが相対リダイレクトを機能させます — サーバーがLocationとして/new-pathを返した場合、urljoinは現在のURLのベースに対してそれを解決します。
エッジケースと落とし穴
URL解析は最初は単純に見えますが、そうではないことを前述しました。実際に自分やチームメイトが痛い目を見た特定の状況を以下に挙げます。
IPv6ホスト
URLのIPv6アドレスはこのようになります:
http://[2001:db8::1]:8080/path
ブラケットは必須です。:で分割してホストとポートを抽出しようとすると、ゴミが返ってきます。JavaScriptのURLコンストラクタはこれを正しく処理します — url.hostnameは2001:db8::1(ブラケットなし)を返し、url.portは8080を返します。Pythonのurlparseも処理します。ただし手動の文字列分割を試みる場合、IPv6はその理由の一つです。
パーセントエンコードされたクエリパラメータ
これは微妙な点です。クエリパラメータのキー自体がパーセントエンコードされている場合 — filter[status]に対してfilter%5Bstatus%5Dのように — 異なるパーサーは異なる処理をします。JavaScriptのURLSearchParamsはデコードしてくれます。Pythonのparse_qsもデフォルトでデコードします。ただし、特に古いものは一貫していないライブラリもあります。
常にパースライブラリが値だけでなくキーもデコードするかどうか確認しましょう。
プロトコルの欠落
//example.com/pathのようなURLはプロトコル相対URLです — 現在のページコンテキストからプロトコルを継承します。URLコンストラクタはベースなしでは無効として拒否します。そしてスキームなしのexample.com/pathのようなものは技術的にはURLではありません。ドメインのように見える相対パスです。
new URL('//example.com/path');
// TypeError: Failed to construct 'URL': Invalid URL
new URL('//example.com/path', 'https://current-page.com');
// 動作します: https://example.com/path
ユーザー入力を受け付けるツールを構築している場合、プロトコルの欠落を検出してユーザーに促すか、https://をフォールバックとして想定したほうが良いでしょう。
URLとURI
技術的には、URLはURIのサブセットです。URI(Uniform Resource Identifier)はリソースを識別します。URL(Uniform Resource Locator)はそれをどこで見つけるかも記述します(フェッチのためのスキームを含む)。実際には、ほとんどの開発者はこれらすべてに「URL」を使います。ただしurn:isbn:0451450523やmailto:user@example.comのようなものを解析する場合、これらはscheme://authority/pathパターンに従わないため、URLパーサーが一貫して処理しない場合があります。
フラグメントはクライアントサイドのみ
セキュリティの観点から重要なので繰り返します:#tokens、#access_token=abc123のようなもの — サーバーはそれを見ません。誰かがフラグメントに機密データを渡している場合、サーバーログには表示されませんが、ブラウザの履歴には残り、クライアントサイドのJavaScript(サードパーティスクリプトを含む)から見える可能性があります。
URLパーサーvs手動文字列分割 — いつどちらを使うか
split('?')やsplit('&')を実際のURLパーサーの代わりに使う開発者がいます(私もそうでした)。時にはそれで問題ありません!よく管理された入力に対する使い捨てのスクリプトなら、たぶん大丈夫です。
ただし、正直なルールを言えば:URLがユーザー入力、サードパーティAPI、または自分がコントロールしないシステムから来る場合は、本物のパーサーを使いましょう。エッジケース — エンコーディング、IPv6、不足しているポート、埋め込まれた認証情報、相対URL — は必ず現れ、手動の分割は大きく失敗する代わりに静かに間違った結果を生み出します。
JavaScriptの組み込みURL APIとPythonのurllib.parseは、ほぼすべてのユースケースに対して十分です。URL正規化、国際ドメイン向けのIDNAエンコーディング、または非標準スキームの特殊処理が必要な場合のみライブラリを使いましょう。
クイックな一回限りのURL検査には、toolboxhubs.com/en/tools/url-parserが便利です。URLを貼り付けてすべての構成要素をすぐに確認できます — 特にネストされたエンコーディングのあるURLをデバッグしていて、クエリ文字列に実際に何があるか不明な場合に重宝します。
実世界の例:GitHub API URLの解析
具体的な例でまとめましょう。GitHub APIを呼び出すスクリプトを構築していて、トークンを漏らさずにリクエストをログに記録したいとします。典型的な認証済みGitHub APIのURLはこんな感じです:
https://github-token:ghp_REDACTED@api.github.com/repos/my-org/my-repo/pulls?state=open&per_page=100&page=1
JavaScriptでの処理方法:
function sanitizeGitHubURL(rawUrl) {
let url;
try {
url = new URL(rawUrl);
} catch {
return '[invalid URL]';
}
// 埋め込まれた認証情報を削除
url.username = '';
url.password = '';
// 有用な情報は引き続き抽出可能
const info = {
host: url.hostname,
path: url.pathname,
params: Object.fromEntries(url.searchParams),
sanitized: url.toString(),
};
return info;
}
const result = sanitizeGitHubURL(
'https://github-token:ghp_abc123@api.github.com/repos/my-org/my-repo/pulls?state=open&per_page=100&page=1'
);
console.log(result);
// {
// host: 'api.github.com',
// path: '/repos/my-org/my-repo/pulls',
// params: { state: 'open', per_page: '100', page: '1' },
// sanitized: 'https://api.github.com/repos/my-org/my-repo/pulls?state=open&per_page=100&page=1'
// }
url.username = ''とurl.password = ''を設定してurl.toString()を呼び出すと、認証情報なしのクリーンなURLが得られます。ログに記録するのがずっと安全です。
まとめ
URLは開発者として常に視野の端にあるものです — 常に存在し、たいていは理解されていて、時々苛立たしい。パーセントの二重エンコードによるバグに遭遇したり、クエリパラメータのキーにブラケットが含まれている理由を10分かけて解明したりすると、URLを単純な文字列として扱うのをやめます。
重要なポイント:
- ユーザーが提供したものや外部ソースのURLには、文字列分割ではなく本物のパーサー(JavaScript
URLAPI、Pythonurllib.parse)を使う - フラグメントはクライアントサイドのみ — サーバーはそれを見ない
- 相対URLを正しく解決するにはベースが必要
- パーセントエンコーディングはクエリ文字列のキーと値の両方に適用される
- IPv6ホストは単純なコロン分割を壊す
- URLとURIは技術的には異なるが、ほぼすべての人がどちらも「URL」と呼ぶ
クイックな検査とデバッグには、ビジュアルなURLパーサーツールが時間を節約します。本番コードでは、標準ライブラリのパーサーは堅牢でよくテストされています — 特定のニーズがない限りサードパーティパッケージを使う必要はありません。
URL構造に慣れると、ウェブのデバッグの多くが明確になります:設定ミスのリダイレクトを見抜き、漏洩した認証情報を見つけ、APIコールが実際に何を送信しているかを追跡し、自分自身のURLについてより正確に推論できるようになります。少ない労力で大きな効果が得られるスキルの一つです。