在Python中过滤特定字节

22 投票
4 回答
21056 浏览
提问于 2025-04-17 09:33

我在我的Python程序中遇到了这个错误:ValueError: 所有字符串必须与XML兼容:Unicode或ASCII,不允许有NULL字节或控制字符

这个问题,从/dev/random获取随机文本时在lxml中引发错误:所有字符串必须与XML兼容:Unicode或ASCII,不允许有NULL字节,解释了这个问题。

解决办法是过滤掉某些字节,但我对怎么做这件事感到困惑。

有人能帮忙吗?

编辑:抱歉,如果我没有提供足够的信息。这个字符串数据来自一个外部API查询,我无法控制数据的格式。

4 个回答

4

我觉得这个方法有点过于严格,而且似乎运行得很慢,但我的程序还是很快。在努力理解问题出在哪里的时候(即使我尝试过@John的cleaned_string实现),我最终还是调整了他的答案,用下面的代码去除不可打印的ASCII字符(Python 2.7):

from curses import ascii
def clean(text):
    return str(''.join(
            ascii.isprint(c) and c or '?' for c in text
            )) 

我不太确定我在更好的选项上做错了什么,但我只是想继续往前走……

19

另一种比上面提到的方法快得多的方式是使用正则表达式,像这样:

re.sub(u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', text)

根据我的测试,这种方法比上面的快了超过10倍:

import timeit

func_test = """
def valid_xml_char_ordinal(c):
    codepoint = ord(c)
    # conditions ordered by presumed frequency
    return (
        0x20 <= codepoint <= 0xD7FF or
        codepoint in (0x9, 0xA, 0xD) or
        0xE000 <= codepoint <= 0xFFFD or
        0x10000 <= codepoint <= 0x10FFFF
    );
''.join(c for c in r.content if valid_xml_char_ordinal(c))
"""

func_setup = """
import requests; 
r = requests.get("https://stackoverflow.com/questions/8733233/")
"""

regex_test = """re.sub(u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', r.content)"""
regex_setup = """
import requests, re; 
r = requests.get("https://stackoverflow.com/questions/8733233/")
"""

func_test = timeit.Timer(func_test, setup=func_setup)
regex_test = timeit.Timer(regex_test, setup=regex_setup)

print func_test.timeit(100)
print regex_test.timeit(100)

输出结果:

> 2.63773989677
> 0.221401929855

简单来说,我们的做法是先下载一次这个网页(就是你现在正在阅读的页面),然后对它的内容分别用功能性方法和正则表达式方法各运行100次。

使用功能性方法大约需要2.6秒。
使用正则表达式方法大约只需要0.2秒。


更新:在评论中提到,这个答案中的正则表达式之前删除了一些在XML中应该允许的字符。这些字符包括在补充多语言平面中的任何内容,包括古代文字,比如楔形文字、象形文字,还有(奇怪的是)表情符号。

现在的正则表达式是正确的。将来快速测试这个的方法是使用re.DEBUG,它会打印出:

In [52]: re.compile(u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', re.DEBUG)
max_repeat 1 4294967295
  in
    negate None
    range (32, 55295)
    literal 9
    literal 10
    literal 13
    range (57344, 65533)
    range (65536, 1114111)
Out[52]: re.compile(ur'[^ -\ud7ff\t\n\r\ue000-\ufffd\U00010000-\U0010ffff]+', re.DEBUG)

对于这个错误我深感抱歉。我只能说我在别处找到了这个答案,然后把它放在这里。这个错误是别人的,但我传播了它。对于因此受到影响的任何人,我深表歉意。

更新2,2017-12-12:我从一些OSX用户那里了解到,这段代码在所谓的窄版本Python上无法工作,而OSX有时会出现这种情况。你可以通过运行import sys; sys.maxunicode来检查。如果打印出65535,那么这里的代码在你安装“宽版本”之前是无法工作的。在这里了解更多信息。

31

根据链接问题的回答,XML标准定义了什么是有效字符:

Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]

把这个转换成Python代码:

def valid_xml_char_ordinal(c):
    codepoint = ord(c)
    # conditions ordered by presumed frequency
    return (
        0x20 <= codepoint <= 0xD7FF or
        codepoint in (0x9, 0xA, 0xD) or
        0xE000 <= codepoint <= 0xFFFD or
        0x10000 <= codepoint <= 0x10FFFF
        )

你可以根据需要使用这个函数,比如:

cleaned_string = ''.join(c for c in input_string if valid_xml_char_ordinal(c))

撰写回答