Python UTF-8 XML解析(SUDS):去除“无效标记”
在处理UTF-8时,有一个常见的错误,就是出现“无效的字符”。
在我的例子中,这个问题来自于一个SOAP服务提供商,他们对unicode字符不太尊重,直接把值截断到100个字节,完全忽视了第100个字节可能正好处于一个多字节字符的中间。例如:
<name xsi:type="xsd:string">浙江家庭教会五十人遭驱散及抓打 圣诞节聚会被断电及抢走物品(图、视频\xef\xbc</name>
最后两个字节是一个3字节unicode字符的残余部分,因为截断的时候假设大家都在用1字节的字符。接下来,使用sax解析器时:
xml.sax._exceptions.SAXParseException: <unknown>:1:2392: not well-formed (invalid token)
我不再关心这个字符了。它应该从文档中删除,以便让sax解析器正常工作。
这个XML的回复在其他方面都是有效的,除了这些值。
问题是:如何在不解析整个文档和不重新发明UTF-8编码来检查每个字节的情况下,去掉这个字符呢?
使用的工具是:Python+SUDS
2 个回答
@FlipMcF 的回答是正确的 - 我只是想分享我对他解决方案的过滤器,因为原来的方法对我来说不太管用(我在我的 XML 文件中有一些表情符号字符,它们虽然在 UTF-8 编码下是正确的,但还是导致了 XML 解析器崩溃):
class UnicodeFilter(MessagePlugin):
def received(self, context):
from lxml import etree
from StringIO import StringIO
parser = etree.XMLParser(recover=True) # recover=True is important here
doc = etree.parse(StringIO(context.reply), parser)
context.reply = etree.tostring(doc)
结果发现,SUDS把xml当作'字符串'(而不是unicode)来看,所以这些都是编码过的值。
1) 过滤器:
badXML = "your bad utf-8 xml here" #(type <str>)
#Turn it into a python unicode string - ignore errors, kick out bad unicode
decoded = badXML.decode('utf-8', errors='ignore') #(type <unicode>)
#turn it back into a string, using utf-8 encoding.
goodXML = decoded.encode('utf-8') #(type <str>)
2) SUDS: 见 https://fedorahosted.org/suds/wiki/Documentation#MessagePlugin
from suds.plugin import MessagePlugin
class UnicodeFilter(MessagePlugin):
def received(self, context):
decoded = context.reply.decode('utf-8', errors='ignore')
reencoded = decoded.encode('utf-8')
context.reply = reencoded
还有
from suds.client import Client
client = Client(WSDL_url, plugins=[UnicodeFilter()])
希望这能帮到某个人。
注意:感谢 John Machin!
关于 errors='ignore'
的 issue8271 可能会在这里给你带来麻烦。如果这个bug没有修复,使用'ignore'时会消耗掉接下来的几个字节,以满足长度要求。
在解码一个无效的UTF-8字节序列时,只有
开始字节和后续字节被视为无效,而不是开始字节所指定的字节数。
这个问题在以下版本中修复:
Python 2.6.6 rc1
Python 2.7.1 rc1 (以及所有未来的2.7版本)
Python 3.1.3 rc1 (以及所有未来的3.x版本)
Python 2.5及以下版本会有这个问题。
在上面的例子中,"\xef\xbc</name".decode('utf-8', errors='ignore')
应该返回 "</name"
,但在有bug的python版本中,它返回 "/name"
。
前四位(0xe
)描述了一个3字节的UTF字符,所以字节0xef
、0xbc
,然后(错误地)0x3c
('<'
)被消耗掉了。
0x3c
不是一个有效的后续字节,这本身就造成了无效的3字节UTF字符。
修复后的python版本只会移除第一个字节和仅有效的后续字节,留下 0x3c
不被消耗。