URL エンコード対象文字数
検索語に空白や日本語を入れて URL を作ると、アドレスバーに貼った瞬間に %E3%81%AD のような文字列に化けます。壊れたのではありません。そのままでは置けない文字が、置き換えられただけです。
URL にそのまま置ける文字は、意外と少ない
URL の中では、記号の多くが区切りとしての役割を持っています。? はクエリの始まり、& は次のパラメータ、/ は階層の区切り、# はページ内の位置です。値の中にこれらが混ざると、区切りなのか中身なのかを見分けられません。
そこで、区切りに使う記号や、そもそも URL に書けない文字は、% と 16 進数 2 桁の組に置き換えます。これがパーセントエンコーディングです。そのまま置いてよいのは、RFC 3986 で unreserved と呼ばれる次の範囲だけです。
- 英字
A-Za-z - 数字
0-9 - 記号 4 つ
-_.~
これ以外はすべて置き換えの対象になります。空白も、+ も = も、日本語も入ります。. は記号なので対象に見えますが、この 4 つに入っているので置き換えません。逆に / は見慣れているぶん安全に見えますが、値の中では必ず置き換えが要ります。
プレーンテキスト
検索 A -> %E6%A4%9C%E7%B4%A2%20A
path/to?x=1 -> path%2Fto%3Fx%3D1日本語 1 文字が %XX 3 組に化ける
日本語は UTF-8 でバイトに直してから置き換えられるので、1 文字が %XX 3 組、9 文字ぶんに膨らみます。ここで数え方を決めておきます。今回数えるのは、%XX が何組できるかではありません。入力の中の何文字が置き換えの対象かです。日本語 1 文字は、バイトが 3 つでも 1 と数えます。
数えられると何が嬉しいのかというと、置き換えたあとの長さが先に分かることです。1 文字が 3 文字や 9 文字に膨らむので、対象の数が分かればおよその長さが読めます。URL の長さに上限のある経路へ渡す前や、書き込む先の大きさを決めるときに効いてきます。
1 文字ずつ見て、条件に合うものを数える
やることは走査と数え上げです。集合を先に作っておいて、含まれるかどうかで数を増やします。
Python
vowels = set("aeiou")
count = 0
for ch in "encoding":
if ch in vowels:
count += 1
print(count) # 3集合への in は、中身が増えても速さが変わりません。JavaScript なら、正規表現で範囲を書くほうが読みやすいこともあります。
JavaScript
console.log(/[a-z]/.test("A")); // false厳密には、URL のどこに置く値かによって置き換えの要否は少し変わります。パスの途中なのか、クエリの値なのかで、そのまま置ける記号が違うためです。今回はそこを単純にして、unreserved 以外はすべて対象、と決めて数えます。
文字列を回すときは for...of を使います。添字で回すと、絵文字のように内部で 2 つ分の場所を使う文字が 2 文字として数えられてしまいます。for...of は文字の単位で進むので、この食い違いが起きません。
要件
- A-Z a-z 0-9 - _ . ~ を安全文字 (unreserved) とみなす
- それ以外の文字をエンコード対象として数え、整数を返す
- 日本語などの非 ASCII 文字は 1 文字として数える
入出力例
urlEncodeCount("") → 0
urlEncodeCount("hello") → 0
urlEncodeCount("hello world") → 1
urlEncodeCount("a+b=c") → 2
urlEncodeCount("こんにちは") → 5
urlEncodeCount("a-b_c.d~e") → 0
urlEncodeCount("1+1=2") → 2
urlEncodeCount("page?q=hi#top") → 3