python:在XML中转义非ASCII字符

6 投票
3 回答
6573 浏览
提问于 2025-04-16 08:55

我用下面的源文件成功打印出了我的测试XML文件,但它对非ASCII字符的处理不太好:

xmltest.py:

import xml.sax.xmlreader
import xml.sax.saxutils

def testJunk(file, e2content):
  attr0 = xml.sax.xmlreader.AttributesImpl({})
  x =  xml.sax.saxutils.XMLGenerator(file)
  x.startDocument()
  x.startElement("document", attr0)

  x.startElement("element1", attr0)
  x.characters("bingo")
  x.endElement("element1")

  x.startElement("element2", attr0)
  x.characters(e2content)
  x.endElement("element2")

  x.endElement("document")
  x.endDocument()

如果我这样做:

>>> import xmltest
>>> xmltest.testJunk(open("test.xml","w"), "ascii 001: \001")

那么我得到的XML文件里会有字符代码001。我搞不清楚怎么处理这个字符。Firefox告诉我这不是一个格式正确的XML,并且对这个字符表示不满。我该怎么解决这个问题呢?

补充说明:我想记录一个我无法控制的函数的输出,这个函数输出了非ASCII字符。


更新:好的,现在我知道在接受的范围之外的字符不能用这种形式进行编码。(或者说,它们可以被编码,但这对XML格式不正确没有帮助。)不过,如果我定义一种方法,它们是可以被转义的。

(供将来参考:W3C有一个有用的页面,虽然不在XML标准内,但上面说“控制代码应该用适当的标记替换”,不过并没有给出具体的例子。)

如果我想用以下方式转义超出接受范围的字符:

转义前:(代表一个字符,而不是字面上的8个字符字符串)

 abcdefghijkl

转义后:

 abcd<u>0001</u>efgh<u>0002</u>ijkl

我该如何在Python中做到这一点呢?

def escapeXML(src)
    dest = ??????
    return dest

3 个回答

1

关于这个问题,Python有一个公开的bug报告,链接在这里:https://bugs.python.org/issue5166。现在还不确定这个问题会怎么解决或者是否会被修复,因为这个报告已经开了有一段时间了。不过,定期查看一下这个链接是个好主意,看看Python是否会内置一个处理无效XML字符的合适解决方案。

1

这对我来说似乎有效。

r = re.compile(ur'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF' \
  + ur'\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]')
def escapeInvalidXML(string):
  def replacer(m):
    return "<u>"+('%04X' % ord(m.group(0)))+"</u>"
  return re.sub(r,replacer,string)

举个例子:

>>> s='this is a \x01 test \x0B of something'
>>> escapeInvalidXML(s)
'this is a <u>0001</u> test <u>000B</u> of something'
>>> s2 = u'this is a \x01 test \x0B of \uFDD0'
>>> escapeInvalidXML(s2)
u'this is a <u>0001</u> test <u>000B</u> of <u>FDD0</u>'

字符范围可以参考这个链接:http://www.w3.org/TR/2006/REC-xml-20060816/#charsets。我没有把所有字符都处理,只处理了下面 \uFFFF 的那些。


更新:哎呀,忘了调整 SAX 的 startElement/characters 方法,并且要正确处理多行内容:

import re
import xml.sax.xmlreader
import xml.sax.saxutils

r = re.compile(ur'(.*?)(?:([^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF' \
    + ur'\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD])|([\n])|$)')
attr0 = xml.sax.xmlreader.AttributesImpl({})
def splitInvalidXML(string):
    list = []
    def replacer(m):
        g1 = m.group(1)
        if (len(g1) > 0):
            list.append(g1)
        g2 = m.group(2)
        if (not g2 == None):
            list.append(ord(g2))
        g3 = m.group(3)
        if (not g3 == None):
            list.append(g3)
        return ""
    re.sub(r,replacer,string)
    return list

