1.11 检查一个字符串是文本还是二进制
任务
在Python中,普通字符串既可以容纳文本,也可以容纳任意的字节,现在需要探知(当然,完全是启发式的试探:对于这个问题并没有什么精准的算法)一个字符串中的数据究竟是文本还是二进制。
解决方案
我们采取Perl的判定方法,如果字符串中包含了空值或者其中有超过30%的字符的高位被置1(意味着该字符的码值大于126)或是奇怪的控制码,我们就认为这段数据是二进制数据。我们得自己编写代码,其优点是对于特殊的程序需求,我们随时可以调整这种启发式的探知方式:
from _ _future_ _ import division # 确保/不会截断
import string
text_characters = "".join(map(chr, range(32, 127))) + "\n\r\t\b"
_null_trans = string.maketrans("", "")
def istext(s, text_characters=text_characters, threshold=0.30):
# 若s包含了空值,它不是文本
if "\0" in s:
return False
# 一个“空”字符串是“文本”(这是一个主观但又很合理的选择)
if not s:
return True
# 获得s的由非文本字符构成的子串
t = s.translate(_null_trans, text_characters)
# 如果不超过30%的字符是非文本字符,s是字符串
return len(t)/len(s) <= threshold
讨论
可以轻易地修改函数istext的启发式探知部分,只需传递一个指定的阀值作为判断某字符串所含数据是“文本”(即正常的ASCII字符加上4个“正常”的控制码,在文本中这几个控制码都是有意义的)的基准,默认的阀值是0.30(30%)。举个例子,如果期望它是采用了iso-8859-1的意大利文本,可以给text_characters参数添加意大利语中的一些重音字母,“àèéìòù”。
很多时候,需要检查的对象是文件,而不是字符串,也就是说要判断文件中的内容是文本还是二进制数据。同样地,我们仍可采用Perl的启发式方法,用前面提供的istext函数来检查文件的第一个数据块:
def istextfile(filename, blocksize=512, **kwds):
return istext(open(filename).read(blocksize), **kwds)
注意,默认情况下,istext函数中的len(t)/len(s)将被截断成0,因为这是一个整数之间的除法结果。以后的版本(估计是Python 3.0,几年后发布),Python中的/操作符的意义会被改变,这样我们在做除法运算的时候就不会发生截断—如果你确实需要截断,可以用截断除法操作符//。
不过,现在Python还没有改变除法的语义,这是为了保证一定的向后兼容性。为了让成千上万行的现有的Python程序和模块平滑地工作于所有的Python 2.x版本,这非常重要。不过,对于语言版本的主版本号的改变,Python允许进行不考虑向后兼容性的改变。
因此,对于本节的解决方案中的模块,按照未来版本中计划的行为模式来改变除法的行为是非常方便的,我们用这种方式来引入模块:
from _ _future_ _ import division
这条语句并不影响程序的其余部分,只影响紧随此声明的模块;通过这个模块,/表现得像“真实的除法”(没有截断)。对于Python 2.3和2.4,dvision可能是唯一需要从 _future _导入的模块。其他的一些未来版本中计划的特性,nested_scope和生成器,现在已经是语言的一部分了,因而无法被关闭—当然明确导入它们没有什么坏处,但只有在你的程序需要能够运行在老版本的Python环境下时,这种做法才有意义。