为什么ElementTree会拒绝带有“编码不正确”错误的UTF-16 XML声明?

16
在Python 2.7中,当将编码为"UTF-16"的unicode字符串传递给ElementTree的fromstring()方法时,如果XML声明中指定的编码不正确,则会收到一个ParseError错误消息:
>>> from xml.etree import ElementTree
>>> data = u'<?xml version="1.0" encoding="utf-16"?><root/>'
>>> ElementTree.fromstring(data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Program Files (x86)\Python 2.7\lib\xml\etree\ElementTree.py", line 1300, in XML
    parser.feed(text)
  File "C:\Program Files (x86)\Python 2.7\lib\xml\etree\ElementTree.py", line 1642, in feed
    self._raiseerror(v)
  File "C:\Program Files (x86)\Python 2.7\lib\xml\etree\ElementTree.py", line 1506, in _raiseerror
    raise err
xml.etree.ElementTree.ParseError: encoding specified in XML declaration is incorrect: line 1, column 30

那是什么意思?是什么让ElementTree这样认为?

毕竟,我传入的是Unicode代码点,而不是字节串。这里没有涉及编码。怎么可能是错误的呢?

当然,有人可能会说任何编码都是不正确的,因为这些Unicode代码点并没有被编码。但是,为什么UTF-8不被拒绝为“不正确的编码”呢?

>>> ElementTree.fromstring(u'<?xml version="1.0" encoding="utf-8"?><root/>')
我可以轻松地解决这个问题,只需将Unicode字符串编码为UTF-16编码的字节字符串并将其传递给fromstring(),或者将Unicode字符串中的encoding="utf-16"替换为encoding="utf-8"。但我希望了解为什么会引发那个异常。ElementTree文档没有提到只接受字节字符串的事情。
具体而言,由于我的输入数据可能非常大,我想避免这些额外的操作,并尽可能地避免在内存中将它们重复处理并增加CPU开销。
1个回答

18
我不会试图为这种行为辩解,但是会解释为什么在代码编写时会出现这种情况。
简而言之:Python使用的XML解析器expat操作的是字节而不是Unicode字符。在将字符串传递给ElementTree.fromstring之前,必须调用.encode('utf-16-be').encode('utf-16-le')
ElementTree.fromstring(data.encode('utf-16-be'))

证明: ElementTree.fromstring 最终调用了 pyexpat.xmlparser.Parse,该函数实现在 pyexpat.c 中:

static PyObject *
xmlparse_Parse(xmlparseobject *self, PyObject *args)
{
    char *s;
    int slen;
    int isFinal = 0;

    if (!PyArg_ParseTuple(args, "s#|i:Parse", &s, &slen, &isFinal))
        return NULL;

    return get_parse_result(self, XML_Parse(self->itself, s, slen, isFinal));
}

所以您传递的Unicode参数将使用`s#`进行转换。 `PyArg_ParseTuple` 的docs说:
s#(字符串,Unicode或任何兼容读取缓冲区的对象)[const char*,int(或Py_ssize_t,请参见下文)]此变体在两个C变量中存储,第一个指向字符字符串的指针,第二个是其长度。 在这种情况下,Python字符串可能包含嵌入的空字节。 如果可能进行这样的转换,则 Unicode对象返回对象的默认编码字符串版本的指针。 所有其他兼容读取缓冲区的对象都会返回对原始内部数据表示的引用。
让我们来检查一下:
from xml.etree import ElementTree
data = u'<?xml version="1.0" encoding="utf-8"?><root>\u2163</root>'
print ElementTree.fromstring(data)

给出错误:

UnicodeEncodeError: 'ascii' codec can't encode character u'\u2163' in position 44: ordinal not in range(128)

这意味着当你指定encoding="utf-8"时,仅仅是因为你的输入中没有非ASCII字符,在Unicode字符串编码为ASCII时才能运作。如果在解析之前添加以下内容,则UTF-8将按预期工作:
import sys
reload(sys).setdefaultencoding('utf8')

然而,将defaultencoding设置为'utf-16-be'或'utf-16-le'是无效的,因为ElementTree的Python位执行直接字符串比较,在UTF-16领域开始出现失败。

感谢详尽的分析!听起来需要提交一个给ET文档的补丁。 :) - Henrik Heimbuerger
1
如何从文件中而非字符串中使用此函数? - Ali Karaca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接