python:在XML中转义非ASCII字符
我用下面的源文件成功打印出了我的测试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 个回答
关于这个问题,Python有一个公开的bug报告,链接在这里:https://bugs.python.org/issue5166。现在还不确定这个问题会怎么解决或者是否会被修复,因为这个报告已经开了有一段时间了。不过,定期查看一下这个链接是个好主意,看看Python是否会内置一个处理无效XML字符的合适解决方案。
这对我来说似乎有效。
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>
"\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>
</e>")
>>> t.text
'\n'
# Hex escaping of "\x01" doesn't work; it's not a valid XML character
>>> t = ET.fromstring("<e></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)