Escape e unescape di XML
Applica l’escape ai caratteri speciali, o riporta le entità a testo.
Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.
Incolla del testo a sinistra e torna indietro con ogni carattere che XML tratta come markup sostituito da un riferimento a entità. Inverti la direzione e i riferimenti vengono decodificati nei caratteri che nominano. L’input è testo semplice e non un documento, quindi non deve essere analizzabile: un frammento, un singolo valore di attributo o una URL da sola funzionano tutti.
Ci si arriva dopo che un parser si è lamentato di una e commerciale. "The entity name must immediately follow the '&'" di Xerces, "EntityRef: expecting ';'" di libxml2 e "invalid character in entity name" di Expat sono lo stesso problema: una & nuda nel testo o in un attributo, di solito da una query string tipo ?a=1&b=2 incollata dentro un elemento.
Qui la differenza è ciò che lo strumento si rifiuta di fare. XML definisce cinque entità con nome e nessun’altra. La maggior parte degli escaper sul web sono escaper HTML con l’etichetta XML e ti restituiranno , che nessun parser XML accetta. Questo conosce le cinque e i riferimenti numerici in decimale ed esadecimale, lascia tutto il resto esattamente com’è scritto, e non carica nulla.
Cinque entità predefinite, e basta
La sezione 4.6 di XML 1.0 definisce esattamente cinque entità con nome. Quello è l’insieme completo. Non esiste una tabella di entità HTML ereditata, ed è la sorpresa consueta quando un blocco di HTML viene spostato in un file di configurazione XML o in una description RSS.
Scrivere in un documento senza DTD non è un problema di stile, è un errore di buona formazione: il parser ha un nome e nulla gli dice che cosa significhi. Usa   oppure   al suo posto, e lo stesso per © ed é. Una DTD può dichiarare nomi ulteriori, ed è così che &companyName; funziona in DocBook: per questo la direzione di unescape lascia intatto qualunque nome non riconosca.
- & per &
- < per <
- > per >
- " per "
- ' per '. HTML 4 non ha mai definito ', perciò i serializzatori che alimentano pipeline miste emettono spesso ' al suo posto.
Riferimenti numerici di carattere
Qualsiasi carattere lecito può essere scritto con il suo code point: é in decimale, é in esadecimale. I due sono lo stesso carattere, gli zeri iniziali sono ammessi, e la x deve essere minuscola: A non è quindi un riferimento di carattere e viene rifiutato come entità non dichiarata.
La direzione di unescape gestisce entrambe le forme e controlla che il risultato stia nell’intervallo Unicode; un riferimento malformato o fuori intervallo resta com’è scritto invece di essere sostituito da un punto interrogativo. La direzione di escape non emette mai riferimenti numerici: UTF-8 porta accenti, CJK ed emoji così come sono, quindi convertirli costa leggibilità e non dà nulla.
Quando ogni carattere va davvero sottoposto a escape
Le regole sono più strette di quanto quasi tutti immaginino, e conta quando stai leggendo il documento di qualcun altro e devi decidere se è rotto. La & e il < sono obbligatori ovunque. Il > è richiesto in un solo punto: la sezione 2.4 di XML 1.0 dice che va sottoposto a escape dentro la sequenza letterale ]]> nel contenuto, quando questa non sta chiudendo una sezione CDATA. Quindi <code>if (a]]>b)</code> è obbligatorio e <note>a > b</note> è lecito.
Questo strumento applica l’escape a tutti e cinque senza condizioni, un soprainsieme di ciò che serve a qualunque contesto: l’output si può quindi incollare ovunque senza tenere traccia del contesto in cui ti trovi. Non è l’escape minimo, e ora sai quali caratteri rimettere a posto.
- & e <: obbligatori nel contenuto degli elementi e nei valori degli attributi, sempre.
- >: facoltativo, tranne dentro la sequenza ]]>.
- ": obbligatorio solo dentro un valore di attributo racchiuso fra virgolette doppie.
- ': obbligatorio solo dentro un valore di attributo racchiuso fra apici singoli. Nel contenuto degli elementi nessuna delle due virgolette richiede escape.
I caratteri a cui non si può applicare alcun escape
XML 1.0 definisce quali caratteri un documento possa contenere, la produzione Char, e gran parte dell’intervallo di controllo C0 non c’è: da U+0001 a U+0008, U+000B, U+000C e da U+000E a U+001F. Sopravvivono solo tabulazione, avanzamento riga e ritorno a capo. U+0000 è illecito in ogni versione di XML, e lo sono anche i surrogati isolati.
Quindi  non è un escape per il carattere ESC. Viola il vincolo Legal Character, perché il carattere che nomina non può comparire in un documento XML 1.0 in alcun modo, e scriverlo come riferimento non lo ripulisce. Python che segnala "not well-formed (invalid token)" su un file di log pieno di sequenze di escape ANSI è esattamente questo; per byte arbitrari la risposta è base64.
Lo strumento su questo è onesto anziché furbo: l’escape lascia passare un carattere di controllo tale e quale, e l’unescape trasforma  in un vero U+0001, perché è ciò che dice il riferimento. Passa il risultato nel controllo di sintassi prima di rimetterlo in un documento.
Farlo da codice
Ogni linguaggio offre qualcosa per questo e la maggior parte offre la cosa sbagliata, perché la funzione ovvia è un escaper HTML. Questi esempi usano la strada specifica per XML in ciascun ecosistema, e dicono che cosa ognuno sbaglia.
// 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();Lo schema comune a tutti e cinque: l’escaper a livello di stringa è la risposta comoda, il writer è quella corretta, perché solo il writer sa se sta scrivendo contenuto di elemento o un valore di attributo, e solo il writer rifiuta i caratteri a cui non si può applicare alcun escape.
Domande frequenti
Perché rompe il mio XML?
Perché XML non l’ha mai definito. Le cinque entità predefinite sono &, <, >, " e ', e l’elenco finisce lì. Tutto il resto che ti dà HTML viene da una tabella di entità che XML deliberatamente non ha ereditato.
Un parser che incontra senza una DTD nello scope segnala un’entità non dichiarata: ha un nome e nulla gli dice che cosa significhi. Usa   o   al suo posto. L’eccezione è un documento la cui DTD dichiara quel nome, ed è per questo che lo stesso markup funziona in XHTML e fallisce in un semplice file di configurazione XML.
Devo applicare l’escape al segno di maggiore?
Quasi mai. XML 1.0 lo richiede in una sola situazione: quando > compare nella sequenza letterale ]]> dentro il contenuto di un elemento e non sta chiudendo una sezione CDATA, dove altrimenti un parser che cerca la fine di una sezione CDATA andrebbe in confusione.
Ovunque altro è facoltativo, anche nei valori degli attributi, quindi <note>a > b</note> è ben formato così com’è. Questo strumento lo converte comunque, così l’output è sicuro da incollare in qualunque contesto. Per la forma minima, rimetti > ovunque tranne dentro una sequenza ]]>.
Meglio usare CDATA invece dell’escape?
CDATA sopprime il riconoscimento di < e & come markup. È tutto ciò che fa, ed è lo strumento giusto quando una persona modifica il contenuto a mano: codice sorgente incorporato, un’espressione XSLT piena di parentesi angolari, SQL mantenuto a mano.
È lo strumento sbagliato nel resto dei casi. Non può contenere la sequenza ]]>, perché questa lo termina, il che ne fa un vero vettore di injection per contenuti non fidati. Non espande le entità, quindi <![CDATA[&]]> produce sei caratteri letterali, e non ammette caratteri illeciti. La maggior parte dei serializzatori non lo conserva nemmeno come nodo distinto: non trattare mai l’"essere CDATA" come qualcosa di significativo.
Il testo che incollo viene mandato da qualche parte?
No. L’escape è una sostituzione di stringhe eseguita come JavaScript in questa scheda. Non c’è alcuna richiesta da fare, quindi non c’è alcun server a cui farla.
Qui conta più di quanto sembri: questo strumento si usa sulla parte di payload che si è rotta, il che in pratica significa URL con token nella query string, stringhe di connessione e testo d’errore preso da un log. Apri il pannello di rete mentre scrivi e resterà vuoto. Quello che scrivi è conservato nel localStorage di questo browser così un ricaricamento non lo perde, non viene mai trasmesso, e Svuota lo rimuove all’istante.
Perché la mia & è diventata &amp;?
Il testo è stato sottoposto a escape due volte, di solito per un escape manuale applicato a una stringa che un writer aveva già trattato, o per un template che fa escape in uscita a cui è stato passato un valore già trattato in ingresso.
La direzione di unescape toglie uno strato: eseguila una volta per tornare a & e una seconda per tornare a &. Se &amp; continua a comparire in produzione, il bug è quasi sempre un escape fatto in casa davanti a un writer DOM o di stream che stava già facendo il lavoro. Applicare l’escape nell’ordine sbagliato fallisce al contrario: sostituire < prima di & trasforma < in &lt;, ed è per questo che l’esempio JavaScript fa una sola passata con un’espressione regolare.