URLは一見シンプルに見えます。しかしhttps://api.example.com:8443/v2/search?q=hello+world&page=1&sort=desc&filter%5B%5D=active&filter%5B%5D=paid#resultsのようなURLを目にすると、各部分が何をしているのか正確に把握したくなります。URLパーサーはURLを分解し、パーセントエンコーディングを頭の中でデコードする必要なく、各コンポーネントを検査できます。
URLの解剖
すべてのURLはRFC 3986で定義された標準的な構造に従います:
scheme://[userinfo@]host[:port]/path[?query][#fragment]
上記の例を分解すると:
| コンポーネント | 値 |
|---|---|
| スキーム | https |
| ホスト | api.example.com |
| ポート | 8443 |
| パス | /v2/search |
| クエリ文字列 | q=hello+world&page=1&sort=desc&filter%5B%5D=active&filter%5B%5D=paid |
| フラグメント | results |
URL を貼り付けると、ブラウザーの URL オブジェクトの各項目(protocol・username・password・host・hostname・port・pathname・search・hash・origin・URL 全体)と、デコード済みのクエリパラメーター表が表示されます(同じキーが繰り返される場合は値ごとに 1 行)。既定のポートは省かれ、https://example.com:443/ の port は空になります。URL にパスワードがあると password 欄は伏せ字になり、「表示」を押すと見えます。項目とパラメーターはその場で書き換えられ、URL が組み直されます。
コンポーネントの詳細
スキーム
スキームはプロトコルを指定します。主なスキーム:
| スキーム | 用途 |
|---|---|
https | セキュアHTTP |
http | 非暗号化HTTP(本番環境では非推奨) |
ws / wss | WebSocket |
ftp | ファイル転送 |
mailto | メールリンク |
data | インラインデータURI |
blob | ブラウザオブジェクトURL |
オーソリティ:ユーザー情報・ホスト・ポート
オーソリティコンポーネントは[userinfo@]host[:port]です。
ユーザー情報(user:password@host)は現代のURLではほとんど使われず、セキュリティリスクとして扱われています。URLの認証情報はサーバーログに記録されてしまいます。
ホストはドメイン名・IPv4アドレス・IPv6アドレス(角括弧必須:[::1])になります。登録可能なドメインが末尾 2 つのラベルとは限りません。shop.example.co.jp では example.co.jp で、これはパブリックサフィックスリストでしか判断できません。ZeroTool のパーサーは hostname をそのまま表示し、分割はしません。
ポートはオプションです。省略した場合、デフォルトが使われます。HTTPSは443、HTTPは80。明示的にデフォルトポートを指定することは(HTTPSで:443)有効ですが冗長です。
パス
パスはリソースを識別します。パスセグメントは/で区切られます。先頭と末尾のスラッシュは意味を持ちます。/api/usersと/api/users/はフレームワークによって異なるルーティングになる場合があります。
パスパラメーター(/users/:idの:id)はURL仕様の一部ではなく、ルーティングの慣習です。URLパーサーはリテラルのパスを表示し、ルーターがパターンを解釈します。
クエリ文字列
クエリ文字列は?以降#以前のすべてです。&で区切られたkey=valueペアの列です。
パーセントエンコーディング:URLで許可されていない文字は%XX(XXは16進コード)としてエンコードされます。主な例:
| 文字 | エンコード |
|---|---|
| スペース | %20または+ |
[ | %5B |
] | %5D |
# | %23 |
@ | %40 |
/ | %2F |
= | %3D |
スペースを + でエンコードするのは application/x-www-form-urlencoded 形式(HTML フォーム)に由来します。URLSearchParams もスペースを + で書き、encodeURIComponent は %20 で書きます。
繰り返しキー:filter%5B%5D=active&filter%5B%5D=paidはfilter[]=active&filter[]=paidにデコードされます。PHP・Rails・多くのフレームワークは繰り返しキーを配列として解釈します。
フラグメント
フラグメント(#results)はサーバーに送信されません。ブラウザだけが処理し、通常はそのIDを持つ要素にスクロールします。SPAはハッシュベースルーターでクライアントサイドルーティングにフラグメントを使用します。
URLパーサーが実際に必要な場面
APIデバッグ
ネットワークインスペクターからURLをコピーしてきました。エンコードされたクエリパラメーター・珍しいポートがあり、末尾の#がフラグメントなのかタイポなのか分かりません。パーサーに貼り付けると各コンポーネントが明確になります。
リダイレクトチェーンの分析
リダイレクトチェーンを追跡しています。チェーン内の各URLにパラメーターがあり、それらを比較する必要があります。各URLをパースすることで差異が明確になります。
セキュリティレビュー
長いURLにはクエリパラメーターに認証情報・内部ホスト名・エンコードされたペイロードが含まれていることがあります。URLをコンポーネントに分解することでこれらが見えやすくなります。クエリパラメーターのtoken=eyJ...やユーザー情報のadmin:password@internal.hostを発見できます。
プログラムによるURL構築
コードでURLを構築する際、各コンポーネントの正しいエンコーディングを理解することでバグを防げます:
// 誤り — クエリ値はエンコードが必要
const url = `https://api.example.com/search?q=${userInput}`;
// 正しい
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', userInput); // 自動でエンコード
コードでのURL解析
JavaScript(ブラウザ + Node.js)
URL APIはすべてのモダン環境で利用可能です:
const url = new URL('https://api.example.com:8443/v2/search?q=hello+world&page=1&filter%5B%5D=active&filter%5B%5D=paid#results');
console.log(url.protocol); // "https:"
console.log(url.hostname); // "api.example.com"
console.log(url.port); // "8443"
console.log(url.pathname); // "/v2/search"
console.log(url.hash); // "#results"
// クエリパラメーター
url.searchParams.get('q'); // "hello world"(デコード済み)
url.searchParams.get('page'); // "1"
url.searchParams.getAll('filter[]'); // ["active", "paid"]
// 全パラメーターを反復
for (const [key, value] of url.searchParams) {
console.log(key, value);
}
Python
from urllib.parse import urlparse, parse_qs
raw = "https://api.example.com:8443/v2/search?q=hello+world&page=1&filter[]=active&filter[]=paid#results"
parsed = urlparse(raw)
print(parsed.scheme) # https
print(parsed.hostname) # api.example.com
print(parsed.port) # 8443
print(parsed.path) # /v2/search
print(parsed.fragment) # results
params = parse_qs(parsed.query)
print(params['q']) # ['hello world']
print(params['filter[]']) # ['active', 'paid']
Go
import (
"fmt"
"net/url"
)
func main() {
raw := "https://api.example.com:8443/v2/search?q=hello+world&page=1#results"
u, _ := url.Parse(raw)
fmt.Println(u.Scheme) // https
fmt.Println(u.Hostname()) // api.example.com
fmt.Println(u.Port()) // 8443
fmt.Println(u.Path) // /v2/search
fmt.Println(u.Fragment) // results
fmt.Println(u.Query().Get("q")) // hello world
}
よくあるURLのミス
二重エンコード:すでにエンコードされたURLを再度エンコードしてしまう。%20が%2520になります(%25は%なので、%2520はスペースではなく%20にデコードされます)。常に生の値をエンコードし、すでにエンコードされた文字列は再エンコードしないでください。
フラグメントの除外を忘れる:サーバーがリクエストで#fragmentを探しても見つかりません。フラグメントはクライアント専用です。
順序の前提:クエリパラメーターの順序は保証されていません。a=1&b=2とb=2&a=1の順序に依存するロジックは作らないでください。
比較でのポートの欠落:https://example.comとhttps://example.com:443は同じURLですが、文字列比較では異なります。
まとめ
URLの解析はAPI作業・デバッグ・セキュリティレビューで頻繁に必要になります。構造(スキーム・オーソリティ・パス・クエリ・フラグメント)と各コンポーネントのエンコーディングを理解することで、バグを防ぎデバッグ時間を節約できます。