def submitCharacters(x, string):
    for fragment in splitInvalidXML(string):
        if (isinstance(fragment,int)):
            x.startElement("u", attr0)
            x.characters('%04X' % fragment)
            x.endElement("u")
        else:
            x.characters(fragment)

def test1(fname):
    with open(fname,'w') as f:
        x = xml.sax.saxutils.XMLGenerator(f)
        x.startDocument()
        x.startElement('document',attr0)
        submitCharacters(x, 'this is a \x01 test\nof the \x02\x0b xml system.')
        x.endElement('document')
        x.endDocument()

test1('test.xml')

这样会产生:

<?xml version="1.0" encoding="iso-8859-1"?>
<document>this is a <u>0001</u> test
of the <u>0002</u><u>000B</u> xml system.</document>
4

"\001" 也就是 \x01 是一个 ASCII 控制码。不过,它并不是 XML 允许的字符。只有 这几个 ASCII 控制码 是可以用的,它们是 \t(制表符)、\n(换行符)和 \r(回车符)。

举个例子:

>>> import xml.etree.cElementTree as ET
# Raw newline works
>>> t = ET.fromstring("<e>\n</e>")
>>> t.text
'\n'

# Hex escaping of a newline works
>>> t = ET.fromstring("<e>&#xa;</e>")
>>> t.text
'\n'

# Hex escaping of "\x01" doesn't work; it's not a valid XML character
>>> t = ET.fromstring("<e>&#x1;</e>")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 106, in XML
cElementTree.ParseError: reference to invalid character number: line 1, column 3

如果你想在 XML 文档中包含一些不合法的 XML 字符,你必须通过额外的转义方式来隐藏它们,以免被 XML 解析器识别。这个方法需要被记录下来,发布出去,并且让读者能够理解。

比如,在 Microsoft Excel 2007 及以上版本的 XLSX 文件中,不能作为有效 XML 字符的 Unicode 代码点是通过 _xhhhh_ 的形式被“走私”过去的,其中 hhhh 是这个代码点的十六进制表示。在你的例子中,这将是 7 个字节 _x0001_。注意,文本中任何 _ 字符都需要被转义,以免被错误地解释为开始一个 _xhhhh_ 的序列。

这真是麻烦、痛苦且效率低下。你可能需要考虑其他的方法。使用 XML 真的是必要的吗?用 CSV 文件(天哪,太可怕了!)是否能更好地满足你的需求呢?

编辑 关于提问者的编码提议的一些说明:

A. 虽然 \r 是有效的 XML 1.0 输入字符,但它需要 立即进行强制转换,所以你也应该对它进行转义。

B. 这个方案假设/希望 <u>hhhh</u> 不会和其他标记混淆。

C. 我之前说的关于 Microsoft 转义方案的看法有些偏颇。其实它相对来说是比较优雅、简单且高效的。为了让你的读者更好地理解你的方案,你应该展示一下需要的代码,以便将那些麻烦的部分解码并重新组合起来。请记住,微软的方案需要有人写一个转义函数和一个解码函数,而你的方案则需要对每种工具(SAX、DOM、ElementTree)进行不同的处理。

D. 在细节上,代码有点不太规范:

if (len(g1) > 0): 应该写成 if g1:

if (not foo == None): 有三个地方偏离了常见的写法:(1)多余的括号(2)用 not x == y 而不是 x != y(3)用 != None 而不是 is not None

不要用 list(以及其他内置对象的名称)作为你自己变量的名字。

编辑 2 你想用正则表达式分割字符串,为什么不直接用 re.split 呢?

splitInvalidXML2 = re.compile(
    ur'([^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD])'
    ).split

def submitCharacters2(x, string):
    badchar = True
    for fragment in splitInvalidXML2(string):
        badchar = not badchar
        if badchar:
            x.startElement("u", attr0)
            x.characters('%04X' % ord(fragment))
            x.endElement("u")
        elif fragment:
            x.characters(fragment)

撰写回答