XMLエスケープとは:特殊文字の扱いとPython・JavaScriptでの実装

スポンサーリンク

XMLには特別な意味を持つ文字があります。これらをそのまま書くとXMLパーサーが誤解してエラーになるため、エスケープ(別の表現に置き換え)が必要です。

APIのリクエストボディにコードスニペットやHTMLを含める場合、XMLエスケープを忘れると400エラーになります。

エスケープが必要な文字

文字 エスケープ後 理由
< &lt; タグの開始と誤解される
> &gt; タグの終了と誤解される
& &amp; エンティティの開始と誤解される
" &quot; 属性値の区切りと誤解される
' &apos; 属性値の区切りと誤解される

エスケープが必要な例

<!-- NG: <-ch がタグの開始と解釈されてパースエラー -->
<content>result := <-ch</content>

<!-- OK -->
<content>result := &lt;-ch</content>
<!-- NG: & がエンティティの開始と解釈される -->
<title>A & B</title>

<!-- OK -->
<title>A &amp; B</title>

Python でのエスケープ

xml.sax.saxutils.escape()

標準ライブラリの xml.sax.saxutils にある escape()<>& を変換します。

from xml.sax.saxutils import escape

text = 'result := <-ch  // チャネルから受け取る'
print(escape(text))
# result := &lt;-ch  // チャネルから受け取る

"' も変換したい場合は entities 引数を使います。

from xml.sax.saxutils import escape

text = 'He said "Hello & Goodbye"'
escaped = escape(text, entities={'"': '&quot;', "'": '&apos;'})
print(escaped)
# He said &quot;Hello &amp; Goodbye&quot;

逆変換(アンエスケープ)

unescape() でエスケープを元に戻せます。

from xml.sax.saxutils import unescape

text = '&lt;div&gt;Hello &amp; World&lt;/div&gt;'
print(unescape(text))
# <div>Hello & World</div>

xml.etree.ElementTree を使う場合

ET.SubElementET.tostring を使うと自動でエスケープしてくれるため、手動エスケープ不要です。

from xml.etree import ElementTree as ET

root = ET.Element("entry")
title = ET.SubElement(root, "title")
title.text = "A & B <sample>"  # 自動でエスケープされる

print(ET.tostring(root, encoding="unicode"))
# <entry><title>A &amp; B &lt;sample&gt;</title></entry>

JavaScript でのエスケープ

JavaScriptには標準のXMLエスケープ関数がないため、自作するか DOMParser を使います。

自作関数

function escapeXml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

console.log(escapeXml('result := <-ch'));
// result := &lt;-ch

& を最初に変換しないと、後のエスケープ結果(&lt; など)がさらに変換されてしまうため順番が重要です。

TextNode を使う方法(ブラウザ環境)

function escapeXml(str) {
  const div = document.createElement('div');
  div.appendChild(document.createTextNode(str));
  return div.innerHTML;
}

Ruby でのエスケープ

require 'cgi'

CGI.escapeHTML('<div>Hello & "World"</div>')
# => "&lt;div&gt;Hello &amp; &quot;World&quot;&lt;/div&gt;"

Go でのエスケープ

html パッケージの EscapeString はXMLにも使えます。

import "html"

s := html.EscapeString("<-ch // チャネル受信")
fmt.Println(s)
// &lt;-ch // チャネル受信

encoding/xml パッケージでXMLを構造体から生成する場合は自動エスケープされます。

type Entry struct {
    Title string `xml:"title"`
}

entry := Entry{Title: "A & B <sample>"}
out, _ := xml.Marshal(entry)
fmt.Println(string(out))
// <Entry><title>A &amp; B &lt;sample&gt;</title></Entry>

よくあるエラーと原因

エラー 原因
400 XML Parse Failed <& などが未エスケープのままXMLに含まれている
XML syntax error 属性値の " がエスケープされていない
entity not defined & の後に認識できないエンティティ名が続いている

まとめ

# Python(標準ライブラリ)
from xml.sax.saxutils import escape
escaped = escape(text)
// JavaScript
const escaped = str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
# Ruby
require 'cgi'
escaped = CGI.escapeHTML(text)
// Go
import "html"
escaped := html.EscapeString(text)
  • & は必ず最初に変換する(順番を間違えると二重エスケープになる)
  • XMLライブラリ(Python の ElementTree、Go の encoding/xml)を使えば自動エスケープされるため手動変換不要
  • AtomPub API や RSS フィードなど、XMLをPOSTするときは必ずエスケープを確認する