Webページを見ているとき、アドレスバーに %E3%81%82 のような文字列が表示されることがあります。これはURLエンコード(パーセントエンコーディング)と呼ばれる変換です。日本語や特殊文字をURLで安全に扱うための仕組みです。

仕組み自体は単純なのですが、実務では「スペースが + になったり %20 になったりする」「エンコードしたはずなのに文字化けする」といったつまずき方をしがちです。この記事では基本の仕組みに加えて、実際に不具合の原因になりやすいポイントまで整理します。

URLで使える文字と使えない文字

URLで使用できる文字はRFC 3986(インターネット標準仕様)によって定められています。英数字(A-Z、a-z、0-9)と - _ . ~ の4記号は「非予約文字」と呼ばれ、そのままURLに含めることができます。

一方、次の文字はURLの構造を区切るための「予約文字」です。

文字 URLでの役割
? パス部とクエリ文字列の区切り
& クエリパラメータ同士の区切り
= パラメータ名と値の区切り
# フラグメント(ページ内アンカー)の開始
/ パスの階層の区切り
+ クエリ文字列ではスペースを意味することがある

これらを「区切り」ではなく「ただのデータ」として扱いたいときは、エンコードが必要になります。日本語や絵文字などの非ASCII文字も、そのままではURLに含められません。

パーセントエンコーディングの仕組み

URLエンコードでは、使えない文字を %XX の形式に変換します(XXは16進数2桁)。文字をUTF-8でバイト列に変換し、各バイトを % と16進数2桁で表す、というだけの単純な変換です。

元の文字 UTF-8バイト列 URLエンコード後
E3 81 82 %E3%81%82
東京 E6 9D B1 E4 BA AC %E6%9D%B1%E4%BA%AC
スペース 20 %20 または +
& 26 %26
% 25 %25

「あ」はUTF-8で3バイトの文字なので、エンコードすると %E3%81%82 という9文字に膨らみます。日本語のキーワードを含むURLが極端に長くなるのはこのためです。

なお、変換の基準になる文字コードはUTF-8とは限りません。2000年代前半の日本語サイトではShift_JISが使われていたため、同じ「あ」でも %82%A0 とエンコードされていました。古いシステムから引き継いだURLをデコードして文字化けする場合は、UTF-8以外でエンコードされている可能性を疑ってみてください。

スペースは %20+

実務で最も混乱しやすいのがスペースの扱いです。どちらも正解ですが、使う場所が違います。

形式 使う場所 由来
%20 URLのどこでも使える RFC 3986(URLの一般仕様)
+ クエリ文字列の中だけ HTMLフォームの送信形式 application/x-www-form-urlencoded

つまり + は「URLの仕様」ではなく「HTMLフォームの仕様」に由来するものです。そのため、パス部分に書いた + はスペースになりません

https://example.com/search?q=a+b   → q は「a b」として解釈される
https://example.com/a+b/           → パスの「a+b」はプラス記号のまま

そして厄介なのが、データとして本物の + を渡したいときです。クエリ文字列に + をそのまま書くとスペースとして解釈されてしまうため、%2B にエンコードする必要があります。「C++」や「1+1」を検索フォームに渡すと結果がおかしくなる、という不具合はこれが原因であることがほとんどです。

encodeURIencodeURIComponent の使い分け

JavaScriptにはURLエンコード用の関数が2つあり、取り違えるとバグになります。

const url = 'https://example.com/?q=1';

encodeURI(url);
// → https://example.com/?q=1     ... 記号がそのまま残る

encodeURIComponent(url);
// → https%3A%2F%2Fexample.com%2F%3Fq%3D1  ... 記号もすべて変換される

使い分けの基準はシンプルです。

  • encodeURIComponent … パラメータの「値」をエンコードするとき。? & = / も変換されるため、値の中に区切り文字が入っていても構造が壊れません。基本的にはこちらを使います。
  • encodeURI … URL全体を1本まとめてエンコードするとき。区切り文字は変換されないため、URLの構造は保たれます。日本語を含むURLをそのまま扱いたい場面などに限られます。

