问题 为什么ElementTree拒绝使用“编码不正确”的UTF-16 XML声明?


在Python 2.7中,将unicode字符串传递给ElementTree时 fromstring() 方法有 encoding="UTF-16" 在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() 或者通过更换 encoding="utf-16" 同 encoding="utf-8" 在unicode字符串中,但我想了解为什么会引发异常。该 ElementTree的文档 没有说只接受字节串​​。

具体来说,我想避免这些额外的操作,因为我的输入数据可能会变得非常大,我想避免在内存中使用它们两次,并且处理它们的CPU开销超过绝对必要。


11889
2018-06-04 19:25


起源



答案:


我不会试图证明这种行为的合理性,而是要解释为什么它实际上是用编写的代码进行的。

简而言之:Python使用的XML解析器, 外籍人士,操作字节,而不是unicode字符。你必须打电话 .encode('utf-16-be') 要么 .encode('utf-16-le') 在传递给它之前在字符串上 ElementTree.fromstring

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 说:

s#(字符串,Unicode或任何读缓冲区兼容对象)[const char   *,int(或Py_ssize_t,见下文)]这个变量在s上存储成两个C变量,第一个是指向字符串的指针,第二个是   一个长度。在这种情况下,Python字符串可能包含嵌入式   空字节。 Unicode对象传回指向默认编码的指针   对象的字符串版本 如果这种转换是可能的。所有   其他读取缓冲区兼容的对象传回对raw的引用   内部数据表示。

我们来看看这个:

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"你很幸运,当Unicode字符串被编码为ASCII时,输入中没有非ASCII字符。如果在解析之前添加以下内容,则UTF-8将按预期使用该示例:

import sys
reload(sys).setdefaultencoding('utf8')

但是,将defaultencoding设置为'utf-16-be'或'utf-16-le'是行不通的,因为ElementTree的Python位确实直接进行字符串比较,这些比较在UTF-16中开始失败。


16
2018-06-06 20:35



感谢您的广泛分析!听起来像ET文档的补丁提交是有序的。 :) - Henrik Heimbuerger
如何使用来自不是字符串的文件? - Ali Karaca


答案:


我不会试图证明这种行为的合理性,而是要解释为什么它实际上是用编写的代码进行的。

简而言之:Python使用的XML解析器, 外籍人士,操作字节,而不是unicode字符。你必须打电话 .encode('utf-16-be') 要么 .encode('utf-16-le') 在传递给它之前在字符串上 ElementTree.fromstring

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 说:

s#(字符串,Unicode或任何读缓冲区兼容对象)[const char   *,int(或Py_ssize_t,见下文)]这个变量在s上存储成两个C变量,第一个是指向字符串的指针,第二个是   一个长度。在这种情况下,Python字符串可能包含嵌入式   空字节。 Unicode对象传回指向默认编码的指针   对象的字符串版本 如果这种转换是可能的。所有   其他读取缓冲区兼容的对象传回对raw的引用   内部数据表示。

我们来看看这个:

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"你很幸运,当Unicode字符串被编码为ASCII时,输入中没有非ASCII字符。如果在解析之前添加以下内容,则UTF-8将按预期使用该示例:

import sys
reload(sys).setdefaultencoding('utf8')

但是,将defaultencoding设置为'utf-16-be'或'utf-16-le'是行不通的,因为ElementTree的Python位确实直接进行字符串比较,这些比较在UTF-16中开始失败。


16
2018-06-06 20:35



感谢您的广泛分析!听起来像ET文档的补丁提交是有序的。 :) - Henrik Heimbuerger
如何使用来自不是字符串的文件? - Ali Karaca