- 技術 💻
URLエンコード・デコード変換ツール|%E3%81%82の読み方と言語別の違い
結論
URLエンコードは、URLにそのまま書けない文字を、文字コード(ふつうは UTF-8)のバイトに分けて「%+16進数2桁」で書く変換です。「あ」は UTF-8 で E3 81 82 の3バイトなので %E3%81%82 になります。デコードはその逆で、%XX をバイトに戻してから文字として読みます。
この記事でわかること
- %E3%81%82 の読み方と、よく見る %20・%2B・%2F・%25 の意味
- encodeURIComponent・quote・urlencode など6言語の関数で同じ文字列を変換した実物と違いの表
- + と %20、二重エンコード(%25)、Shift_JIS の % で文字化けする原因と直し方
URL エンコード・デコードを読み込み中です。
ツールの使い方:上の入力欄に文字列を貼ると、その場で変換結果が出ます。ボタンを押す操作はありません。
- デコード:%XX を含む文字列を貼ると、いちばん上に「デコードした結果」が出ます。文字コードは UTF-8・Shift_JIS・EUC-JP から選べます。「デコードで + を空白として読む」のチェックを外すと、+ を + のまま残します。読めない文字(�)があるとその数を、無いときは % の後ろが16進数2桁でない「壊れた %」の数を表示します。
- エンコード:入力した文字列を、encodeURIComponent 相当・encodeURI 相当・フォーム送信の形(空白→+)の3通りに同時に変換します。それぞれに「コピー」ボタンがあります。選んだ文字コードはエンコードにも使われます。
- 見本:「日本語のURL」「%の形を読む」「Shift_JIS の%」「+ を含む」の4つのボタンで、よくある入力を試せます。
- 入力した内容はこの端末の中だけで変換し、どこにも送信しません。社内の URL やログもそのまま貼れます。
%XX を含む文字列を貼ったときも、エンコードの3つの欄には「入力をさらにエンコードした形」(% が %25 になる)が出ます。元の文字に戻したいときは、いちばん上の「デコードした結果」だけを見ます。
この記事で実際に動かしたもの:上のツール(Chromium で表示して9通りの入力を確認)、JavaScript・Python・PHP・Go・Ruby の標準関数、curl、127.0.0.1 の自作サーバーで受け取ったブラウザとフォームのリクエスト。
よく調べられることの答えを先にまとめます。
| やりたいこと | いちばん早い方法 | 詳しい節 |
|---|---|---|
| %E3%81%82 などを日本語に戻したい | 上のツールに貼る(UTF-8 のまま) | %E3%81%82の読み方 |
| 日本語を URL に入れたい | 値だけを encodeURIComponent 相当で変換 | encodeURIComponentとencodeURIの違い |
| %20 と + どちらが正しい | パスは %20。フォームの値はどちらでも空白として読まれることが多い | +と%20の違い |
| Python で変換したい | 値は quote(s, safe='')、クエリ全体は urlencode | PythonのURLエンコード |
| PHP の urlencode と rawurlencode | パスは rawurlencode、クエリは http_build_query | PHP・Go・Rubyの違い |
| デコードすると文字化けする | 文字コードを Shift_JIS・EUC-JP に変えて読む | Shift_JIS・EUC-JPの% |
| %25 が混ざって読めない | 二重エンコード。2回デコードし、送る側を直す | 二重エンコード(%25) |
| URIError: URI malformed が出た | 壊れた % か、UTF-8 でない % が入っている | よくあるエラー |
flowchart TD
A[文字 あ] -->|UTF-8 でバイトにする| B[E3 81 82]
B -->|1バイトずつ % と16進数2桁| C[%E3%81%82]
C -->|デコード| D[バイト E3 81 82]
D -->|UTF-8 として読む| A
確認環境:Ubuntu 24.04.5 LTS の上で、Node.js 22.23.4、Python 3.13.18、PHP 8.3.6(mbstring)、Go 1.24.7、Ruby 3.3.6、curl 8.5.0、jq 1.7 の標準の関数・コマンドで同じ文字列を変換し、出力をそのまま載せました。ブラウザの動きは Playwright から Chromium 153.0.8010.12 を起動し、127.0.0.1 に立てた自作の Python サーバーで、アドレスバーの URL と、UTF-8・Shift_JIS のページのフォーム送信(GET・POST)で実際に届いたリクエスト行と本文を記録しました。記事内ツールは、テーマの code.js を読み込んだページを Chromium(幅390px)で開き、9通りの入力で表示を確かめました。Java は既存の記事、Excel の ENCODEURL 関数と PowerShell は公式ドキュメントの範囲で書いています。外部のサービスには接続していません(確認日 2026年11月7日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「URLエンコード・デコード変換ツール|%E3%81%82の読み方と言語別の違い」
URLエンコードとは(パーセントエンコーディングの意味)
この節では、URLエンコードが何のための変換で、デコードとどういう関係なのかが分かります。
URLエンコードは、URL の中で特別な意味を持つ文字や、URL に書けない文字を「%」と16進数2桁の並びに置き換える決まりです。仕様(RFC 3986)での正式な名前は「パーセントエンコーディング(Percent-Encoding)」で、URI エンコードと呼ばれることもあります。
URL は、もともと英数字と一部の記号だけで書く前提で作られています。そのため、次の2種類の文字はそのままでは困ります。
- 区切りとして意味を持つ記号:
?(ここからクエリ)、&(パラメーターの区切り)、=(名前と値の区切り)、#(ここからフラグメント)、/(パスの区切り)など。値の中にこれらがあると、区切りと見分けがつかなくなります。 - 英数字と記号以外の文字:日本語、空白、改行、絵文字など。
たとえば検索語が「C++ 入門」のとき、そのまま ?q=C++ 入門 と書くと、受け取る側は + を空白と読み「C 入門」になります(後の節で実際に起きた結果を載せます)。値を C%2B%2B%20%E5%85%A5%E9%96%80 にしておけば、区切りと値がはっきり分かれます。
1エンコード
文字 → バイト → %XX。日本語や記号を URL に安全に入れるための変換。「あ」→「%E3%81%82」。
2デコード
%XX → バイト → 文字。受け取った側やログを読む人が、元の文字に戻す変換。「%E3%81%82」→「あ」。
3暗号ではない
決まった表で置き換えるだけなので、誰でも元に戻せます。パスワードや個人情報を隠す目的には使えません。
似た名前の変換に Base64 があります。Base64 はバイト列全体を64種類の文字で書き直す方式で、URL エンコードとは目的も結果も違います。Base64 の各言語のコードは関連記事「Base64エンコード各言語コード一覧|sha256ハッシュのコマンド早見表」で扱っています。
%E3%81%82の読み方(UTF-8のバイトに戻す)
この節では、%E3%81%82 のような文字列を、どこで区切ってどう読めば「あ」になるのかが分かります。
%XX の1つが1バイトです。%E3%81%82 は E3・81・82 の3バイトで、UTF-8 では日本語のかな・漢字の多くが3バイトになるため、3つで1文字です。手で読む手順は次のとおりです。
% で区切る
%E3 %81 %82 の3つに分ける。% の後ろの2文字が16進数(0〜9・A〜F)で1バイト。
先頭のバイトで文字の長さを知る
E3 は2進数で 1110 0011。先頭が 1110 なら「3バイトで1文字」、110 なら2バイト、11110 なら4バイト(絵文字など)、0 なら1バイト(英数字)。
印の部分を除いてつなぐ
11100011・10000001・10000010 から、先頭の 1110 と、続きのバイトの 10 を除いた 0011・000001・000010 をつなぐと 0011000001000010。
Unicode の番号として読む
0011000001000010 は16進数で 3042。U+3042 は「あ」。
Python で同じ計算をすると、次のとおりになりました。
c = 'あ'; b = c.encode()
print(hex(ord(c)), b.hex(' '), ' '.join(f'{x:08b}' for x in b))
bits = ''.join(f'{x:08b}' for x in b)
p = bits[4:8] + bits[10:16] + bits[18:24]
print(p, hex(int(p, 2)))
0x3042 e3 81 82 11100011 10000001 10000010
0011000001000010 0x3042
実際に読むときは、手で計算するより変換ツールや1行のコマンドを使います。自分で確かめる方法を2つ挙げます。
- ブラウザの開発者ツール(F12 → Console)で
decodeURIComponent('%E3%81%82')と打つと'あ'が返ります。 - ターミナルで
python3 -c "import sys,urllib.parse as u;print(u.unquote(sys.argv[1]))" '%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91'と打つと「文字化け」と出ました。
1文字が何文字の %XX になるか
UTF-8 では文字の種類でバイト数が変わるので、エンコード後の長さも変わります。Python の quote(s, safe='') で数えた結果です。
日本語29文字を含む記事のスラッグ(java-urlエンコードデコード入門-と20の違い早見表)は、UTF-8 で173文字になりました。半角カナの「ア」も UTF-8 では3バイト(%EF%BD%B1)です。日本語の多い URL が長くなるのはこのためです。
よく見る%XXの早見表(%20・%2B・%2F・%E3%80%80)
この節では、ログや URL でよく見かける %XX が、元はどの文字なのかが一目で分かります。
英数字以外の記号は、ASCII の番号をそのまま16進数2桁にしたものです。次の表は Python の quote(文字, safe='') で出した値です。
| %XX | 元の文字 | よく出る場面 |
|---|---|---|
| %20 | 半角空白 | 検索語・ファイル名の空白 |
| %2B | + | C++・電話番号の +81。+ のままだと空白に読まれることがある |
| %2F | / | 値の中のパス・日付(2026/11/07) |
| %3F・%26・%3D | ? ・& ・= | リダイレクト先の URL をパラメーターで渡すとき |
| %23 | # | ハッシュタグ・色コード(#ff8800) |
| %25 | % | 二重エンコードの目印(%E3 → %25E3) |
| %3A・%40 | : ・@ | メールアドレス・時刻 |
| %22・%3C | " ・< | 引用符・HTML の断片 |
| %0A・%0D%0A | 改行(LF・CRLF) | テキストエリアの値 |
| %E3%80%80 | 全角空白 | 日本語の検索語の区切り |
%E3 で始まる3つ組は、ひらがな・カタカナ・記号に多く出ます。%E4〜%E9 で始まる3つ組は多くが漢字、%EF%BC〜%EF%BD は全角英数字や半角カナ、%F0 で始まる4つ組は絵文字です。
URLにそのまま使える文字・使えない文字(RFC 3986)
この節では、仕様のうえでどの文字がエンコードなしで使え、どの文字が区切りとして予約されているのかが分かります。
RFC 3986 では、英字(A〜Z・a〜z)、数字(0〜9)、- . _ ~ の4つの記号を「非予約文字(unreserved)」と呼び、いつでもそのまま書けます。この文字をわざわざ %XX にしても同じ URL として扱われ、作る側は %XX にすべきでない(should not)とされています。
| 分類 | 文字 | 扱い |
|---|---|---|
| 非予約文字 | A〜Z a〜z 0〜9 - . _ ~ | そのまま書く |
| 予約文字(gen-delims) | : / ? # [ ] @ | URL の区切り。値の中にあるときは %XX にする |
| 予約文字(sub-delims) | ! $ & ' ( ) * + , ; = | 部分ごとの区切り。値の中にあるときは %XX にする |
| それ以外 | 空白・" < > \ ^ ` { | } ・日本語・改行 | 必ず %XX にする |
仕様には、ほかに次の決まりがあります。
- 16進数は大文字に:%e3 と %E3 は同じ意味ですが、作る側は大文字を使うべき(should)とされています。後の節のとおり、curl は小文字で送りました。
- 同じ文字列を2回エンコード・デコードしない:2回エンコードすると % が %25 になり、別の文字列になります(二重エンコード)。
- デコードは区切りを見分けたあと:先に全体をデコードすると、値の中の %26(&)が区切りの & に化けます。
RFC 3986 は文字コードを決めていません。今の Web では UTF-8 が標準で、ブラウザの URL の仕様(WHATWG URL Standard)も UTF-8 でエンコードします。Shift_JIS などで % を作る仕組みは、後の節で扱います。
encodeURIComponentとencodeURIの違い(JavaScript)
この節では、JavaScript の2つの関数が何を変換し何を残すのか、どちらを使うべきかが分かります。
encodeURIComponent は「値」用で、/ ? & = # なども変換します。encodeURI は「URL 全体」用で、URL の区切りを残し、日本語や空白だけを変換します。Node.js 22 で同じ文字列を渡した結果です。
const t = 'https://example.com/検索?q=C++ 入門&x=a/b#top';
console.log('encodeURI :', encodeURI(t));
console.log('encodeURIComp :', encodeURIComponent(t));
console.log('URLSearchParams:', new URLSearchParams({q: 'C++ 入門', x: 'a/b'}).toString());
console.log('new URL().href :', new URL('https://example.com/検索?q=C++ 入門#top').href);
console.log('escape(あ) :', escape('あ'));
encodeURI : https://example.com/%E6%A4%9C%E7%B4%A2?q=C++%20%E5%85%A5%E9%96%80&x=a/b#top
encodeURIComp : https%3A%2F%2Fexample.com%2F%E6%A4%9C%E7%B4%A2%3Fq%3DC%2B%2B%20%E5%85%A5%E9%96%80%26x%3Da%2Fb%23top
URLSearchParams: q=C%2B%2B+%E5%85%A5%E9%96%80&x=a%2Fb
new URL().href : https://example.com/%E6%A4%9C%E7%B4%A2?q=C++%20%E5%85%A5%E9%96%80#top
escape(あ) : %u3042
記号ごとに「変換せずに残したもの」を調べると、次のとおりでした。
chars kept by encodeURIComponent: !'()*~-_.
chars kept by encodeURI: !#$&'()*+,/:;=?@~-_.
encodeURIComponent(値に使う)
残すのは英数字と - _ . ! ~ * ' ( ) だけ
/ ? & = # + も %XX にする
クエリの値・パスの1区間・リダイレクト先の URL
encodeURI(URL 全体に使う)
URL の区切り : / ? & = # を残す
+ も残すので C++ の + は空白に読まれうる
日本語の URL を貼れる形にするときだけ
上の encodeURI の結果では q=C++%20… と + が残りました。受け取る側がフォームの形で読むと、この + は空白になります。値を組み立てるときは encodeURIComponent か URLSearchParams を使い、encodeURI は「できあがった URL を貼れる形にする」用途に限ります。
- URLSearchParams は空白を + にするフォームの形で、/ も %2F にしました。URL のパラメーターを読む・書くときの使い方は関連記事「JavaScriptでURLパラメータを取得|URLSearchParams早見表」で扱っています。
- new URL() は、日本語のパスとクエリを自動で %XX にしました。クエリの + はそのまま残ります。
- escape() は
%u3042という独自の形を出しました。UTF-8 の %XX ではないため、サーバー側の標準の関数では読めません。MDN でも非推奨とされています。
RFC 3986 の予約文字 ! ' ( ) * まで %XX にしたいときは、MDN に載っている次の書き方を使います。
function encodeRFC3986URIComponent(str) {
return encodeURIComponent(str).replace(
/[!'()*]/g,
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
);
}
+と%20の違い(フォーム送信の形)
この節では、空白が + になる場合と %20 になる場合の違いと、+ が空白に化ける原因が分かります。
%20 は RFC 3986 のパーセントエンコーディングでの空白、+ は HTML フォームの送信形式(application/x-www-form-urlencoded)での空白です。フォームの形では、英数字と * - . _ だけを残し、空白を + に、+ そのものを %2B にします(WHATWG URL Standard)。
Chromium 153 で、UTF-8 のページのフォームを送ったとき、127.0.0.1 の自作サーバーに届いたリクエストは次のとおりでした。GET でも POST でも、空白は + になりました。
GET /r?q=%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91+%E5%AF%BE%E7%AD%96
POST /r body=q=C%2B%2B+%E5%85%A5%E9%96%80 ct=application/x-www-form-urlencoded
一方、アドレスバーに http://127.0.0.1:31313/検索?q=文字化け 対策&lang=ja#上 と入れて開いたときは、空白が %20 になりました。
location.href: http://127.0.0.1:31313/%E6%A4%9C%E7%B4%A2?q=%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91%20%E5%AF%BE%E7%AD%96&lang=ja#%E4%B8%8A
GET /%E6%A4%9C%E7%B4%A2?q=%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91%20%E5%AF%BE%E7%AD%96&lang=ja
フラグメント(# の後ろ)はサーバーには届かず、ブラウザの中だけで %E4%B8%8A になっていました。
デコードする側で + をどう読むか
+ を空白として読むかどうかは、デコードする関数で決まります。同じ C%2B%2B+x を読ませた結果です。
| 関数 | + の扱い | C%2B%2B+x の結果 |
|---|---|---|
| JavaScript decodeURIComponent | + のまま | C+++x |
| JavaScript URLSearchParams.get | 空白にする | C++ x(q=C%2B%2B+%E5%85%A5%E9%96%80 → C++ 入門) |
| Python unquote | + のまま | C+++x |
| Python unquote_plus・parse_qs | 空白にする | C++ x |
| PHP rawurldecode | + のまま | C+++x |
| PHP urldecode | 空白にする | C++ x |
「パスの空白は %20、クエリの値はフォームの形なら +」と覚えると整理できます。パスに + を使うと、多くのサーバーは + を文字の + として扱います。迷ったら空白は %20、+ は %2B にしておけば、どちらの読み方をする相手にも正しく届きます。記事内ツールでは「デコードで + を空白として読む」のチェックで両方の読み方を切り替えられます。
PythonのURLエンコード・デコード(quote・quote_plus・urlencode)
この節では、Python の urllib.parse の関数ごとの違いと、既定値で起きやすい間違いが分かります。
quote は既定で / を残し、空白を %20 にします。quote_plus は / も変換し、空白を + にします。urlencode は辞書からクエリ文字列を作り、中で quote_plus を使います。Python 3.13.18 で同じ文字列を渡した結果です。
import urllib.parse as u
s = 'C++ 入門/a&b=c'
print('quote :', u.quote(s))
print("quote(safe='') :", u.quote(s, safe=''))
print('quote_plus :', u.quote_plus(s))
print('urlencode :', u.urlencode({'q': 'C++ 入門', 'x': 'a/b'}))
print('urlencode(quote_via=quote):', u.urlencode({'q': 'C++ 入門', 'x': 'a/b'}, quote_via=u.quote))
quote : C%2B%2B%20%E5%85%A5%E9%96%80/a%26b%3Dc
quote(safe='') : C%2B%2B%20%E5%85%A5%E9%96%80%2Fa%26b%3Dc
quote_plus : C%2B%2B+%E5%85%A5%E9%96%80%2Fa%26b%3Dc
urlencode : q=C%2B%2B+%E5%85%A5%E9%96%80&x=a%2Fb
urlencode(quote_via=quote): q=C%2B%2B%20%E5%85%A5%E9%96%80&x=a%2Fb
quote(s) の結果で / が残っている点に注意が要ります。値の中の / を変換したいときは safe='' を付けます。~ は Python 3.7 から RFC 3986 に合わせて変換しなくなりました。
デコード側は次のとおりでした。
unquote(%E3%81%82) : あ
unquote(C%2B%2B+x) : C+++x
unquote_plus(C%2B%2B+x) : C++ x
unquote(%82%A0) as utf-8 : '��'
unquote(%82%A0, encoding='shift_jis'): あ
unquote(%E3%81, errors='strict') -> UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-1: unexpected end of data
unquote(100%) : 100%
parse_qs : {'q': ['C++ 入門', '2']}
unquote_to_bytes : b'\xe3\x81\x82'
- unquote は既定で errors='replace':UTF-8 として読めないバイトは例外にならず、� に置き換わります。文字化けに気づかないまま保存しやすいので、検査したいときは
errors='strict'を付けます。 - 壊れた % はそのまま:
100%は例外にならず、そのまま返りました。 - parse_qs は + を空白にし、同じ名前が2回あればリストにします。
- 文字コードの指定:
quote('あ', encoding='shift_jis')は %82%A0、encoding='euc-jp'は %A4%A2 でした。
PHP・Go・Rubyの関数の違い(urlencodeとrawurlencode)
この節では、PHP・Go・Ruby の標準の関数が、空白と ~ と / をどう変換するかが一覧で分かります。
名前に「raw」「Path」が付く関数は RFC 3986 寄り(空白→%20)、「urlencode」「Query」「www_form」が付く関数はフォームの形(空白→+)です。同じ C++ 入門/a&b=c~ を渡した結果です。
PHP 8.3.6
urlencode : C%2B%2B+%E5%85%A5%E9%96%80%2Fa%26b%3Dc%7E
rawurlencode : C%2B%2B%20%E5%85%A5%E9%96%80%2Fa%26b%3Dc~
http_build_query : q=C%2B%2B+%E5%85%A5%E9%96%80&x=a%2Fb
http_build_query(RFC3986) : q=C%2B%2B%20%E5%85%A5%E9%96%80&x=a%2Fb
go1.24.7
QueryEscape: C%2B%2B+%E5%85%A5%E9%96%80%2Fa%26b%3Dc~
PathEscape : C++%20%E5%85%A5%E9%96%80%2Fa&b=c~
Values.Encode: q=C%2B%2B+%E5%85%A5%E9%96%80&x=a%2Fb
Ruby 3.3.6
encode_www_form_component: C%2B%2B+%E5%85%A5%E9%96%80%2Fa%26b%3Dc%7E
ERB::Util.url_encode : C%2B%2B%20%E5%85%A5%E9%96%80%2Fa%26b%3Dc~
URI.encode_uri_component : C%2B%2B%20%E5%85%A5%E9%96%80%2Fa%26b%3Dc%7E
6言語の結果を、空白・~・/・+ の4点でまとめます(Java は既存の記事へ)。
| 関数 | 空白 | ~ | / | + |
|---|---|---|---|---|
| JS encodeURIComponent | %20 | ~ | %2F | %2B |
| JS encodeURI | %20 | ~ | / | +(残る) |
| JS URLSearchParams | + | ~ | %2F | %2B |
| Python quote | %20 | ~ | /(既定で残る) | %2B |
| Python quote_plus・urlencode | + | ~ | %2F | %2B |
| PHP urlencode | + | %7E | %2F | %2B |
| PHP rawurlencode | %20 | ~ | %2F | %2B |
| Go url.QueryEscape | + | ~ | %2F | %2B |
| Go url.PathEscape | %20 | ~ | %2F | +(残る) |
| Ruby encode_www_form_component | + | %7E | %2F | %2B |
| jq @uri | %20 | ~ | %2F | %2B |
目立つ違いは3つです。PHP の urlencode と Ruby の encode_www_form_component は ~ を %7E にする、Go の PathEscape は + と & と = を残す(パスの1区間では区切りでないため)、Python の quote は / を残すです。署名付きの URL(API の署名・S3 の署名など)は「どの文字を変換するか」まで決まっているので、1文字違うだけで署名が合わなくなります。
Java の URLEncoder.encode は空白を + にするフォームの形で、%20 にしたいときの書き方は関連記事「Java URLエンコード・デコード入門|+と%20の違い早見表」で扱っています。
Excel・PowerShell
Excel には ENCODEURL(文字列) 関数があります。Microsoft のサポートページによると、Excel for Microsoft 365・2024・2021・2019・2016 で使え、Excel for the web と Mac では使えません。PowerShell では .NET の [uri]::EscapeDataString() と [uri]::UnescapeDataString() が使えます(Microsoft Learn の Uri クラスの説明の範囲。手元では動かしていません)。
curl --data-urlencodeとブラウザが実際に送る形
この節では、curl で日本語や + を含む値を送ると何が届くのか、エンコードを忘れると何が起きるのかが分かります。
curl の -d(--data)は値をエンコードせずにそのまま送ります。日本語や + を含む値は --data-urlencode を使います。127.0.0.1 の自作サーバーに届いたリクエストを並べます。
curl --data-urlencode 'q=C++ 入門' http://127.0.0.1:31313/a
curl -d 'q=C++ 入門' http://127.0.0.1:31313/b
curl -G --data-urlencode 'q=C++ 入門' --data-urlencode 'x=a/b' http://127.0.0.1:31313/c
curl --url-query 'q=C++ 入門' http://127.0.0.1:31313/d
curl 'http://127.0.0.1:31313/検索?q=文字化け'
POST /a body=q=C%2B%2B+%E5%85%A5%E9%96%80 ct=application/x-www-form-urlencoded
POST /b body=q=C++ å¥é ct=application/x-www-form-urlencoded
GET /c?q=C%2b%2b+%e5%85%a5%e9%96%80&x=a%2fb
GET /d?q=C%2b%2b+%e5%85%a5%e9%96%80
GET /%e6%a4%9c%e7%b4%a2?q=æååã
- -d のまま(/b):+ も日本語もそのまま送られました(本文の後半はサーバーのログで UTF-8 のバイトを Latin-1 として表示したもの)。受け取る側がフォームの形で読むと、+ は空白になり「C 入門」になります。
- --data-urlencode(/a):= の後ろだけをフォームの形(空白→+、+→%2B)でエンコードしました。
- -G と --url-query(/c・/d):クエリに付けると、16進数が小文字(%2b・%e5)になりました。意味は同じですが、文字列で比べる処理(署名の検証・キャッシュのキー)では違う値として扱われます。
- URL に日本語を直接書いた場合:curl 8.5.0 はパスの日本語は %xx にしましたが、クエリの日本語は UTF-8 のバイトのまま送りました。受け取る側によって結果が変わるので、URL は先にエンコードしておきます。
URL の中に空白を入れると、curl は送る前に止まりました。
$ curl -sS -o /dev/null 'http://127.0.0.1:31313/x?q=a b'
curl: (3) URL rejected: Malformed input to a URL function
exit=3
すでにエンコード済みの値は、--url-query '+q=C%2B%2B' のように先頭に + を付けると、そのまま付けられます(このときも q=C%2b%2b と小文字で送られました)。curl のほかのオプションは関連記事「curlコマンドの使い方とオプション一覧|エラー対処・Windows・wget比較」で扱っています。
コマンドで手早くデコード・エンコードしたいときの1行も載せます(bash と jq 1.7)。
$ printf '%b\n' "$(echo '%E3%81%82%20OK' | sed 's/%/\\x/g')"
あ OK
$ jq -rn '"C++ 入門/a~"|@uri'
C%2B%2B%20%E5%85%A5%E9%96%80%2Fa~
printf の方法は + を空白に戻さないので、フォームの形の文字列は先に + を空白に置き換えてから使います。
Shift_JIS・EUC-JPの%(文字コードで結果が変わる)
この節では、同じ「あ」でも文字コードによって %XX が変わる理由と、Shift_JIS の % を読む方法が分かります。
URL エンコードは「バイト」を %XX にする変換なので、文字をどの文字コードでバイトにするかで結果が変わります。「あ」は UTF-8 で %E3%81%82、Shift_JIS で %82%A0、EUC-JP で %A4%A2 です。
Chromium で Shift_JIS のページ(<meta charset="shift_jis">)のフォームから「文字化け 対策」を GET で送ると、URL は Shift_JIS のバイトでエンコードされました。
utf8 form GET -> http://127.0.0.1:31313/r?q=%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91+%E5%AF%BE%E7%AD%96
sjis form GET -> http://127.0.0.1:31313/r?q=%95%B6%8E%9A%89%BB%82%AF+%91%CE%8D%F4
フォームは、ページの文字コードで値をエンコードします。Shift_JIS で作られた以前からあるサイトや社内システムのログに、%82 や %95 で始まる見慣れない % が出るのはこのためです。これを UTF-8 として読むと文字化けします。
| 文字 | UTF-8 | Shift_JIS | メモ |
|---|---|---|---|
| あ | %E3%81%82 | %82%A0 | EUC-JP は %A4%A2 |
| ア | %E3%82%A2 | %83A | 2バイト目が英字の A なので %XX にならない |
| 表 | %E8%A1%A8 | %95%5C | 2バイト目が \(0x5C) |
| ア(半角) | %EF%BD%B1 | %B1 | Shift_JIS では1バイト |
| 😀 | %F0%9F%98%80 | 変換できない | UnicodeEncodeError |
Shift_JIS の「ア」が %83A になるのは、2バイト目(0x41)が英字の A に当たり、英字は変換しないためです。Python では quote('ア', encoding='shift_jis') が %83A を返しました。絵文字は Shift_JIS に無いため、次のエラーになりました。
UnicodeEncodeError 'shift_jis' codec can't encode character '\U0001f600' in position 0: illegal multibyte sequence
Shift_JIS の % を読むには、デコードのときに文字コードを指定します。記事内ツールで「Shift_JIS の%」の見本を押すと、文字コードが Shift_JIS に切り替わり「文字化け」と表示されます。同じ文字列を UTF-8 のまま読むと、「読めない文字(�)が 2 個あります」のように数が出ます(%82%A0 で確かめた表示)。
echo mb_convert_encoding(urldecode('%82%A0'), 'UTF-8', 'SJIS'), "\n"; // あ
echo urlencode(mb_convert_encoding('あ', 'SJIS', 'UTF-8')), "\n"; // %82%A0
文字化けそのものの原因(Excel・データベース・濁点分離など)は関連記事「文字化けの原因8選|濁点分離・utf8mb4・絵文字の文字数」で扱っています。
二重エンコード(%25)と壊れた%
この節では、%25 が混ざる二重エンコードがなぜ起きるのか、どこで直すのかが分かります。
%25 は「%」そのもののエンコードです。%25E3%2581%2582 のように %25 の後ろに16進数が続いていたら、すでに %E3%81%82 になっていた文字列をもう一度エンコードした「二重エンコード」です。
よくある原因は、値を自分でエンコードしてから、エンコードしてくれる仕組み(URLSearchParams・requests の params・http_build_query など)に渡すことです。Node.js 22 での実物です。
const v = encodeURIComponent('文字化け');
const u1 = new URL('https://example.com/search'); u1.searchParams.set('q', v);
console.log('NG:', u1.href);
const u2 = new URL('https://example.com/search'); u2.searchParams.set('q', '文字化け');
console.log('OK:', u2.href);
console.log('受け取った側:', new URL(u1.href).searchParams.get('q'));
NG: https://example.com/search?q=%25E6%2596%2587%25E5%25AD%2597%25E5%258C%2596%25E3%2581%2591
OK: https://example.com/search?q=%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91
受け取った側: %E6%96%87%E5%AD%97%E5%8C%96%E3%81%91
受け取った側は1回デコードしても「%E6%96%87…」のままで、画面に % の並びが出ます。
クエリの値を組み立てるとき
見分ける問い:「この値はもう % になっているか」
記事内ツールで %25E3%2581%2582 を貼ると、1回デコードした結果として %E3%81%82 が出ます。その結果をもう一度貼ると「あ」になり、何回エンコードされていたかが分かります。
壊れた %(後ろが16進数2桁でない)
「100%」「50% OFF」のように、% の後ろが16進数2桁でない文字列を正しく送るには、% を %25 にします。デコードする関数によって、壊れた % の扱いが分かれました。
| 関数 | 100% を読ませた結果 |
|---|---|
| JavaScript decodeURIComponent | URIError: URI malformed |
| Go url.QueryUnescape | invalid URL escape "%" |
| Python unquote | 100%(そのまま) |
| PHP urldecode | 100%(そのまま) |
| 記事内ツール | %XX を含む文字列の中なら、そのまま残し「16進数2桁でない所が 1 個」のように表示(100%25 と 50% OFF で確認) |
よくあるエラー/つまずき(原文つき)
この節では、URL エンコード・デコードで出るエラーと、文字化けなどのつまずきの原因と直し方が分かります。
| 症状・エラー原文 | 原因 | 直し方 |
|---|---|---|
| URIError: URI malformed(decodeURIComponent('%E3%81')) | 3バイトの文字の途中で切れている | URL を切り詰めていないか確かめる。try/catch で受ける |
| URIError: URI malformed(decodeURIComponent('%82%A0')) | Shift_JIS の % を UTF-8 として読んだ | 文字コードを指定して読む(TextDecoder('shift_jis') など) |
| URIError: URI malformed(encodeURIComponent('\uD800')) | 孤立したサロゲート(絵文字の片割れ) | 文字列を途中で切らない。toWellFormed() で置き換える |
| UnicodeDecodeError: 'utf-8' codec can't decode bytes… | Python unquote に errors='strict' を付け、壊れた UTF-8 を読んだ | 文字コードを確かめる。既定の replace なら � になる |
| invalid URL escape "%8"(Go) | % の後ろが1桁しかない | 送る側で % を %25 にする |
| curl: (3) URL rejected: Malformed input to a URL function | URL に空白などをそのまま書いた | 空白を %20 にするか --url-query を使う |
| C++ が「C 」になる | + をエンコードせずに送り、空白として読まれた | + は %2B にする。encodeURI ではなく encodeURIComponent |
| 画面に %E6%96%87… が出る | 二重エンコード | エンコードを1か所にする |
| %u3042 という形が届く | escape() を使った | encodeURIComponent に置き換える |
| デコード結果が �� になる | Shift_JIS・EUC-JP の % を UTF-8 で読んだ | 記事内ツールで文字コードを切り替える |
JavaScript の3つの URIError は、Node.js 22 で次の原文になりました。
decodeURIComponent('%E3%81') -> URIError: URI malformed
decodeURIComponent('100%') -> URIError: URI malformed
decodeURIComponent('%82%A0') -> URIError: URI malformed
encodeURIComponent('\uD800') -> URIError: URI malformed
Go の url.QueryUnescape は、壊れた % では次のエラーを返し、Shift_JIS のバイトはエラーにせずそのまま返しました。
QueryUnescape(%E3%81%8): invalid URL escape "%8"
QueryUnescape(100%): invalid URL escape "%"
QueryUnescape(%82%A0): "\x82\xa0"
Python・PHP・Go は、UTF-8 として読めないバイトがあってもエラーにしません。デコードした後で文字列が正しい UTF-8 か確かめたいときは、PHP なら mb_check_encoding()(urldecode('%E3%81') では false でした)、Python なら errors='strict' を使います。
どの関数を使うかの判断表
この節では、場面ごとにどの関数・どの形でエンコードすればよいかが分かります。
次の順に考えると、使う関数が決まります。
- 1URL を組み立てる仕組みがある → 生の文字列を渡すURLSearchParams・urlencode・http_build_query・url.Values。自分でエンコードしない
- 2クエリの値を自分で連結する → 値ごとに1回encodeURIComponent・quote(s, safe='')・rawurlencode
- 3パスの1区間(ファイル名・ID)→ %20 の形rawurlencode・url.PathEscape・encodeURIComponent
- 4できあがった日本語の URL を貼れる形に → URL 全体用encodeURI・new URL().href
- 5署名付きの URL・API → 仕様の決まりに合わせる~ や ! ' ( ) * の扱い、16進数の大文字まで仕様書どおりに
| 場面 | JavaScript | Python | PHP |
|---|---|---|---|
| クエリの値1つ | encodeURIComponent | quote(s, safe='') | rawurlencode |
| クエリ全体 | URLSearchParams | urlencode | http_build_query |
| パスの1区間 | encodeURIComponent | quote(s, safe='') | rawurlencode |
| URL 全体 | encodeURI・new URL | (区切ってから quote) | (区切ってから rawurlencode) |
| フォームの値を読む | URLSearchParams.get | parse_qs・unquote_plus | $_GET・urldecode |
| パスを読む | decodeURIComponent | unquote | rawurldecode |
| Shift_JIS で読む | TextDecoder('shift_jis') | unquote(s, encoding='shift_jis') | mb_convert_encoding |
送る前に確かめたいことを並べます。
- 1エンコードは1か所だけ自分でエンコードした値を、エンコードする仕組みに渡していないか
- 2+ を値に含むなら %2BC++・+81・Base64 の + は空白に化けやすい
- 3文字コードは UTF-8相手が Shift_JIS を求める以前からあるシステムなら、その文字コードでエンコードする
- 4デコードは区切ってから全体を先にデコードすると、値の中の %26 が区切りの & に化ける
- 5デコードの失敗を受けるdecodeURIComponent は try/catch で囲み、利用者の入力で画面が止まらないようにする
よくある質問(URLエンコード・デコード)
この節では、URL エンコード・デコードについてよく調べられる疑問への答えが分かります。
Q. %E3%81%82 は何と読みますか?
UTF-8 でエンコードした「あ」です。E3・81・82 の3バイトで1文字です。上のツールに貼ると「あ」と表示されます。
Q. URL に日本語をそのまま書いてもよいですか?
アドレスバーに日本語のまま入れても、Chromium が実際に送ったリクエスト行と location.href は %XX になっていました。HTML の href やプログラムから送る URL は、先にエンコードしておくと確実です。
Q. %20 と + はどちらを使えばよいですか?
パスは %20 です。クエリの値は、フォームの形で読む相手なら + でも空白になりますが、%20 はどちらの読み方でも空白になります。+ そのものを送るときは %2B にします。
Q. 大文字の %E3 と小文字の %e3 は違いますか?
意味は同じです(RFC 3986)。ただし文字列として比べる処理では別物として扱われるので、作る側は大文字にそろえます。curl のクエリは小文字で送られました。
Q. ~(チルダ)はエンコードすべきですか?
RFC 3986 では非予約文字なので不要です。PHP の urlencode と Ruby の encode_www_form_component は %7E にしましたが、受け取る側ではどちらも ~ に戻ります。
Q. デコードしたら文字化けしました。
Shift_JIS か EUC-JP でエンコードされた % の可能性があります。%82・%83・%88〜%9F で始まる % が多いなら Shift_JIS を、%A4・%A5 などが多いなら EUC-JP を試します。上のツールで文字コードを切り替えると確かめられます。
Q. URL エンコードで個人情報を隠せますか?
隠せません。決まった表での置き換えなので、誰でも元に戻せます。URL はアクセスログやブラウザの履歴にも残るため、秘密の値は URL に入れない設計にします。
Q. URL の長さに上限はありますか?
日本語は1文字で9文字分に増えるので、長くなりやすいです。上限はブラウザ・サーバー・中継する仕組みごとに違い、超えるとサーバーが 414 URI Too Long を返すことがあります。長いデータは POST の本文で送ります。
Q. ツールに貼った内容は送信されますか?
送信されません。変換はこの端末のブラウザの中だけで行います。
Q. 1回デコードしても % が残ります。
二重エンコードです。%25 が混ざっていたら、デコードした結果をもう一度デコードします。直すべきは送る側で、エンコードを1か所にします。
まとめ
- URL エンコードは、文字を文字コード(ふつうは UTF-8)のバイトにして、1バイトずつ「%+16進数2桁」で書く変換。「あ」は %E3%81%82。
- 値には encodeURIComponent・quote(s, safe='')・rawurlencode、クエリ全体には URLSearchParams・urlencode・http_build_query。encodeURI は + や & を残す。
- 空白はパスでは %20、フォームの形では +。+ を送るときは %2B。デコードの関数によって + の読み方が違う。
- Shift_JIS のページのフォームは Shift_JIS のバイトで % を作る。文字化けしたら文字コードを変えて読む。
- %25 が混ざったら二重エンコード。エンコードは1か所にし、デコードの失敗は try/catch で受ける。
参考・出典
確認日はいずれも 2026年11月7日です。
- IETF「RFC 3986 Uniform Resource Identifier (URI): Generic Syntax」2. Characters:https://www.rfc-editor.org/rfc/rfc3986#section-2
- WHATWG「URL Standard」percent-encoded bytes・application/x-www-form-urlencoded:https://url.spec.whatwg.org/
- MDN「encodeURIComponent()」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
- MDN「encodeURI()」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
- MDN「URLSearchParams」:https://developer.mozilla.org/ja/docs/Web/API/URLSearchParams
- Python ドキュメント「urllib.parse --- URL を解析して構成要素にする」:https://docs.python.org/ja/3/library/urllib.parse.html
- PHP マニュアル「rawurlencode」:https://www.php.net/manual/ja/function.rawurlencode.php
- PHP マニュアル「urlencode」:https://www.php.net/manual/ja/function.urlencode.php
- Go「net/url」:https://pkg.go.dev/net/url
- curl「curl man page」--data-urlencode・--url-query:https://curl.se/docs/manpage.html
- Microsoft サポート「ENCODEURL 関数」:https://support.microsoft.com/ja-jp/excel/functions/encodeurl-function
- Microsoft Learn「Uri.EscapeDataString メソッド」:https://learn.microsoft.com/ja-jp/dotnet/api/system.uri.escapedatastring
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「URLエンコード・デコード変換ツール|%E3%81%82の読み方と言語別の違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。