リダイレクト先のURLをパラメータで渡す場面を考えると違いが明確になります。

// NG: 渡したいURLの ? や & が区切りとして解釈され、構造が壊れる
'/login?next=' + encodeURI('/search?q=あ&page=2');

// OK: 値としてまるごと安全に埋め込める
'/login?next=' + encodeURIComponent('/search?q=あ&page=2');

PHPにも同様に2種類あり、rawurlencode() がRFC 3986準拠(スペースは %20)、urlencode() がフォーム形式(スペースは +)です。URLのパスを組み立てるときは rawurlencode() を使うのが安全です。

二重エンコードという事故

エンコード済みの文字列をもう一度エンコードしてしまうのが「二重エンコード」です。% 自体が %25 に変換されるため、こうなります。

あ            → %E3%81%82        (1回目)
%E3%81%82     → %25E3%2581%2582  (2回目)

こうなったURLをデコードしても、1回では %E3%81%82 という文字列が返ってくるだけで、日本語には戻りません。%25 が並んでいたら二重エンコードを疑う、と覚えておくと原因の切り分けが早くなります。

よくある発生パターンは次のようなものです。

  • フレームワークが自動でエンコードしているのに、手動でもエンコードしてしまった
  • ブラウザのアドレスバーからコピーした(すでにエンコード済みの)URLを、そのままエンコードして使った
  • リダイレクトを複数回経由するうちに、各段階でエンコードが重ねがけされた

実例:エンコードされたURLのせいで原因究明が止まった話

範囲検索の機能を作ったときに、実際に起きたトラブルです。

なお、この記事に登場する項目名・日付・URLは、実際のシステムで使われていたものではありません。起きたことはそのままに、説明しやすい例へ置き換えています。

検索条件はGETパラメータで渡す作りで、パラメータ名にはクライアントが画面上で設定した項目名をそのまま使う仕様でした。項目名は日本語なので、実際に送信されるURLはこうなります。

?製造開始日=2026-01-01&製造終了日=2026-03-31

↓ 実際に送信されるURL

?%E8%A3%BD%E9%80%A0%E9%96%8B%E5%A7%8B%E6%97%A5=2026-01-01&%E8%A3%BD%E9%80%A0%E7%B5%82%E4%BA%86%E6%97%A5=2026-03-31

検索は画面のボタンから実行される前提だったため、パラメータが揃っているかどうかの検証は入れていませんでした。

ボタンから検索するから大丈夫だろ、という慢心があったわけですね。

ある日の仕事中、エラーが起きたという問い合わせが飛んできました。ログを確認すると404が発生しており、内部的には $params['製造終了日'] が存在せず、値を取得できないまま404ページへ落ちていたのです。

ところが、ログに残っているURLはエンコードされたままです。目に入るのは %E8%A3%BD%E9%80%A0%E7%B5%82 のような16進数の羅列で、どこがおかしいのかすぐに判断できませんでした。ユーザー側も同じで、エンコードされたURLのまま検索していたため、自分の使っているURLに問題があるとは気づいていませんでした。

そのため、実際に使われたURLをデコードしてようやく分かりました。

%E8%A3%BD%E9%80%A0%E9%96%8B%E5%A7%8B%E6%97%A5=2026-01-01&%E8%A3%BD%E9%80%A0%E7%B5%82

↓ デコード

製造開始日=2026-01-01&製造終

URLが 製造終 で終わっていました。= も値もありません。パラメータ名の途中でURLが切れていたのです。URLをコピーして貼り付ける際に、末尾まで選択しきれていなかったのだと思われます。

URLエンコード/デコードツールに、途中で切れたURLを貼り付けてデコードした画面
84文字あったエンコード済みのURLが、デコードすると20文字の「製造開始日=2026-01-01&製造終」になる。パラメータ名の途中で切れていることがここで初めて見える

原因が分かるまでにかかったのは15分ほど。ミス自体は単純なものでしたが、エンコードされた状態のURLを眺めている限り、この15分は永遠に終わりませんでした。

