3行まとめ
- URLをパーセントエンコード/デコードするブラウザ完結ツールを作った。中身は標準の
encodeURIComponent/encodeURIそのままだが、この2つの使い分けが本題 -
encodeURIComponentはクエリの「値」用、encodeURIはURL「全体」用。前者は/ ? & =まで変換し、後者は残す。ただしencodeURIComponentでも! ~ * ' ( )は変換されない - 空白は
%20になり+にはならない。フォーム(application/x-www-form-urlencoded)で+が要るなら自前で置換する。逆に値の中の+は%2Bになる
APIのクエリに日本語や特殊文字を載せる、アクセスログのパーセントエンコードを読み解く——URLエンコードは頻出だが、encodeURIComponent と encodeURI のどちらを使うか、空白が + にならない、といった所で毎回迷う。
ぱんだツールズのURLエンコード/デコードツールは、テキストをパーセントエンコード・デコードするツール。方式(encodeURIComponent / encodeURI)を選べる。処理はブラウザ内で完結し、入力はサーバーに送信されない。
実装は標準APIを呼ぶだけなので、この記事は「その標準APIをどう使い分けるか」に振り切って、encodeURIComponent と encodeURI の違い、変換されずに残る記号、空白と + の罠、UTF-8パーセントエンコードの仕組み、デコードの例外を、実装ベースで解説する。
実装は標準API — だから使い分けが全て
エンコード・デコードは方式に応じて標準関数を呼ぶだけ。
const result = mode === 'component' ? encodeURIComponent(input) : encodeURI(input)
// デコード
const result = mode === 'component' ? decodeURIComponent(input) : decodeURI(input)
車輪の再発明はしない。パーセントエンコーディングは仕様(RFC 3986)が確立していて、ブラウザ標準の実装が正しく速い。自前で書くとUTF-8のバイト分解やサロゲートペアで事故る。問題は「どっちの関数を選ぶか」で、ここを外すとURLが壊れる。
encodeURIComponent と encodeURI の境界
2つの関数の違いは「何を変換せずに残すか」に尽きる。どちらも英数字と - _ . ! ~ * ' ( ) は変換しない。差はそれ以外の記号の扱い。
| 記号 | encodeURIComponent |
encodeURI |
|---|---|---|
; , / ? : @ & = + $ # |
変換する(%2F など) |
変換しない(残す) |
| 日本語・空白など | 変換する | 変換する |
-
encodeURIComponent:/ ? & = #といったURLの区切り記号まで容赦なく変換する。だから「URLの部品(クエリの値ひとつ)」を安全に包める。値に&や=が入っていても区切りと誤認されない -
encodeURI:URLの区切り記号は構造として残す。だから「URL全体」を渡して、日本語やスペースだけをエンコードしたいときに使う
使い分けの原則はシンプル。クエリパラメータの「値」を作るなら encodeURIComponent、既にできあがったURL全体をまとめてエンコードするなら encodeURI。迷ったら encodeURIComponent——値を1つずつ包んで組み立てる方が事故りにくい。
// 値を個別に包んで組み立てる(推奨)
const url = `https://api.example.com/search?q=${encodeURIComponent(keyword)}&tag=${encodeURIComponent(tag)}`
逆に、encodeURI に「値だけ」を渡すと、値の中の & や = が残ってしまい、クエリの区切りと衝突して壊れる。encodeURI は「もう構造が確定したURL」専用と覚えておくといい。
encodeURIComponent でも残る記号 — ! ~ * ' ( )
ここが盲点。encodeURIComponent は「ほぼ全部エンコードする」イメージだが、! ~ * ' ( ) の6文字は変換されない。RFC 3986 が予約する記号の一部で、これらはそのまま通る。
このうち ~(チルダ)は RFC 3986 の「非予約文字(unreserved)」なので、エンコードせず残しても仕様上問題ない。厄介なのは残り5つの ! ' ( ) *(サブデリミタ)で、厳密なエンコードを要求する相手——OAuth 1.0 の署名や、( ) ' を特別扱いするサーバー——では、これらが素通りすることで署名不一致や解釈のズレが起きる。その場合は encodeURIComponent の後段で、この5文字だけ手当てする。
function strictEncode(s: string): string {
// ~ は unreserved なので触らず、! ' ( ) * の5文字だけ追加エンコード
return encodeURIComponent(s).replace(
/[!'()*]/g,
(c) => '%' + c.charCodeAt(0).toString(16).toUpperCase(),
)
}
encodeURIComponent を「完全なエンコード」と思い込まず、残る記号があると知っておくのが肝心。このツールはブラウザ標準の挙動をそのまま見せる方針なので、これらの記号は残った状態で出力される。
空白は %20、+ にはならない
一番よく踏む罠。encodeURIComponent(' ') の結果は %20 であって + ではない。
encodeURIComponent('a b') // "a%20b"(+ ではない)
encodeURIComponent('a+b') // "a%2Bb"(+ は %2B にエンコードされる)
一方、HTMLフォームの送信形式 application/x-www-form-urlencoded では、歴史的経緯から空白を + で表す。つまりフォーム相当のエンコードが欲しいなら、%20 を + に置換する一手間が要る。
// フォーム(application/x-www-form-urlencoded)相当にする
const formEncoded = encodeURIComponent(value).replace(/%20/g, '+')
逆方向の非対称も要注意で、値の中のリテラルの + は encodeURIComponent で %2B になる。「+=空白」と決めつけている受け側に生の + を送ると空白と解釈されるので、%2B へ確実に包めるのは実はありがたい。「+ は空白なのか、プラス記号なのか」は文脈(クエリ文字列かフォーム本体か)次第で割れる古典的な曖昧さで、%20 と %2B を意識的に使い分けるのが安全策になる。
日本語はUTF-8のバイトごとにパーセント化
日本語などのマルチバイト文字は、UTF-8にエンコードしたバイト列を1バイトずつ %XX にする。
encodeURIComponent('東') // "%E6%9D%B1"
「東」はUTF-8で E6 9D B1 の3バイトなので、%E6%9D%B1 になる。encodeURIComponent / encodeURI はどちらもUTF-8基準なので、現代のUTF-8なサーバーとはそのまま噛み合う。1文字が複数の %XX になるのはこのため。絵文字のようなサロゲートペアの文字も、標準関数がUTF-8の4バイトへ正しく分解してくれるので、自前でコードポイントを触る必要はない。
デコードは壊れた入力で例外を投げる
デコード側 decodeURIComponent / decodeURI は、不正なパーセント列に対して URIError を投げる。% の後に16進2桁が続かない、UTF-8として不完全なバイト列、といった入力で例外になる。
decodeURIComponent('%E6%9D') // URIError: URI malformed(3バイト目が欠けている)
decodeURIComponent('%ZZ') // URIError: URI malformed
なので、外部から来た文字列をデコードするときは try/catch が要る。このツールも try/catch で囲み、失敗時は「不正なエンコード文字列の可能性があります」と伝えるようにしている。エンコードは(サロゲート単独でない限り)まず失敗しないが、デコードはユーザーが貼り付けた壊れた文字列で普通に例外になるので、そこだけ守りを入れる。
まとめ
- URLエンコードは標準の
encodeURIComponent/encodeURIに任せる。自前実装はUTF-8分解で事故るだけ。本質は使い分け - クエリの「値」は
encodeURIComponent(/ ? & =まで変換)、URL「全体」はencodeURI(区切りは残す)。迷ったら値を個別に包む前者 -
encodeURIComponentでも! ~ * ' ( )は残る。OAuth など厳密な相手には後段で追加エンコードする - 空白は
%20で+にならない。フォーム相当が要るなら%20→+に置換。値の中の+は%2Bになる - 日本語はUTF-8バイトごとに
%XX。デコードは壊れた入力でURIErrorを投げるので try/catch が要る
APIのクエリ組み立てやログの解読にどうぞ。入力はブラウザの外に出ない。
ぱんだツールズ では他にも Base64変換・JSON整形・文字コード変換・ハッシュ生成など、Web開発の細かい変換作業に効くブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。