Escaping strings for use in XML

Viewed 40138

I'm using Python's xml.dom.minidom to create an XML document. (Logical structure -> XML string, not the other way around.)

How do I make it escape the strings I provide so they won't be able to mess up the XML?

7 Answers

The accepted answer from Andrey Vlasovskikh is the most complete answer to the OP. But this topic comes up for most frequent searches for python escape xml and I wanted to offer a time comparison of the three solutions discussed in this article along with offering a fourth option we choose to deploy due to the enhanced performance it offered.

All four rely on either native python data handling, or python standard library. The solutions are offered in order from the slowest to the fastest performance.

Option 1 - regex

This solution uses the python regex library. It yields the slowest performance:

import re
table = {
    "<": "&lt;",
    ">": "&gt;",
    "&": "&amp;",
    "'": "&apos;",
    '"': "&quot;",
}
pat = re.compile("({})".format("|".join(table)))

def xmlesc(txt):
    return pat.sub(lambda match: table[match.group(0)], txt)

>>> %timeit xmlesc('<&>"\'')
1.48 µs ± 1.73 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

FYI: µs is the symbol for microseconds, which is 1-millionth of second. The other implementation's completion times are measured in nanoseconds (ns) which is billionths of a second.

Option 2 -- xml.sax.saxutils

This solution uses python xml.sax.saxutils library.

from xml.sax.saxutils import escape
def xmlesc(txt):
    return escape(txt, entities={"'": "&apos;", '"': "&quot;"})

>>> %timeit xmlesc('<&>"\'')
832 ns ± 4.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Option 3 - str.replace

This solution uses the string replace() method. Under the hood, it implements similar logic as python's xml.sax.saxutils. The saxutils code has a for loop that costs some performance, making this version slightly faster.

def xmlesc(txt):
    txt = txt.replace("&", "&amp;")
    txt = txt.replace("<", "&lt;")
    txt = txt.replace(">", "&gt;")
    txt = txt.replace('"', "&quot;")
    txt = txt.replace("'", "&apos;")
    return txt

>>> %timeit xmlesc('<&>"\'')
503 ns ± 0.725 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Option 4 - str.translate

This is the fastest implementation. It uses the string translate() method.

table = str.maketrans({
    "<": "&lt;",
    ">": "&gt;",
    "&": "&amp;",
    "'": "&apos;",
    '"': "&quot;",
})
def xmlesc(txt):
    return txt.translate(table)

>>> %timeit xmlesc('<&>"\'')
352 ns ± 0.177 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
xml_special_chars = {
    "<": "&lt;",
    ">": "&gt;",
    "&": "&amp;",
    "'": "&apos;",
    '"': "&quot;",
}

xml_special_chars_re = re.compile("({})".format("|".join(xml_special_chars)))

def escape_xml_special_chars(unescaped):
    return xml_special_chars_re.sub(lambda match: xml_special_chars[match.group(0)], unescaped)

All the magic happens in re.sub(): argument repl accepts not only strings, but also functions.

Related