結論

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 の % で文字化けする原因と直し方

2026年11月7日更新|Node.js 22・Python 3.13・PHP 8.3・Go 1.24・curl 8.5・Chromium 153で実行確認

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文字です。手で読む手順は次のとおりです。

1

% で区切る

%E3 %81 %82 の3つに分ける。% の後ろの2文字が16進数(0〜9・A〜F)で1バイト。

大文字・小文字は同じ意味(%e3 も %E3)

2

先頭のバイトで文字の長さを知る

E3 は2進数で 1110 0011。先頭が 1110 なら「3バイトで1文字」、110 なら2バイト、11110 なら4バイト(絵文字など)、0 なら1バイト(英数字)。

UTF-8 の決まり

3

印の部分を除いてつなぐ

11100011・10000001・10000010 から、先頭の 1110 と、続きのバイトの 10 を除いた 0011・000001・000010 をつなぐと 0011000001000010。

16ビット

4

Unicode の番号として読む

0011000001000010 は16進数で 3042。U+3042 は「あ」。

ひらがなは U+3041〜U+3096

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='') で数えた結果です。

1字英数字「A」そのまま A
9字かな・漢字「あ」%E3%81%82(3バイト)
12字絵文字「😀」%F0%9F%98%80(4バイト)

日本語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%A0EUC-JP は %A4%A2
ア%E3%82%A2%83A2バイト目が英字の A なので %XX にならない
表%E8%A1%A8%95%5C2バイト目が \(0x5C)
ア(半角)%EF%BD%B1%B1Shift_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…」のままで、画面に % の並びが出ます。

クエリの値を組み立てるとき

やってはいけないencodeURIComponent した値を、さらに searchParams.set や params= に渡す。受け取り側で2回デコードしてごまかす
→
代わりにエンコードは1か所だけ。仕組みに渡すなら生の文字列を渡し、自分で連結するなら encodeURIComponent を1回だけ使う

見分ける問い:「この値はもう % になっているか」

記事内ツールで %25E3%2581%2582 を貼ると、1回デコードした結果として %E3%81%82 が出ます。その結果をもう一度貼ると「あ」になり、何回エンコードされていたかが分かります。

壊れた %(後ろが16進数2桁でない)

「100%」「50% OFF」のように、% の後ろが16進数2桁でない文字列を正しく送るには、% を %25 にします。デコードする関数によって、壊れた % の扱いが分かれました。

関数 100% を読ませた結果
JavaScript decodeURIComponentURIError: URI malformed
Go url.QueryUnescapeinvalid URL escape "%"
Python unquote100%(そのまま)
PHP urldecode100%(そのまま)
記事内ツール%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 functionURL に空白などをそのまま書いた空白を %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' を使います。

どの関数を使うかの判断表

この節では、場面ごとにどの関数・どの形でエンコードすればよいかが分かります。

次の順に考えると、使う関数が決まります。

  1. 1URL を組み立てる仕組みがある → 生の文字列を渡すURLSearchParams・urlencode・http_build_query・url.Values。自分でエンコードしない
  2. 2クエリの値を自分で連結する → 値ごとに1回encodeURIComponent・quote(s, safe='')・rawurlencode
  3. 3パスの1区間(ファイル名・ID)→ %20 の形rawurlencode・url.PathEscape・encodeURIComponent
  4. 4できあがった日本語の URL を貼れる形に → URL 全体用encodeURI・new URL().href
  5. 5署名付きの URL・API → 仕様の決まりに合わせる~ や ! ' ( ) * の扱い、16進数の大文字まで仕様書どおりに
場面 JavaScript Python PHP
クエリの値1つencodeURIComponentquote(s, safe='')rawurlencode
クエリ全体URLSearchParamsurlencodehttp_build_query
パスの1区間encodeURIComponentquote(s, safe='')rawurlencode
URL 全体encodeURI・new URL(区切ってから quote)(区切ってから rawurlencode)
フォームの値を読むURLSearchParams.getparse_qs・unquote_plus$_GET・urldecode
パスを読むdecodeURIComponentunquoterawurldecode
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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり