画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本(空白と記号)
$ python3 url_encode.py "Hello, World!"
Hello%2C%20World%21そのまま残す4つの記号
$ python3 url_encode.py "a-b_c.d~e"
a-b_c.d~e言語によって残りやすい記号
$ python3 url_encode.py "!*'()"
%21%2A%27%28%29URL をまるごと渡す(: も / も変える)
$ python3 url_encode.py "https://example.com/?q=1&r=2"
https%3A%2F%2Fexample.com%2F%3Fq%3D1%26r%3D2日本語
$ python3 url_encode.py "天気 東京"
%E5%A4%A9%E6%B0%97%20%E6%9D%B1%E4%BA%AC絵文字(4バイトの文字)
$ python3 url_encode.py 🍣
%F0%9F%8D%A3全角の英数字
$ python3 url_encode.py AB1
%EF%BC%A1%EF%BC%A2%EF%BC%91「+」と「=」
$ python3 url_encode.py 1+1=2
1%2B1%3D2すでにパーセントで書いてある
$ python3 url_encode.py a%20b
a%2520b改行(\r\n)が入っている
$ python3 url_encode.py "1行目
2行目"
1%E8%A1%8C%E7%9B%AE%0D%0A2%E8%A1%8C%E7%9B%AEタブ
$ python3 url_encode.py "a b"
a%09b濁点を分けて書いた「が」
$ python3 url_encode.py が
%E3%81%8B%E3%82%99空白だけ
$ python3 url_encode.py " "
%20%20%20空の文字列
$ python3 url_encode.py ""
長い値(日本語30文字)
$ python3 url_encode.py ああああああああああああああああああああああああああああああ
%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%8210言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
url_encode.php8.4.26 で 15 / 15 件の出力が正解と一致
<?php // 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える // rawurlencode は RFC 3986 のとおり(残すのは英数字と - _ . ~ だけ・空白は %20・16進数は大文字) // urlencode は空白を「+」にして「~」も %7E にする(名前が似ているので取り違えやすい) echo rawurlencode($argv[1]) . "\n";
動かし方(ファイルを保存したフォルダで)
$ php url_encode.php "Hello, World!"
url_encode.py3.14.7 で 15 / 15 件の出力が正解と一致
# 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える import sys from urllib.parse import quote # safe="" を付けないと「/」が残る(既定が safe="/" なのでパスに使う想定)。quote_plus は空白を「+」にする print(quote(sys.argv[1], safe=""))
動かし方(ファイルを保存したフォルダで)
$ python3 url_encode.py "Hello, World!"
url_encode.go1.27.2 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
package main
import (
"fmt"
"net/url"
"os"
"strings"
)
func main() {
// QueryEscape が残す文字は RFC 3986 と同じだが、空白だけ「+」にする
// 入力に入っていた「+」は %2B になっているので、あとから「+」を %20 に置き換えてよい
s := strings.ReplaceAll(url.QueryEscape(os.Args[1]), "+", "%20")
fmt.Println(s)
}
動かし方(ファイルを保存したフォルダで)
$ go run url_encode.go "Hello, World!"
url_encode.js24.15.0 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える // encodeURIComponent は ! * ' ( ) の5つを残す(RFC 3986 では変える文字)。その5つだけ自分で変える const encoded = encodeURIComponent(process.argv[2]).replace(/[!*'()]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(), ); console.log(encoded);
動かし方(ファイルを保存したフォルダで)
$ node url_encode.js "Hello, World!"
UrlEncode.java21.0.12 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
import java.nio.charset.StandardCharsets;
public class UrlEncode {
static final String KEEP = "-_.~";
public static void main(String[] args) {
// URLEncoder.encode は空白を「+」、「~」を %7E にして「*」を残す(フォームの形で、URL の形ではない)
StringBuilder sb = new StringBuilder();
for (byte b : args[0].getBytes(StandardCharsets.UTF_8)) {
int v = b & 0xFF; // byte は符号付きなので、そのまま使うと負になる
char c = (char) v;
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || KEEP.indexOf(c) >= 0) {
sb.append(c);
} else {
sb.append(String.format("%%%02X", v));
}
}
System.out.println(sb);
}
}
動かし方(ファイルを保存したフォルダで)
$ java UrlEncode.java "Hello, World!"
url_encode.cs10.0.402 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える // Uri.EscapeDataString は .NET 6 から RFC 3986 のとおり(それより前は ! * ' ( ) を残していた) // WebUtility.UrlEncode は空白を「+」にして、! * ( ) をそのまま残す(フォームの形) Console.WriteLine(Uri.EscapeDataString(args[0]));
動かし方(ファイルを保存したフォルダで)
$ dotnet run url_encode.cs -- "Hello, World!"
url_encode.rs1.99.0 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
fn main() {
let args: Vec<String> = std::env::args().collect();
// 標準に URL の関数が無いので自分で書く。String の as_bytes はそのまま UTF-8 のバイトの並び
let mut out = String::new();
for &b in args[1].as_bytes() {
let c = b as char;
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
out.push(c);
} else {
out.push_str(&format!("%{:02X}", b));
}
}
println!("{}", out);
}
動かし方(ファイルを保存したフォルダで)
$ rustc url_encode.rs && ./url_encode "Hello, World!"
url_encode.rb4.0.7 で 15 / 15 件の出力が正解と一致
# 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
# CGI.escape は空白を「+」にする。URI.encode_www_form_component はさらに「~」を %7E にして「*」を残す
# .b でバイトの並びにしてから変える(文字のまま gsub すると日本語1文字に %XX が1つになる)
puts ARGV[0].b.gsub(/[^A-Za-z0-9\-_.~]/) { |c| "%%%02X" % c.ord }
動かし方(ファイルを保存したフォルダで)
$ ruby url_encode.rb "Hello, World!"
url_encode.kt2.4.21 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
fun main(args: Array<String>) {
// java.net.URLEncoder は空白を「+」、「~」を %7E にして「*」を残す(フォームの形)
val sb = StringBuilder()
for (b in args[0].toByteArray(Charsets.UTF_8)) {
val v = b.toInt() and 0xFF // Byte は符号付きなので、そのまま使うと負になる
val c = v.toChar()
if (c in 'A'..'Z' || c in 'a'..'z' || c in '0'..'9' || c in "-_.~") {
sb.append(c)
} else {
sb.append("%%%02X".format(v))
}
}
println(sb)
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc url_encode.kt -include-runtime -d url_encode.jar && java -jar url_encode.jar "Hello, World!"
url_encode.swift6.3.3 で 15 / 15 件の出力が正解と一致
// 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える
// addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) は & や = を残す(Foundation も要る)
// utf8 はそのまま UTF-8 のバイトの並び
let keep = Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".utf8)
let hex = Array("0123456789ABCDEF")
var out = ""
for b in CommandLine.arguments[1].utf8 {
if keep.contains(b) {
out.append(Character(UnicodeScalar(b)))
} else {
out.append("%")
out.append(hex[Int(b >> 4)])
out.append(hex[Int(b & 0xF)])
}
}
print(out)
動かし方(ファイルを保存したフォルダで)
$ swift url_encode.swift "Hello, World!"
よく見かける書き方で試すと
仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | よく見かける書き方の答え(10言語) |
|---|---|
基本(空白と記号)"Hello, World!" |
|
そのまま残す4つの記号"a-b_c.d~e" |
|
言語によって残りやすい記号"!*'()" |
|
URL をまるごと渡す(: も / も変える)"https://example.com/?q=1&r=2" |
|
日本語"天気 東京" |
|
絵文字(4バイトの文字)🍣 |
|
全角の英数字AB1 |
|
「+」と「=」1+1=2 |
|
すでにパーセントで書いてあるa%20b | ✓10言語すべて正解 |
改行(\r\n)が入っている"1行目
2行目" |
|
タブ"a b" | ✓10言語すべて正解 |
濁点を分けて書いた「が」が |
|
空白だけ" " |
|
空の文字列"" | ✓10言語すべて正解 |
長い値(日本語30文字)ああああああああああああああああああああああああああああああ |
|
この書き方の決まり
- 引数は文字列1つ。パーセントエンコードして1行で出す
- そのまま残すのは A〜Z a〜z 0〜9 と 「-」「_」「.」「~」の4つだけ(RFC 3986 の「予約されていない文字」)
- それ以外は、その文字を UTF-8 のバイトに分けて、1バイトずつ
%XXにする(XX は大文字の16進数2桁) - 空白は
%20にする(+にしない)。改行・タブも同じようにバイトにする - 文字の単位ではなくバイトの単位で変える(絵文字は4バイトなので
%XXが4つ並ぶ) - 空の文字列は空のまま
解説
標準の関数がそのまま使えるのは10言語中3言語
「URL エンコード」という名前の関数は、たいていどの言語にもあります。ただしフォームの形(application/x-www-form-urlencoded)と URL の形(RFC 3986)の2通りがあり、名前からはどちらか分かりません。この回の決まり(RFC 3986)に、標準の関数がそのまま合ったのは10言語中3言語でした。
| 言語 | 書き方 | |
|---|---|---|
| そのまま使える | PHP・Python・C# | rawurlencode($s)・quote(s, safe="")・Uri.EscapeDataString(s) |
| 1行足せば済む | JavaScript・Go | encodeURIComponent のあと ! * ' ( ) を変える・QueryEscape のあと + を %20 にする |
| 自分でバイトを回す | Java・Kotlin・Ruby・Rust・Swift | UTF-8 のバイトを1つずつ見て %XX にする |
PHP は urlencode と rawurlencode の2つがあり、後ろに raw が付くほうが URL の形です。Python の quote は safe の既定が "/" なので、クエリの値に使うときは safe="" を書き足します。C# の Uri.EscapeDataString は .NET 6 から RFC 3986 のとおりになりました(それより前は ! * ' ( ) を残していました)。
空白が「+」になる(10言語中6言語)
空白3つだけを渡したケース(c013)で、よく見かける書き方の 10言語中6言語が +++ を返しました。
| よく見かける書き方 | Hello, World! の答え |
|---|---|
| 決まり | Hello%2C%20World%21 |
PHP urlencode | Hello%2C+World%21 |
Go url.QueryEscape | Hello%2C+World%21 |
Java・Kotlin URLEncoder.encode | Hello%2C+World%21 |
Ruby CGI.escape | Hello%2C+World%21 |
C# WebUtility.UrlEncode | Hello%2C+World! |
JavaScript encodeURIComponent | Hello%2C%20World! |
Swift .urlQueryAllowed | Hello,%20World! |
+ を空白と読むのは、HTML のフォームを送るときの形です。クエリの値としては + でも読めることが多いのですが、同じ文字列をパスの一部やヘッダーの値に入れると、+ のまま残って別の文字列になります。日本語(c005)も、空白のところだけ + になります。
逆に、入力に入っていた + は %2B になります(c008)。Go の書き方で QueryEscape のあとに + を %20 に置き換えてよいのは、このためです。残っている + は空白から来たものだけです。
「~」を変えてしまう(10言語中4言語)
a-b_c.d~e(c002)は、決まりではまったく変わりません。よく見かける書き方では 10言語中4言語(PHP・Java・Kotlin・C#)が a-b_c.d%7Ee を返しました。~ が「予約されていない文字」に入ったのは RFC 3986(2005年)からで、それより前の決まりで作られた関数は ~ を変えます。
Ruby の CGI.escape は ~ を残しますが、URI.encode_www_form_component は %7E にします(Ruby 4.0.7 で確かめました)。同じ言語の中でも関数によって違います。
! * ' ( ) が残る
!*'()(c003)の5文字で、よく見かける書き方の答えが4通りに割れました。
| よく見かける書き方 | !*'() の答え |
|---|---|
| 決まり | %21%2A%27%28%29 |
JavaScript encodeURIComponent | !*'() |
Swift .urlQueryAllowed | !*'() |
C# WebUtility.UrlEncode | !*%27() |
Java・Kotlin URLEncoder.encode | %21*%27%28%29 |
この5文字は RFC 3986 では「副デリミタ」で、変えなくても URL としては正しく読めます。ただし、' が残ったまま HTML の属性に入れると引用符を閉じてしまうので、02 HTML の特殊文字をエスケープする と組み合わせるか、この回のように全部変えてしまうほうが安全です。JavaScript で encodeURIComponent のあとに5文字だけ変える書き方は、この差を埋めるためによく貼られています。
文字ではなくバイトに分ける
Rust には標準に URL の関数が無いので、自分で書くことになります。よく見かける「1文字ずつ見て、英数字でなければ %XX にする」書き方は、15件中7件しか通りませんでした(10言語で唯一、半分を割りました)。
| 入力 | よく見かける書き方(Rust)の答え | 決まり |
|---|---|---|
天気 東京(c005) | 天気%20東京 | %E5%A4%A9%E6%B0%97%20%E6%9D%B1%E4%BA%AC |
🍣(c006) | %1F363 | %F0%9F%8D%A3 |
AB1(c007) | AB1 | %EF%BC%A1%EF%BC%A2%EF%BC%91 |
a-b_c.d~e(c002) | a%2Db%5Fc%2Ed%7Ee | a-b_c.d~e |
is_alphanumeric() は Unicode 全体を見るので、ひらがな・漢字・全角の英数字にも true を返します(06 URL 用の文字列(slug)を作る と同じ問題です)。is_ascii_alphanumeric() に替えると、今度は日本語が %XX になりますが、文字の値をそのまま16進数にしてしまうと %1F363 のように桁が増えます。パーセントエンコードは「1バイトを %XX 1つ」にする決まりなので、先に UTF-8 のバイトに分けます。
Rust の as_bytes()、Swift の utf8、Java の getBytes(StandardCharsets.UTF_8)、Ruby の .b が、その「バイトの並び」です。Java と Kotlin の byte は符号付きなので、& 0xFF を付けないと %FFFFFFE3 のようになります。
バイトに分けてあれば、濁点を分けて書いた「が」(c012。か+濁点の2文字)も %E3%81%8B%E3%82%99 と素直に出ます。文字数を数える必要がないので、絵文字の数え方の違いも関係しません。
URL をまるごと渡すと
https://example.com/?q=1&r=2(c004)を渡したとき、決まりでは : も / も ? も & も変えます(クエリの値に入れる想定なので、区切り記号も値の一部として変える)。よく見かける書き方では2言語が違いました。
- Python の
quote(s)はsafeの既定が"/"なので、https%3A//example.com/%3Fq%3D1%26r%3D2と/だけ残します。パスを作るときの既定です - Swift の
addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)は、クエリ全体(q=1&r=2のような文字列)に使う集合なので& = ? / :を残し、入力をほぼそのまま返しました
どちらも関数が悪いのではなく、「URL のどの部分を作るのか」で許す文字が変わるということです。値1つを作るときは、残す文字をいちばん少なくしておくと間違えません。
すでに %20 と書いてある文字列
a%20b(c009)は、決まりでは a%2520b になります。よく見かける書き方も 10言語すべてが同じ答えでした。% 自体を %25 にするのは正しい動きです。
困るのは、エンコード済みの文字列をもう一度エンコードしてしまう場合です。どの言語も「もうエンコードしてあるか」は判断できないので、変える前の文字列を持っておいて、組み立てるときに1回だけ変えるようにします(02 HTML の特殊文字をエスケープする と同じ考え方です)。
改行とタブ
改行(c010)は %0D%0A、タブ(c011)は %09 になります。よく見かける書き方でも、タブは10言語すべてが、改行は Rust 以外の9言語が同じ答えでした。ヘッダーやログに値を入れるときは、改行が %0D%0A になっていることが大事です(生の改行が混ざると、行が増えて別の意味になります)。