この件から得た教訓は2つあります。

  • エンコードされたURLは、目視で異常を見つけられない。 障害調査でURLを見るときは、まずデコードしてから読む——と決めておくだけで切り分けが早くなります。
  • GETパラメータは「必ず揃っている」前提で書かない。 画面のボタン経由でしか来ないはずのリクエストでも、URLは編集もコピペもされます。この件のあと、欠損時のデフォルト値とエラーメッセージの表示を追加しました。

なお当時は自分用のツールを持っておらず、外部のデコードサイトに業務システムのURLを貼り付けるしかありませんでした。業務で扱うURLを外部サービスへ送信することには、正直かなり抵抗がありました。 ブラウザ内だけで完結する変換ツールを手元に持っておきたい——このサイトを作り始めた理由のひとつは、こうした場面の積み重ねです。

実務でURLエンコードが必要になる場面

  • クエリパラメータに日本語を含める場合:検索フォームで日本語を入力してURLを見ると ?q=%E6%A4%9C%E7%B4%A2 のようになります。ブラウザが自動的にエンコードしているためです。
  • APIリクエストを手動で組み立てる場合:パラメータ値をエンコードしないと、値に含まれる &= が区切り文字として誤解釈されます。
  • URLを別のURLのパラメータに含める場合:リダイレクト先の指定など。encodeURIComponent が必須になる典型例です。
  • 日本語ファイル名へのリンク:ファイル名部分をエンコードしないと、サーバーによっては404になります。
  • ログやアクセス解析の確認:アクセスログに記録されたURLはエンコードされたままなので、デコードしないと何を検索されたのか読めません。

よくある質問

Q. エンコードされたURLをデコードすると文字化けします

A. エンコード時の文字コードがUTF-8ではない可能性があります。%82%A0 のように 80〜9F 台のバイトが多い場合はShift_JISの疑いが濃厚です。また、%25 が含まれていれば二重エンコードなので、2回デコードすれば元に戻ります。

Q. 日本語のURLをそのままアドレスバーに貼っても動くのはなぜですか

A. ブラウザが送信時に自動でエンコードしているためです。アドレスバーには読みやすさのためデコードした状態で表示されますが、実際にサーバーへ送られているのはエンコード後のURLです。

Q. - _ . ~ はエンコードしなくてよいのですか

A. RFC 3986で非予約文字とされているため、エンコードは不要です。ただし古いライブラリの中には ~%7E に変換するものもあります。どちらでも意味は同じですが、署名やハッシュの計算対象にURLを使う場合は表記が一致しないと検証に失敗するため注意が必要です。

Q. スペースを %20+ のどちらにすべきか迷います

A. 迷ったら %20 を選んでおくと安全です。%20 はURLのどこでも同じ意味になりますが、+ がスペースとして扱われるのはクエリ文字列の中だけだからです。

Q. URLが途中で切れているかどうかを見分けるコツはありますか

A. デコードしてみるのが確実です。パラメータ名や値が不自然な位置で終わっていれば、そこで切れています。また %XX の途中で切れている場合は、デコード自体が失敗したり、文字化けした記号が現れたりします。エンコードされたままの状態では、URLが長いほど異常に気づけません。

まとめ

URLエンコードは「使えない文字をUTF-8のバイト列に分解して %XX で表す」という単純な仕組みですが、実務でつまずくのはたいてい仕組みそのものではなく、その周辺にあります。

  • スペースの %20+ は、由来も使える場所も違う
  • JavaScriptでは基本的に encodeURIComponent を使う
  • %25 を見かけたら二重エンコードを疑う
  • デコードして文字化けするなら、UTF-8以外を疑う
  • 障害調査でURLを見るときは、まずデコードしてから読む

以下のURLエンコード / デコードツールでは、貼り付けたテキストの変換結果をその場で確認できます。デコードして日本語に戻るかどうかを見れば、二重エンコードや文字コードの食い違い、URLの途切れをすぐに切り分けられます。

変換はすべてブラウザ内で行われ、入力した内容が外部に送信されることはありません。業務システムのURLやログの一部を貼り付けても、そのデータが手元から出ていくことはないので、調査用途にも安心して使えます。