Escapar e desescapar XML
Escape caracteres especiais, ou converta entidades em texto.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole texto à esquerda e ele volta com cada caractere que o XML trata como marcação substituído por uma referência de entidade. Inverta a direção e as referências são decodificadas de volta nos caracteres que nomeiam. A entrada é texto puro, não um documento, então não precisa nem fazer parsing: um trecho, um único valor de atributo ou uma URL sozinha funcionam igualmente.
Você recorre a isto depois que um analisador reclamou de um "e comercial". "The entity name must immediately follow the '&'" do Xerces, "EntityRef: expecting ';'" do libxml2 e "invalid character in entity name" do Expat são todos o mesmo problema: um & solto no texto ou num atributo, normalmente vindo de uma query string como ?a=1&b=2 colada dentro de um elemento.
O que é diferente aqui é o que a ferramenta se recusa a fazer. O XML define cinco entidades nomeadas e nenhuma a mais. A maioria dos escapadores da web são escapadores de HTML com rótulo de XML e vão te entregar , que nenhum analisador XML aceita. Este conhece as cinco e as referências numéricas em decimal e hexadecimal, deixa qualquer outra coisa exatamente como estava escrita, e não envia nada.
Cinco entidades predefinidas, e nada além disso
A seção 4.6 do XML 1.0 define exatamente cinco entidades nomeadas. Esse é o conjunto inteiro. Não existe tabela de entidades HTML herdada, que é a surpresa habitual quando um bloco de HTML é movido para um arquivo de configuração XML ou para uma description de RSS.
Escrever num documento sem DTD não é um problema de estilo, é um erro de boa formação: o analisador tem um nome e nada lhe diz o que esse nome significa. Use   ou   no lugar, e o mesmo vale para © e é. Uma DTD pode declarar nomes extras, que é como &companyName; funciona no DocBook, então a direção de desescape deixa qualquer nome que ela não reconheça exatamente como estava escrito.
- & para &
- < para <
- > para >
- " para "
- ' para '. O HTML 4 nunca definiu ', então serializadores que alimentam pipelines mistas costumam emitir ' em vez dele.
Referências numéricas de caractere
Qualquer caractere legal pode ser escrito pelo seu ponto de código: é em decimal, é em hexadecimal. Os dois são o mesmo caractere, zeros à esquerda são permitidos, e o x precisa ser minúsculo, então A não é uma referência de caractere e é rejeitado como entidade não declarada.
A direção de desescape trata as duas formas e confere se o resultado está dentro da faixa Unicode; uma referência malformada ou fora da faixa é deixada como está, em vez de virar um ponto de interrogação. A direção de escape nunca emite referências numéricas: o UTF-8 carrega acentos, CJK e emoji como eles mesmos, então escapá-los custa legibilidade e não traz nada.
Quando cada caractere precisa mesmo ser escapado
As regras são mais estreitas do que quase todo mundo supõe, o que importa quando você está lendo o documento de outra pessoa e decidindo se ele está quebrado. O & e o < são obrigatórios em todo lugar. O > é exigido num único lugar: a seção 2.4 do XML 1.0 diz que ele precisa ser escapado dentro da sequência literal ]]> no conteúdo, quando isso não estiver fechando uma seção CDATA, então <code>if (a]]>b)</code> é obrigatório e <note>a > b</note> é legal.
Esta ferramenta escapa os cinco incondicionalmente, um superconjunto do que qualquer contexto exige, então a saída pode ser jogada em qualquer lugar sem você precisar acompanhar em que contexto está. Não é o escape mínimo, e agora você sabe quais caracteres pode devolver.
- & e <: obrigatórios no conteúdo de elemento e em valores de atributo, sempre.
- >: opcional, exceto dentro da sequência ]]>.
- ": obrigatório só dentro de um valor de atributo entre aspas duplas.
- ': obrigatório só dentro de um valor de atributo entre aspas simples. Nenhuma das aspas precisa de escape no conteúdo de elemento.
Os caracteres que não dá para escapar de jeito nenhum
O XML 1.0 define quais caracteres um documento pode conter, a produção Char, e boa parte da faixa de controle C0 não está lá: U+0001 a U+0008, U+000B, U+000C e U+000E a U+001F. Só tabulação, quebra de linha e retorno de carro sobrevivem. U+0000 é ilegal em toda versão do XML, e substitutos isolados também são.
Então  não é um escape para o caractere ESC. Ele viola a restrição Legal Character, porque o caractere que ele nomeia não pode aparecer num documento XML 1.0 por meio algum, e escrevê-lo como referência não o higieniza. O Python reportando "not well-formed (invalid token)" num arquivo de log cheio de sequências de escape ANSI é exatamente isso; para bytes arbitrários a resposta é base64.
A ferramenta é honesta quanto a isso em vez de esperta: o escape deixa um caractere de controle passar direto, e o desescape transforma  num U+0001 de verdade, porque é o que a referência diz. Passe o resultado pelo verificador de sintaxe antes de devolvê-lo a um documento.
Fazendo isso em código
Toda linguagem traz algo para isso e a maioria traz a coisa errada, porque a função óbvia é um escapador de HTML. Estes usam o caminho específico de XML em cada ecossistema, e dizem o que cada um erra.
// XML defines exactly five named entities. A single regex pass avoids the
// classic bug of replacing & after < and turning < into &lt;.
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
// Element content: & and < are mandatory, > only inside the sequence ]]>.
function escapeText(s) {
return s.replace(/[&<]/g, (c) => ESCAPES[c]).replace(/]]>/g, ']]>');
}
// Attribute value: escape the delimiter you are using, plus tab, newline and
// carriage return. Attribute-value normalisation turns a literal one of those
// into a plain space, so a reference is the only way to keep it.
function escapeAttribute(s) {
return s
.replace(/[&<"]/g, (c) => ESCAPES[c])
.replace(/\t/g, '	')
.replace(/\n/g, ' ')
.replace(/\r/g, ' ');
}
// The reverse. Note the deliberate absence of an HTML entity table: is
// undefined in XML, so leaving it alone is more honest than decoding it.
const NAMED = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
function unescapeXml(s) {
return s.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[A-Za-z][A-Za-z0-9]*);/g, (whole, body) => {
if (body[0] === '#') {
const hex = body[1] === 'x';
const cp = parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
return cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : whole;
}
return NAMED[body] !== undefined ? NAMED[body] : whole;
});
}from xml.sax.saxutils import escape, quoteattr, unescape
import re
# escape() handles & < > only and knows nothing about quotes. The > is not
# strictly required but it is harmless and covers the ]]> case for free.
text = escape('Terms & conditions <see clause 4>')
# Terms & conditions <see clause 4>
# The two quote entities have to be supplied yourself.
FIVE = {'"': '"', "'": '''}
text = escape(source, FIVE)
# quoteattr() returns the value WITH its delimiters, picking single quotes when
# the value contains a double quote, and turning tab, newline and carriage
# return into numeric references so normalisation cannot flatten them.
attr = quoteattr('say "hello" then stop')
# 'say "hello" then stop' <- single-quoted, so the " needs no escape
# unescape() reverses & < > plus whatever mapping you pass. It does NOT decode
# numeric character references, so é comes back unchanged. html.unescape()
# does decode them, but it also decodes and 250 other HTML names XML
# never defined, which silently rewrites your data. Do it explicitly instead:
def unescape_xml(s: str) -> str:
s = unescape(s, {'"': '"', ''': "'"})
return re.sub(
r'&#(x[0-9a-fA-F]+|[0-9]+);',
lambda m: chr(int(m.group(1)[1:], 16) if m.group(1)[0] == 'x' else int(m.group(1))),
s,
)// The JDK has no public XML escaper for a bare string. Two right answers.
// 1. Let a writer do it. XMLStreamWriter escapes for the context it is
// writing into, the only approach that gets attribute values right.
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
XMLStreamWriter w = XMLOutputFactory.newInstance().createXMLStreamWriter(out);
w.writeStartElement("note");
w.writeAttribute("href", "https://example.com/?a=1&b=2"); // escaped for you
w.writeCharacters("Terms & conditions <apply>"); // escaped for you
w.writeEndElement();
w.close();
// 2. Escape a standalone string with commons-text. Do NOT use commons-lang3's
// deprecated StringEscapeUtils.escapeXml, which knew nothing about the
// characters XML cannot represent.
import org.apache.commons.text.StringEscapeUtils;
String safe = StringEscapeUtils.escapeXml10("Terms & conditions <apply>");
// Terms & conditions <apply>
// escapeXml10 does something its name does not advertise: it DELETES the
// characters XML 1.0 cannot hold (U+0001 to U+0008, U+000B, U+000C,
// U+000E to U+001F, unpaired surrogates) rather than escaping them, because
// no escape for them exists. escapeXml11 writes the control characters as
// numeric references instead, which is only legal if the document declares
// version="1.1", and it still drops U+0000 and unpaired surrogates.
String back = StringEscapeUtils.unescapeXml(safe);
// The five predefined entities plus decimal and hex character references.
// Names it does not know are left as written.using System.Linq;
using System.Security;
using System.Xml;
// SecurityElement.Escape does the five predefined entities in one call:
// < > & " ' every time, a superset of what any single context needs.
string safe = SecurityElement.Escape("Terms & conditions <apply>");
// Terms & conditions <apply>
// It does not remove the characters XML cannot carry, so check those yourself.
// XmlConvert.IsXmlChar implements the Char production from XML 1.0. Keep
// surrogates or astral characters (emoji, rarer CJK) are destroyed.
static string StripIllegal(string s) =>
string.Concat(s.Where(c => XmlConvert.IsXmlChar(c) || char.IsSurrogate(c)));
// In practice prefer a writer. It escapes for the context and throws on a
// character the document cannot hold, instead of producing a file that fails
// to parse somewhere downstream.
var settings = new XmlWriterSettings { Indent = true, CheckCharacters = true };
using var writer = XmlWriter.Create(Console.Out, settings);
writer.WriteStartElement("note");
writer.WriteAttributeString("href", "https://example.com/?a=1&b=2");
writer.WriteString("Terms & conditions <apply>");
writer.WriteEndElement();
// There is no framework unescaper, and WebUtility.HtmlDecode is the wrong
// tool: it decodes and the rest of the HTML set. Read a fragment
// instead, which decodes exactly what XML defines and nothing else.
static string Unescape(string escaped)
{
var rs = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
using var r = XmlReader.Create(new StringReader("<r>" + escaped + "</r>"), rs);
r.ReadToFollowing("r");
return r.ReadElementContentAsString();
}<?php
// ENT_XML1 is the flag that matters and the one everybody omits. Without it
// you get the HTML entity table: an apostrophe becomes ' (harmless), and
// with ENT_HTML5 you can get names XML will reject outright.
$safe = htmlspecialchars(
$text,
ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
// & < > " ' become & < > " '
//
// ENT_SUBSTITUTE replaces invalid UTF-8 with U+FFFD. Before PHP 8.1 the
// default was to return an empty string on invalid input, which is very easy
// to miss in a feed generator fed by a legacy database.
$back = html_entity_decode($safe, ENT_XML1 | ENT_QUOTES, 'UTF-8');
// With ENT_XML1 this decodes the five predefined entities and numeric
// character references and leaves alone. Drop the flag and it decodes
// into U+00A0, silently rewriting your data.
// Building a document rather than a string: DOMDocument escapes on save and
// rejects characters XML cannot represent, which htmlspecialchars passes
// straight through.
$doc = new DOMDocument('1.0', 'UTF-8');
$note = $doc->createElement('note');
$note->appendChild($doc->createTextNode($text));
$doc->appendChild($note);
echo $doc->saveXML();O padrão nos cinco: o escapador em nível de string é a resposta conveniente e o writer é a correta, porque só o writer sabe se está escrevendo conteúdo de elemento ou valor de atributo, e só o writer recusa caracteres que não podem ser escapados de forma alguma.
Perguntas frequentes
Por que quebra meu XML?
Porque o XML nunca o definiu. As cinco entidades predefinidas são &, <, >, " e ', e essa é a lista completa. Tudo o mais que o HTML lhe dá vem de uma tabela de entidades que o XML deliberadamente não herdou.
Um analisador que encontra sem uma DTD em escopo reporta uma entidade não declarada: ele tem um nome e nada lhe diz o que esse nome significa. Use   ou   no lugar. A exceção é um documento cuja DTD declara o nome, e é por isso que a mesma marcação funciona em XHTML e falha num arquivo de configuração XML comum.
Preciso escapar o sinal de maior que?
Quase nunca. O XML 1.0 exige isso numa situação: quando > aparece na sequência literal ]]> dentro do conteúdo de um elemento e não está fechando uma seção CDATA, caso em que um analisador procurando o fim de uma seção CDATA ficaria confuso.
Em todo o resto é opcional, inclusive dentro de valores de atributo, então <note>a > b</note> está bem formado do jeito que está. Esta ferramenta escapa mesmo assim para que a saída seja segura de colar em qualquer contexto. Para a forma mínima, devolva o > em todo lugar, menos dentro de uma sequência ]]>.
Devo usar CDATA em vez de escapar?
CDATA suprime o reconhecimento de < e & como marcação. É só isso que ele faz, e é a ferramenta certa quando uma pessoa edita o conteúdo à mão: código-fonte embutido, uma expressão XSLT cheia de sinais de menor e maior, SQL mantido manualmente.
É a ferramenta errada no resto do tempo. Ele não pode conter a sequência ]]>, porque ela o encerra, o que o torna um vetor de injeção real para conteúdo não confiável. Ele não expande entidades, então <![CDATA[&]]> dá seis caracteres literais, e não permite caracteres ilegais. A maioria dos serializadores também não o preserva como nó distinto, então nunca trate "ser CDATA" como algo significativo.
O texto que eu colo é enviado para algum lugar?
Não. O escape é uma substituição de string rodando como JavaScript nesta aba. Não há requisição a fazer, então não há servidor para quem fazê-la.
Isso importa aqui mais do que parece: esta ferramenta é usada na parte do payload que quebrou, o que na prática significa URLs com tokens na query string, strings de conexão e texto de erro tirado de um log. Abra o painel de rede enquanto digita e ele vai continuar vazio. Sua entrada fica no localStorage deste navegador para que um recarregamento não a perca, nunca é transmitida, e Limpar a remove na hora.
Por que meu & virou &amp;?
O texto foi escapado duas vezes, geralmente por um escape manual aplicado a uma string que um writer já tinha escapado, ou por um template que escapa na saída recebendo um valor escapado na entrada.
A direção de desescape desfaz uma camada, então rode uma vez para voltar a & e outra para voltar a &. Se &amp; fica aparecendo em produção, o bug quase sempre é um escape feito à mão na frente de um writer de DOM ou de stream que já estava fazendo o serviço. Escapar na ordem errada falha ao contrário: substituir < antes de & transforma < em &lt;, e por isso o exemplo de JavaScript usa uma única passagem com expressão regular.