画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
タグ
$ python3 escape.py "<b>太字</b>"
<b>太字</b>5つすべて
$ python3 escape.py "& < > \" '"
& < > " '属性に入れる値(' と ")
$ python3 escape.py "It's \"OK\""
It's "OK"すでにエスケープしてある
$ python3 escape.py "& <"
&amp; &lt;スクリプト
$ python3 escape.py "<script>alert('x')</script>"
<script>alert('x')</script>変える文字が無い
$ python3 escape.py "こんにちは 🍣"
こんにちは 🍣空の文字列
$ python3 escape.py ""
改行はそのまま
$ python3 escape.py "1行目
<2行目>"
1行目
<2行目>10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
escape.php8.4.26 で 8 / 8 件の出力が正解と一致
<?php
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
$s = $argv[1];
// htmlspecialchars は ' を ' と書く(この回の ' と違う)ので、自分で置き換える。
// strtr に配列を渡すと、元の文字列を1回だけ見て置き換えるので、作った < の & がもう一度変わることがない
echo strtr($s, [
'&' => '&',
'<' => '<',
'>' => '>',
'"' => '"',
"'" => ''',
]) . "\n";
動かし方(ファイルを保存したフォルダで)
$ php escape.php "<b>太字</b>"
escape.py3.14.7 で 8 / 8 件の出力が正解と一致
# 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
import sys
s = sys.argv[1]
# html.escape は ' を ' と書く(この回の ' と違う)ので、自分で置き換える。
# translate は1文字ずつ1回だけ置き換えるので、& を先に変える順番を気にしなくてよい
TABLE = str.maketrans({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})
print(s.translate(TABLE))
動かし方(ファイルを保存したフォルダで)
$ python3 escape.py "<b>太字</b>"
escape.go1.27.2 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
package main
import (
"fmt"
"os"
"strings"
)
// html.EscapeString は " を " と書く(この回の " と違う)ので、自分で置き換える。
// strings.NewReplacer は元の文字列を1回だけ見て置き換えるので、作った < の & がもう一度変わることがない
var escaper = strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
`"`, """,
"'", "'",
)
func main() {
fmt.Println(escaper.Replace(os.Args[1]))
}
動かし方(ファイルを保存したフォルダで)
$ go run escape.go "<b>太字</b>"
escape.js24.15.0 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
const s = process.argv[2];
// JavaScript の標準にはエスケープの関数がないので、自分で置き換える。
// 文字列を渡す replace は最初の1つしか変えないので、g を付けた正規表現で5文字をまとめて1回で置き換える
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
console.log(s.replace(/[&<>"']/g, (c) => map[c]));
動かし方(ファイルを保存したフォルダで)
$ node escape.js "<b>太字</b>"
Escape.java21.0.12 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
public class Escape {
public static void main(String[] args) {
String s = args[0];
// Java の標準にはエスケープの関数がないので、1文字ずつ見て置き換える。
// 5文字はどれも1つの char に収まるので、絵文字(char 2つ)はそのまま書き写される
StringBuilder out = new StringBuilder();
for (char c : s.toCharArray()) {
switch (c) {
case '&' -> out.append("&");
case '<' -> out.append("<");
case '>' -> out.append(">");
case '"' -> out.append(""");
case '\'' -> out.append("'");
default -> out.append(c);
}
}
System.out.println(out);
}
}
動かし方(ファイルを保存したフォルダで)
$ java Escape.java "<b>太字</b>"
escape.cs10.0.402 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
string s = args[0];
// WebUtility.HtmlEncode は絵文字や é なども 🍣 のような数字の形に変えてしまう(この回は5文字だけ変える)ので、自分で置き換える。
// Replace はすべて置き換える。& を最初に変えないと、あとで作った < の & まで & になる
string escaped = s
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
Console.WriteLine(escaped);
動かし方(ファイルを保存したフォルダで)
$ dotnet run escape.cs -- "<b>太字</b>"
escape.rs1.99.0 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
// Rust の標準ライブラリにはエスケープの関数がないので、1文字ずつ見て置き換える
fn escape(s: &str) -> String {
// 変えると長くなるので、少し多めに場所を取っておく
let mut out = String::with_capacity(s.len() + s.len() / 8);
// chars は1文字(Unicode のコードポイント)ずつ取り出すので、日本語や絵文字が途中で切れない
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
fn main() {
let args: Vec<String> = std::env::args().collect();
println!("{}", escape(&args[1]));
}
動かし方(ファイルを保存したフォルダで)
$ rustc escape.rs && ./escape "<b>太字</b>"
escape.rb4.0.7 で 8 / 8 件の出力が正解と一致
# 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える # CGI.escapeHTML は5文字を & < > " ' に変える(この回の決まりと同じ)ので、そのまま使う。 # Ruby 4.0 では cgi ライブラリの大半が外されたが、エスケープの部分(cgi/escape)は残っている require "cgi/escape" puts CGI.escapeHTML(ARGV[0])
動かし方(ファイルを保存したフォルダで)
$ ruby escape.rb "<b>太字</b>"
escape.kt2.4.21 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
fun main(args: Array<String>) {
val s = args[0]
// Kotlin(JVM)の標準にはエスケープの関数がないので、1文字ずつ見て置き換える。
// 5文字はどれも1つの Char に収まるので、絵文字(Char 2つ)はそのまま書き写される
val escaped = buildString {
for (c in s) {
when (c) {
'&' -> append("&")
'<' -> append("<")
'>' -> append(">")
'"' -> append(""")
'\'' -> append("'")
else -> append(c)
}
}
}
println(escaped)
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc escape.kt -include-runtime -d escape.jar && java -jar escape.jar "<b>太字</b>"
escape.swift6.3.3 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
// Swift の標準ライブラリにはエスケープの関数がないので、1文字ずつ見て置き換える
let s = CommandLine.arguments[1]
var out = ""
// Swift の Character は「見た目の1文字」で、< のあとに結合文字が続くと2つで1文字になり '<' と一致しない。
// unicodeScalars で、コードポイント1つずつ見る
for u in s.unicodeScalars {
switch u {
case "&": out += "&"
case "<": out += "<"
case ">": out += ">"
case "\"": out += """
case "'": out += "'"
default: out.unicodeScalars.append(u)
}
}
print(out)
動かし方(ファイルを保存したフォルダで)
$ swift escape.swift "<b>太字</b>"
よく見かける書き方で試すと
仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | よく見かける書き方の答え(7言語) |
|---|---|
タグ"<b>太字</b>" |
|
5つすべて"& < > \" '" |
|
属性に入れる値(' と ")"It's \"OK\"" |
|
すでにエスケープしてある"& <" |
|
スクリプト"<script>alert('x')</script>" |
|
変える文字が無い"こんにちは 🍣" |
|
空の文字列"" | ✓7言語すべて正解 |
改行はそのまま"1行目
<2行目>" |
|
この書き方の決まり
&<>"'の5つを、&<>"'に変える。それ以外の文字(日本語・絵文字・改行)は変えない&を最初に変える- すでに
&と書いてあっても、もう一度変える(&amp;)
解説
' の書き方が、言語ごとに違う
' を '・'・' のどれで書いても、ブラウザでは同じ ' として表示されます。ただ、出力を文字列として比べると違うものになります。テストの期待値や、キャッシュのキーに使うときに困ります。
| 言語の標準の関数 | ' | " | そのほか |
|---|---|---|---|
PHP htmlspecialchars | ' | " | PHP 8.1 から、何も指定しなくても ' を変える |
Python html.escape | ' | " | |
Go html.EscapeString | ' | " | |
C# WebUtility.HtmlEncode | ' | " | 絵文字も 🍣 のような数字の形に変える |
Ruby CGI.escapeHTML | ' | " | この決まりとそのまま一致 |
| JavaScript・Java・Kotlin・Rust・Swift | 標準に関数が無い |
この書き方では、Ruby だけ標準の関数を使い、ほかは自分で置き換える形にしました。PHP の strtr、Python の translate、Go の strings.NewReplacer は、元の文字列を1回だけ見て置き換えるので、変える順番を気にしなくて済みます。
置き換えをつなぐときは、& を最初に
replace を5回つなぐ書き方では、& を最後に変えると、先に作った < の & まで変わって &lt; になります(Java・Kotlin の naive)。つなぐなら & を最初にします。
JavaScript の replace は、最初の1つしか変えない
JavaScript の s.replace("<", "<") は、最初に見つかった1つだけを変えます。Java の replace や C# の Replace はすべて変えるので、同じ名前でも動きが違います。すべて変えるには replaceAll を使うか、/</g のように g を付けた正規表現を渡します。
二重にエスケープしない
この決まりでは、& をもう一度変えて &amp; にします。テンプレートエンジン(多くは自動でエスケープする)に渡す値を自分でもエスケープすると、画面に & と出ます。エスケープは「HTML に埋め込む直前に1回だけ」が原則です。