HTMLの特殊文字をエスケープするには
HTML に埋め込む文字列の & < > と引用符を文字参照に置き換えるエスケープを各言語で示す。
置換の順序と、引用符の変換結果が言語ごとに違う点までを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
import html
raw = '<a href="x?a=1&b=2">Tom & \'Jerry\'</a>'
escaped = html.escape(raw)
print(escaped)
# <a href="x?a=1&b=2">Tom & 'Jerry'</a>
print(html.escape(raw, quote=False))
# <a href="x?a=1&b=2">Tom & 'Jerry'</a>(引用符は変換されない)
print(html.unescape(escaped) == raw) # True標準の html.escape が & < > と二重引用符・単一引用符を文字参照に変換する。
テキストだけに埋め込み、引用符を変換したくないときは quote=False を渡す。
単一引用符は ' になり、他の言語の ' とは書き方が違うが、どちらも同じ文字を指す数値参照である。
JavaScript
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
function escapeHtml(text) {
return text.replace(/[&<>"']/g, (ch) => ESCAPES[ch]);
}
const raw = `<a href="x?a=1&b=2">Tom & 'Jerry'</a>`;
console.log(escapeHtml(raw));
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>標準の HTML エスケープ関数が無いため、対象の 5 文字を対応表で置き換える関数を用意する。
正規表現で 1 回の走査にまとめておけば、& を先に置き換えた結果を後続の置換が壊す順序の問題が起きない。
TypeScript
const ESCAPES: Record<string, string> = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (ch) => ESCAPES[ch]);
}
const raw: string = `<a href="x?a=1&b=2">Tom & 'Jerry'</a>`;
console.log(escapeHtml(raw));
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>対応表を Record<string, string> で型付けし、引数と戻り値を string にしておく。
正規表現が 5 文字だけに絞っているので、ESCAPES[ch] が undefined になることはない。
実行時の振る舞いは JavaScript と同じである。
Go
package main
import (
"fmt"
"html"
)
func main() {
raw := `<a href="x?a=1&b=2">Tom & 'Jerry'</a>`
escaped := html.EscapeString(raw)
fmt.Println(escaped)
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>
fmt.Println(html.UnescapeString(escaped) == raw) // true
}標準の html パッケージの EscapeString が変換、UnescapeString が元に戻す関数である。
二重引用符は " ではなく " に、単一引用符は ' に変換される。
html/template のテンプレートは出力する場所に応じて自動でエスケープするため、この関数は手で組み立てた HTML に値を埋め込む場面で使う。
Rust
fn escape_html(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
fn main() {
let raw = r#"<a href="x?a=1&b=2">Tom & 'Jerry'</a>"#;
println!("{}", escape_html(raw));
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>
}標準ライブラリに HTML エスケープの関数は無いため、文字ごとに match で置き換える関数を書く。
1 文字ずつ新しい String へ書き出すので、& の置換順を気にしなくてよい。
依存クレートを増やさずに済むのも利点である。
Java
public class Main {
static String escapeHtml(String text) {
StringBuilder out = new StringBuilder(text.length());
for (char ch : text.toCharArray()) {
switch (ch) {
case '&' -> out.append("&");
case '<' -> out.append("<");
case '>' -> out.append(">");
case '"' -> out.append(""");
case '\'' -> out.append("'");
default -> out.append(ch);
}
}
return out.toString();
}
public static void main(String[] args) {
String raw = "<a href=\"x?a=1&b=2\">Tom & 'Jerry'</a>";
System.out.println(escapeHtml(raw));
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>
}
}標準ライブラリには HTML をエスケープする汎用のメソッドが無いため、char ごとに switch で置き換える関数を書く。
StringBuilder に書き出す方式なので、& を先に処理する順序を意識しなくてよい。
C#
using System;
using System.Net;
var raw = "<a href=\"x?a=1&b=2\">Tom & 'Jerry'</a>";
var escaped = WebUtility.HtmlEncode(raw);
Console.WriteLine(escaped);
// <a href="x?a=1&b=2">Tom & 'Jerry'</a>
Console.WriteLine(WebUtility.HtmlDecode(escaped) == raw); // TrueSystem.Net.WebUtility.HtmlEncode が標準で用意されており、HtmlDecode で元に戻せる。
引用符は " と ' に変換される。
é や © のような ASCII の範囲外の一部の文字も é や © の数値参照に変換されるが、日本語はそのまま残る。
つまずき
変換する文字は & < > " ' の 5 つで、特に & の扱いで失敗しやすい。
replace を連ねて < を < にしてから & を & にすると、a<b が a&lt;b になり、画面に「<」という文字がそのまま出てしまう。
& を最初に置き換えるか、今回のように 1 回の走査で全部を置き換える。
同じ文字列を二重にエスケープしても同じ問題が起きる。
Python で html.escape(html.escape("Tom & Jerry")) を実行すると Tom &amp; Jerry になった。
エスケープは HTML に出力する直前に 1 回だけ行う。
言語で違う変換結果
同じ入力でも、引用符と非 ASCII 文字の変換結果は言語ごとに違った。
- 単一引用符: Python は '、Go・C# と自作関数(JavaScript・TypeScript・Rust・Java)は '
- 二重引用符: Go は "、それ以外は "
- é と ©: C# は é と © に変換し、Python と Go はそのまま残した
どれもブラウザでは同じ文字として表示されるが、出力を文字列として比較するテストでは、使う関数を固定しておく。
この変換が効く範囲
5 文字の変換は、HTML のテキストと、引用符で囲んだ属性値の中に入れる文字列に対するものである。<script> の中身や、href に入れる URL は解釈のされ方が別なので、この変換だけでは安全にならない。
URL の一部を載せるときは、URL エンコードを別に行う。