本节书摘来自异步社区《Python 3程序开发指南(第2版•修订版)》一书中的第2章,第2.4节,作者[英]Mark Summerfield,王弘博,孙传庆 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.4 字符串
字符串是使用固定不变的str数据类型表示的,其中存放Unicode字符序列。str数据类型可以作为函数进行调用,用于创建字符串对象——参数为空时返回一个空字符串,参数为非字符串类型时返回该参数的字符串形式,参数为字符串时返回该字符串的拷贝。str()函数也可以用作一个转换函数,此时要求第一个参数为字符串或可以转换为字符串的其他数据类型,其后跟随至多两个可选的字符串参数,其中一个用于指定要使用的编码格式,另一个用于指定如何处理编码错误。
前面我们注意到,字符串是使用引号创建的,可以使用单引号,也可以使用双引号,但是字符串两端必须相同。此外,我们还可以使用三引号包含的字符串——这是Python对起始端与终端都使用3个引号包含的字符串的叫法,例如:
text = """A triple quoted string like this can include 'quotes' and
"quotes" without formality. We can also escape newlines \
so this particular string is actually only two lines long."""
如果需要在通常的、引号包含的字符串中使用引号,在要使用的引号与包含字符串的引号不同时,可以直接使用该引号,而不需要进行格式化处理操作,但是如果相同,就必须对其进行转义:
a = "Single 'quotes' are fine; \"doubles\" must be escaped."
b = 'Single \'quotes\' must be escaped; "doubles" are fine.'
Python使用换行作为其语句终结符,但是如果在圆括号内、方括号内、花括号内或三引号包含的字符串内则是例外。在三引号包含的字符串中,可以直接使用换行,而不需要进行格式化处理操作。通过使用n转义序列,也可以在任何字符串中包含换行。表2-7展示了所有的Python转义序列。有些情况下——比如,编写正则表达式时,需要创建带有大量字面意义反斜杠的字符串。(正则表达式将在第12章进行讲解。)由于每个反斜杠都必须进行转义处理,从而造成了不便:
import re
phone1 = re.compile("^((?:[(]\\d+[)])?\\s*\\d+(?:-\\d+)?)$")
解决的方法是使用原始的字符串,这种引号包含的或三引号包含的字符串的第一个引号由字面意义的r引导。在这种字符串内部,所有字符都按其字面意义理解,因此不再需要进行转义。下面给出了使用原始字符串的phone正则表达式:
phone2 = re.compile(r"^((?:[()\d+[]])?\s*\d+(?:-\d+)?)$")
如果需要写一个长字符串,跨越了2行或更多行,但是不使用三引号包含的字符串,那么有两种解决方法:
t = "This is not the best way to join two long strings " + \
"together since it relies on ugly newline escaping"
s = ("This is the nice way to join two long strings "
"together; it relies on string literal concatenation.")
注意上面第二种情况,我们必须使用圆括号将其包含在一起,构成一个单独的表达式——如果不使用圆括号,就只有第一个字符串对s进行赋值,第二个字符串则会导致IndentationError异常。Python的“Idioms and Anti-Idioms”HOWTO文档建议总是使用圆括号将跨越多行的任何语句进行封装,而不使用转义的换行符,我们努力遵照这一建议。
由于.py文件默认使用UTF-8 Unicode编码,因此我们可以在字符串字面值中写入任意Unicode字符,而不拘形式。我们也可以使用十六进制转义序列(或使用Unicode名)将任意Unicode字符放置在字符串内,例如:
>>> euros = "€ \N{euro sign} \u20AC \U000020AC"
>>> print(euros)
€ € € €
上面的情况不能使用十六进制转义序列,因为本身限定在两个digits,无法超过0xFF。要注意的是,Unicode字符名非大小写敏感,其中的空格也是可选的。
如果需要知道字符串中某个特定字符的Unicode字元(赋予Unicode编码中某个字符的整数值),那么可以使用内置的ord()函数,例如:
>>> ord(euros[0])
8364
>>> hex(ord(euros[0]))
'0x20ac'
类似地,我们也可以将表示有效字元的任意整数转换为相应的Unicode 字符,这需要使用内置的chr()函数:
>>> s = "anarchists are " + chr(8734) + chr(0x23B7)
>>> s
'anarchists are '
>>> ascii(s)
"'anarchists are \u221e\u23b7'"
如果在IDLE中输入本身,就输出其字符串形式。对于字符串,这意味着字符是包含在引号中输出的。如果只需要ASCII字符,就可以使用内置的ascii()函数,在可能的地方,该函数使用7比特表示形式返回其参数的对应ASCII表示,否则就使用最短的xhh、uhhhh或Uhhhhhhhh进行转义。在本章后面,我们将了解如何实现对字符串输出的精确控制。
2.4.1 比较字符串
字符串支持通常的比较操作符与>=,这些操作符在内存中逐个字节对字符串进行比较。遗憾的是,进行比较时(比如对字符串列表进行排序),存在两个问题,这两个问题都影响到每种使用Unicode字符串的程序设计语言,不是Python特有的问题。
第一个问题是,有些Unicode字符可以使用两种或更多种字节序列表示。例如,字符Å (Unicode字元0x00C5)可以3种不同的方式使用UTF-8编码的字节表示:[0xE2, 0x84, 0xAB]、[0xC3, 0x85]与[0x41, 0xCC, 0x8A]。幸运的是,我们可以解决这一问题。如果我们导入了unicodedata模块,并以“NFKD”(这是使用的标准化方法,代表Normalization Form Compatibility Decomposition")为第一个参数来调用unicodedata.normalize(),则对包含字符Å(使用任意一种有效字符序列表示)的字符串,该函数返回以UTF-8编码字节表示的字符串总是字节序列[0x41, 0xCC, 0x8A]。
第二个问题是,有些字符的排序是特定于某种语言的。一个实例是在瑞典语中,ä排序在z之后,而在德语中,ä的排序与其被拼写为ae时一样。另一个实例是,在英语中,对ø排序时,与其为o一样,在丹麦语与挪威语中,则排序在z之后。这一类问题还有很多,由于同一个应用程序可能会由不同国家的人(因此所认为的排序顺序会不同)使用,使得这一问题变得更加复杂。此外,有时候字符串是不同语言混合组成的(比如,有些是西班牙语,有些是英语),而有些字符(比如箭头、dingbats与数学符号)并不真正具备有意义的排序位置。
作为一种策略(以便防止出错),Python并不进行推测。在字符串比较时,Python使用的是字符串的内存字节表示,此时的排序是基于Unicode 字元的,比如对英语就是按ASCII顺序。对要比较的字符串进行小写或大写,会产生更贴近自然英语的排序。标准化一般很少需要,除非字符串来自外部源(比如文件或网络socket),但即便是这些情况,一般也不必进行标准化,除非确实需要。我们可以自定义Python的排序方法,第3章将进行讲述。关于Unicode排序的所有问题,在Unicode校勘算法文档(unicode.org/reports/tr10)中有详细的解释。
2.4.2 字符串分片与步距
从要素3的讲解中我们知道,序列中的单个数据项或者字符串中的单个字符,可以使用数据项存取操作符[]来提取。实际上,这一操作符功能很丰富,其用途不仅仅局限于提取一个数据项或字符,还可以提取项或字符的整个分片(子序列),在这种情况下该操作符被用作分片操作符。
我们首先从提取单个字符开始。字符串的索引位置从0开始,直至字符串长度值减去1,但是使用负索引位置也是可能的——此时的计数方式是从最后一个字符到第一个字符。给定赋值操作s = "Light ray",图2-1展示了字符串s中所有有效的索引位置。
负索引值出人意料地有用,尤其是-1,这个值总是代表字符串的最后一个字符。存取超过范围的索引位置(或空字符串中的索引位置)会产生IndexError异常。
分片操作符有3种语法格式:
seq[start]
seq[start:end]
seq[start:end:step]
其中,seq可以是任意序列,比如列表、字符串或元组。start、end与step必须都是整数(或存放整数的变量)。我们使用了第一种语法格式:从序列中提取从start开始的数据项。第二种语法从start开始的数据项(包含)到end结束的数据项(不包含)提取一个分片。稍后我们将讨论第三种语法格式。
如果使用第二种语法格式(一个冒号),我们就可以忽略任意的整数索引值。如果忽略了所有起点索引值,就默认为0;如果忽略了终点索引值,就默认为len(seq),这意味着,如果忽略了两个索引值,比如,s[:],则与s[0:len(s)]是等同的,其作用都是提取——也就是复制整个序列。
给定赋值操作s = "The waxwork man",图2-2展示了字符串s的一些实例分片。
在字符串内插入子字符串的一种方法是混合使用带连接的分片,例如:
>>> s = s[:12] + "wo" + s[12:]
>>> s
'The waxwork woman'
实际上,由于文本"wo"在原始字符串中,因此我们也可以写成s[:12] + s[7:9] + s[12:]达到同样的效果。
在涉及很多字符串时,使用+进行连接、使用+=进行追加等操作并不是特别高效,如果需要连接大量的字符串,通常最好使用str.join()方法,下一小节将进行讲解。
第三种分片语法格式(两个冒号)与第二种类似,区别在于不是提取每一个字符,而是每隔step个字符进行提取。与第二种语法类似,也可以忽略两个索引整数。如果忽略了起点索引值,那么默认为0——除非给定的是负的step值,此时起点索引值默认为-1;如果忽略终点索引值,那么默认为len(seq) ——除非给定的是负的step值,此时终点索引值默认为字符串起点前面。不过,不能忽略step,并且step不能为0。如果不需要step,那么应该使用不包含step变量的第二种语法(一个冒号)。
给定赋值操作s = "he ate camel food",图2-3展示了字符串带步距的分片的两个实例。
上面我们使用默认的起点索引值与终点索引值,因此,s[::-2]从该字符串的最后一个字符开始,向该字符串的起点方向,每隔1个字符提取一个字符。类似地,s[::3]从第一个字符开始,向该字符串的终点方向,每隔2个字符提取一个字符。
将分片与步距结合使用也是可能的,如图2-4所示。
更常见的情况下,步距是与字符串之外的序列类型一起使用的,但是也存在用于字符串的情况:
>>> s, s[::-1]
('The waxwork woman', 'namow krowxaw ehT')
step为-1意味着,每个字符都将被提取,方向为从终点到起点——因此会产生反转的字符串。
2.4.3 字符串操作符与方法
由于字符串是固定序列,所有可用于固定序列的功能都可用于字符串,包括使用in进行成员关系测试,使用+=进行追加操作,使用进行复制,使用=进行增强的赋值复制等。在这一小节中,我们将在字符串的上下文中讨论所有这些操作,此外还讨论了很多字符串方法。表2-8、表2-9、表2-10总结了所有的字符串方法,除了两个特别专业的方法(str.maketrans()与str.translate()),这两个方法将在以后简要讨论。
由于字符串是序列,因此也是有大小的对象,我们可以以字符串为参数来使用len()函数,返回值是字符串中的字符数(如果字符串为空,就返回0)。
我们已经知道,在字符串的操作中,+操作符被重载用于实现字符串连接。如果需要连接大量的字符串,使用str.join()方法是一种更好的方案。该方法以一个序列作为参数(比如,字符串列表或字符串元组),并将其连接在一起存放在一个单独的字符串中,并将调用该方法的字符串作为分隔物添加在每两项之间,例如:
>>> treatises = ["Arithmetica", "Conics", "Elements"]
>>> " ".join(treatises)
'Arithmetica Conics Elements'
>>> "-<>-".join(treatises)
'Arithmetica-<>-Conics-<>-Elements'
>>> "".join(treatises)
'ArithmeticaConicsElements'
第一个实例或许是最常见的,连接一个单独的字符,这里是空格。第三个实例纯粹是连接,使用空字符串意味着字符串序列在连接时中间不使用任何填充。
str.join()方法也可以与内置的reversed()函数一起使用,以实现对字符串的反转,比如,"".join(reversed(s))。当然,通过步距也可以更精确地获取同样的结果,比如,s[::-1]。
*操作符提供了字符串复制功能:
>>> s = "=" * 5
>>> print(s)
=====
>>> s *= 10
>>> print(s)
==================================================
如上面实例所展示的,我们也可以使用复制操作符*的增强版进行赋值。
在用于字符串时,如果成员关系操作符in左边的字符串参数是右边字符串参数的一部分,或者相等,就返回True。
如果我们需要在某个字符串中找到另一个字符串所在的位置,有两种方法,一种是使用str.index()方法,该方法返回子字符串的索引位置,或者在失败时产生一个VaueError异常。另一种是使用str.find()方法,该方法返回子字符串的索引位置,或者在失败时返回-1。这两种方法都把要寻找的字符串作为第一个参数,还可以有两个可选的参数,其中第二个参数是待搜索字符串的起始位置,第三个则是其终点位置。
使用哪种搜索方法纯粹是个人爱好与具体场景,尽管如果搜索多个索引位置,使用str.index()方法通常会生成更干净的代码,如下面两个等价的函数所展示的:
两个版本的extract_from_tag()函数的作用是完全一致的。比如,extract_from_tag ("red", "what a rose this is")返回字符串"rose"。左面这一版本的异常处理部分更清晰地布局在其他代码之外,明确地表示了如何处理错误;右边版本的错误返回值则将分散了错误处理的不同情况。
方法str.count()、str.endswith()、str.find()、str.rfind()、str.index()、str.rindex()与str. startswith()都接受至多两个可选的参数:起点位置与终点位置。这里给出两个等价的语句,并假定s是一个字符串:
s.count("m", 6) == s[6:].count("m")
s.count("m", 5, -3) == s[5:-3].count("m")
可以看出,接受起点与终点索引位置作为参数的方法可以运作在由这些索引值指定的字符串分片上。
下面看另一对等价的代码,主要是为了明确str.partition()的作用:
左面的代码段与右面的代码段并不完全等价,因为右面还创建了一个新变量i。注意我们可以直接分配元组,而不拘于形式。两边的代码都是搜索/的最右边出现,如果字符串s为"/usr/local/bin/firefox",那么两个代码段都会产生同样的结果:('/usr/local/bin', '/', 'firefox')。
我们可以以一个单独的字符串为参数来调用str.endswith()(以及str.startswith()),比如,s.startswith("From:"),或者使用字符串元组作为参数。下面的语句同时使用str.endswith()与str.lower()来打印文件名——如果该文件是一个JPEG文件:
if filename.lower().endswith((".jpg", ".jpeg")):
print(filename, "is a JPEG image")
如果作为参数的字符串至少有一个字符,并且字符串中的每个字符都符合标准,那么is*()方法(比如isalpha()与isspace())就返回True,例如:
>>> "917.5".isdigit(), "".isdigit(), "-2".isdigit(), "203".isdigit()
(False, False, False, True)
is*()方法工作的基础是Unicode字符分类,比如,以字符串"N{circled digit two}03"与"203"为参数调用str.isdigit()都会返回True。出于这一原因,我们不能因为isdigit()函数返回True就判断某个字符串可以转换为整数。
从外部源(其他程序、文件、网络连接尤其是交互式用户)接受字符串时,字符串可能包含不需要的开始空白字符与结尾空白字符。我们可以使用str.lstrip()来剥离左边的空白字符,也可以使用str.rstrip()来剥离右边的空白字符,或者使用str.strip()同时剥离两边的空白字符。我们也可以使用一个字符串作为参数来调用剥离方法,这种情况下,每个字符的每个出现都将被从合适的位置剥离,例如:
>>> s = "\t no parking "
>>> s.lstrip(), s.rstrip(), s.strip()
('no parking ', '\t no parking', 'no parking')
>>> "".strip("[](){}<>")
'unbracketed'
我们可以使用str.replace()方法来在字符串内进行替换。这一方法以两个字符串作为参数,并返回该字符串的副本(其中第一个字符串的所有出现都被第二个字符串所替代)。如果第二个字符串为空,那么这一函数的实际效果是删除第一个字符串的所有出现。在csv2html.py程序中,我们将看到str.replace()方法以及其他一些字符串方法的应用实例,该程序在本章后面的实例一节中介绍。
一个频繁遇到的需求是将字符串分割为一系列子字符串。比如,我们有一个文本文件,需要将其中的数据进行处理,要求每行一个记录,每个记录的字段使用星号进行分隔。为此,可以使用str.split()方法,并以待分割的字符串作为第一个参数,以要分割的最大子数据段数为可选的第二个参数。如果再不指定第二个参数,该方法就会进行尽可能多的分割。下面给出一个实例:
>>> record = "Leo Tolstoy*1828-8-28*1910-11-20"
>>> fields = record.split("*")
>>> fields
['Leo Tolstoy', '1828-8-28', '1910-11-20']
以上面的结果为基础,可以使用str.split()方法对出生日期与死亡日期进行进一步的分割,以便计算其寿命(给定或接受一个年份值):
>>> born = fields[1].split("-")
>>> born
['1828', '8', '28']
>>> died = fields[2].split("-")
>>> print("lived about", int(died[0]) - int(born[0]), "years")
lived about 82 years
上面的代码中,我们必须使用int()方法将年份从字符串转换为整数,除此之外,该代码段是很直接的。我们也可以从fields列表中获取年份,比如,year_born = int(fields[1].split("-")[0])。
表2-8、表2-9、表2-10中没有包含两个方法,即str.maketrans()与str.translate()。str.maketrans()方法用于创建字符间映射的转换表,该方法可以接受一个、两个或三个参数,但是我们这里只展示最简单的(两个参数)调用方式,其中,第一个参数是一个字符串,该字符串中的字符需要进行转换,第二个参数也是一个字符串,其中包含的字符是转换的目标,这两个字符串必须具有相同的长度。str.translate()方法以转换表作为一个参数,并返回某个字符串根据该转换表进行转换后的副本。下面展示了如何将可能包含孟加拉数字的字符串转换为英文数字:
table = "".maketrans("\N{bengali digit zero}"
"\N{bengali digit one}\N{bengali digit two}"
"\N{bengali digit three}\N{bengali digit four}"
"\N{bengali digit five}\N{bengali digit six}"
"\N{bengali digit seven}\N{bengali digit eight}"
"\N{bengali digit nine}", "0123456789")
print("20749".translate(table)) # prints: 20749
print("\N{bengali digit two}07\N{bengali digit four}"
"\N{bengali digit nine}".translate(table)) # prints: 20749
从上面可以看出,在str.maketrans()调用内部以及第二个print()调用内部,我们利用了Python的字符串字面值连接,使得字符串跨越了多行,而不需要对换行进行转义或使用显示的连接。
我们对空字符串调用了str.maketrans()方法,因为该方法不关心其针对的具体字符串,而只是对其参数进行处理,并返回一个转换表2。str.maketrans()方法与str.translate()方法也可以用于删除字符,方法是将包含待删除字符的字符串作为第三个参数传递给str.maketrans()。如果需要更复杂的字符转换,我们可以创建一个自定义的codec——要了解关于这一主题的更多信息,可以参阅codecs模块文档。
Python还有一些其他的库模块提供字符串相关的功能。我们已经简要提及了unicodedata模块,下一小节将展示该模块的应用。其他值得关注的模块还有difflib模块,用于展示文件或字符串之间的差别;io模块的io.StringIO类,用于读、写字符串,就像对文件的读写操作一样;textwrap 模块,该模块提供了用于包裹与填充字符串的函数与方法。此外,还有一个string模块,其中定义了一些有用的常量,比如ascii_letters与ascii_lowercase。在第5章中,我们将看到这些模块的一些应用实例。此外,Python的re模块提供了对正则表达式的充分支持——第13章将专注于讲述这一主题。
2.4.4 使用str.format()方法进行字符串格式化
str.format()方法提供了非常灵活而强大的创建字符串的途径。对于简单的情况,使用str.format()方法是容易的,但是如果需要进行复杂的格式化操作,就要学习该方法需要的格式化语法。
str.format()方法会返回一个新字符串,在新字符串中,原字符串的替换字段被适当格式化后的参数所替代,例如:
>>> "The novel '{0}' was published in {1}".format("Hard Times", 1854)
"The novel 'Hard Times' was published in 1854"
每个替换字段都是由包含在花括号中的字段名标识的。如果字段名是简单的整数,就将被作为传递给str.format()方法的一个参数的索引位置。因此,在这种情况下,名为0的字段被第一个参数所替代,名为1的字段则被第二个参数所替代。
如果需要在格式化字符串中包含花括号,就需要将其复写,下面给出一个实例:
>>> "{{{0}}} {1} ;-}}".format("I'm in braces", "I'm not")
"{I'm in braces} I'm not ;-}"
如果我们试图连接字符串与数字,那么Python将产生TypeError异常,但是使用str.format()方法可以很容易地做到这一点:
>>> "{0}{1}".format("The amount due is $", 200)
'The amount due is $200'
我们也可以使用str.format()方法连接字符串(尽管str.join()方法最适合用于这一目的):
>>> x = "three"
>>> s ="{0} {1} {2}"
>>> s = s.format("The", x, "tops")
>>> s
'The three tops'
在上面的实例中,我们使用了一对字符串变量,不过在本小节的大部分,我们在str.format()方法的应用实例中都使用字符串字面值,这就是为了方便——实际上,任何使用字符串字面值的实例中都可以使用字符串变量,方法是完全一样的。
替换字段可以使用下面的任意一种语法格式:
{field_name}
{field_name!conversion}
{field_name:format_specification}
{field_name!conversion:format_specification}
另外需要注意的一点是,替换字段本身也可以包含替换字段,嵌套的替换字段不能有任何格式,其用途主要是格式化规约的计算。在对格式化规约进行更细致的解读时,我们将展示一个实例。现在我们将逐一研究替换字段的每一个组成部分,首先从字段名开始。
2.4.4.1 字段名
字段名或者是一个与某个str.format()方法参数对应的整数,或者是方法的某个关键字参数的名称。我们将在第4章中讨论关键字参数,但实际上用起来并不难,因此,这里我们给出两个实例,以保证本节讲述内容的完整性:
>>> "{who} turned {age} this year".format(who="She", age=88)
'She turned 88 this year'
>>> "The {who} was {0} last week".format(12, who="boy")
'The boy was 12 last week'
上面的第一个实例使用了两个关键字参数,分别是who与age,第二个实例使用了一个位置参数(到这里为止只在这里使用过)与一个关键字参数。要注意的是,在参数列表中,关键字参数总是在位置参数之后,当然,我们可以在格式化字符串内部以任何顺序使用任何参数。
字段名可以引用集合数据类型——比如,列表。在这样的情况下,我们可以包含一个索引(不是一个分片)来标识特定的数据项:
>>> stock = ["paper", "envelopes", "notepads", "pens", "paper clips"]
>>> "We have {0[1]} and {0[2]} in stock".format(stock)
'We have envelopes and notepads in stock'
0引用的是位置参数,因此,{0[1]}是列表stock参数的第二个数据项,{0[2]}是列表stock参数的第三个数据项。
后面我们将学习Python字典,字典中存储的是key–value项,字典对象也可以用于str.format()方法,我们这里展示一个应用实例,如果不能很好地理解,也不必担心,第3章将再次讲述这一主题。
>>> d = dict(animal="elephant", weight=12000)
>>> "The {0[animal]} weighs {0[weight]}kg".format(d)
'The elephant weighs 12000kg'
就像可以使用整数位置索引来存取列表与元组项一样,我们可以使用键值来存取字典项。
我们也可以存取命名的属性。假定已经导入math模块与sys模块,则可以进行如下一些操作:
>>> "math.pi=={0.pi} sys.maxunicode=={1.maxunicode}".format(math, sys)
'math.pi==3.14159265359 sys.maxunicode==65535'
总而言之,通过字段名语法,可以引用传递给str.format()方法的位置参数与关键字参数。如果参数是集合数据类型,比如列表或字典,或参数还包含一些属性,那么可以使用[]或.表示法存取所需的部分,图2-5中勾勒了这一点。
从Python 3.1开始,忽略字段名成为可能,这种情况下,Python会自动进行处理(使用从0开始的数值),比如:
>>> "{} {} {}".format("Python", "can", "count")
'Python can count'
如果我们使用Python 3.0,那么这里使用的格式字符串就必须是"{0} {1} {2}"。在格式化1、2个项目时,使用这种技术是便利的,但对于多个项目的情况,接下来我们看到的技术更便利,并且在Python 3.0环境下可以使用。
在结束对字符串格式字段名的讨论之前,提及另一种为格式化字符串赋值的相当不同的途径是有价值的,这涉及一种高级技术,但尽快学会是有用的,因为这种技术非常便利。
当前还在作用范围内的局部变量可以通过内置的locals()函数访问,该函数会返回一个字典,字典的键是局部变量名,字典的值则是对变量值的引用。现在,我们可以使用映射拆分将该字典提供给str.format()方法,映射拆分操作符为**,可应用于映射(比如字典)来产生一个适合于传递给函数的键-值列表,比如:
>>> element = "Silver"
>>> number = 47
>>> "Element {number} is {element}".format(**locals())
'Element 47 is Silver'
这种语法可能非常怪异——Perl程序员倒是会感觉很亲切,不过不用担心,第4章会进行解释。现在我们需要知道的就是我们可以在格式化字符串中使用变量名,Python会通过拆分字典(locals()返回的字典,或其他字典)来将变量值填充到str.format()方法。比如,我们可以重写早前看到的“elephant”实例,以便其具备更好的格式(带有更简单的字段名)。
>>> "The {animal} weighs {weight}kg".format(**d)
'The elephant weighs 12000kg'
将字典拆分并提供给str.format()方法时,允许使用字典的键作为字段名。这使得字符串格式更易于理解,也易于维护,因为不需要依赖于参数的顺序。然而,要注意的是,如果需要将不止一个参数传递给 str.format(),那么只有最后一个参数才可以使用映射拆分。
2.4.4.2 转换
在讨论decimal.Decimal数字时,我们注意到,这些数可以以两种方式输出,例如:
>>> decimal.Decimal("3.4084")
Decimal('3.4084')
>>> print(decimal.Decimal("3.4084"))
3.4084
decimal.Decimal的第一种展示方式是其表象形式,这种形式的用途是提供一个字符串——该字符串被Python解释时将重建其表示的对象。Python程序可以评价Python代码段或整个程序,因此,这种表象形式有时候是有用的。不是所有对象都可以提供这种便于重建的表象形式,如果提供,其形式为包含在尖括号中的字符串,比如,sys模块的表象形式为字符串""。
第二种是以字符串形式对decimal.Decimal进行展示的,这种形式的目标是便于阅读,因此其着眼点是展示一些读者感兴趣的东西。如果某种数据类型没有字符串表示形式,但又需要使用字符串进行表示,那么Python将使用表象形式。
Python内置的数据类型都知道str.format()方法,在作为参数传递给这一方法时,将返回一个适当的字符串来展示自己。第6章我们将看到,为自定义数据类型添加对str.format()方法的支持是很直接的。此外,重写数据类型的通常行为并强制其提供字符串形式或表象形式也是可能的,这是通过向字段中添加conversion指定符实现的。目前,有3个这样的指定符:s,用于强制使用字符串形式;r,用于强制使用表象形式;a,用于强制使用表象形式,但仅限于ASCII字符。下面给出一个实例:
>>> "{0} {0!s} {0!r} {0!a}".format(decimal.Decimal("93.4"))
"93.4 93.4 Decimal('93.4') Decimal('93.4')"
在上面的实例中,decimal.Decimal的字符串形式产生的字符串与提供给str.format()(通常情况)的字符串是相同的。同时,在这个比较特定的实例中,由于都只使用ASCII字符,因此,表象形式与ASCII表象形式之间没有区别。
下面给出另一个实例,这次使用的字符串中包含了电影名,存放在变量movie中。如果使用"{0}".format(movie)打印该字符串,那么该字符串将原样输出,但是如果需要阻止非ASCII字符的输出,就可以使用ascii(movie)或"{0!a}".format (movie),这两种方法都将生成字符串'u7ffbu8a33u3067u5931u308fu308cu308b'。
到这里,我们讲述了如何将变量值放置在格式化字符串中,以及如何强制使用字符串形式或表象形式。现在,我们开始考虑值本身的格式问题。
2.4.4.3 格式规约
整数、浮点数以及字符串的默认格式通常都足以满足要求,但是如果需要实施更精确的控制,我们就可以通过格式规约很容易地实现。为了更易于掌握相关的详细信息,我们分别讲述格式化字符串、整数与浮点数,不过图2-6先给出了包括所有这些对象的通常语法格式。
对于字符串而言,我们可以控制的包括填充字符、字段内对齐方式以及字段宽度的最小值与最大值。
字符串格式规约是使用冒号(:)引入的,其后跟随可选的字符对——一个填充字符(可以不是))与一个对齐字符(用于右对齐),之后跟随的是可选的最小宽度(整数),如果需要指定最大宽度,就在其后使用句点,句点后跟随一个整数值。
要注意的是,如果我们指定了一个填充字符,就必须同时指定对齐字符。我们忽略了格式规约的符号与类型部分,因为对字符串没有实际影响。只使用一个冒号而没有任何其他可选的元素是无害的,但也是无用的。
下面看一些实例:
>>> s = "The sword of truth"
>>> "{0}".format(s) # default formatting
'The sword of truth'
>>> "{0:25}".format(s) # minimum width 25
'The sword of truth '
>>> "{0:>25}".format(s) # right align, minimum width 25
' The sword of truth'
>>> "{0:^25}".format(s) # center align, minimum width 25
' The sword of truth '
>>> "{0:-^25}".format(s) # - fill, center align, minimum width 25
'---The sword of truth----'
>>> "{0:.<25}".format(s) # . fill, left align, minimum width 25
'The sword of truth.......'
>>> "{0:.10}".format(s) # maximum width 10
'The sword '
在倒数第二个实例中,我们必须指定左对齐(即便这是默认的)。如果漏掉了
前面我们已经注意到,在格式化规约内部包括替换字段是有可能的,从而有可计算的格式也是可能的。比如,这里给出了使用maxwidth变量设置字符串最大宽度的两种方式:
>>> maxwidth = 12
>>> "{0}".format(s[:maxwidth])
'The sword of'
>>> "{0:.{1}}".format(s, maxwidth)
'The sword of'
第一种方法使用标准的字符串分片,第二种方法使用内部替换字段。
对于整数,通过格式规约,可以控制填充字符、字段内对齐、符号、最小字段宽度、基数等。
整数格式规约以冒号开始,其后可以跟随一个可选的字符对——一个填充字符(可以不是))与一个对齐字符(用于右对齐,=用于在符号与数字之间进行填充),之后跟随的是可选的符号字符:+表示必须输出符号,-表示只输出负数符号,空格表示为正数输出空格;为负数输出符号-。再之后跟随的是可选的最小宽度整数值——其前可以使用字符#引导,以便获取某种基数进制为前缀的输出(对二进制、八进制、十六进制数值),也可以以0引导,以便在对齐时使用0进行填充。如果希望输出其他进制数据,而非十进制数,就必须添加一个类型字符——b用于表示二进制,o用于表示八进制,x用于表示小写十六进制,X用于表示大写十六进制,为了完整性,也可以使用d表示十进制整数。此外,还有两个其他类型字符:c,表示输出整数对应的Unicode字符;n,表示以场所敏感的方式输出数字。
我们可以以两种不同的方式用0进行填充:
>>> "{0:0=12}".format(8749203) # 0 fill, minimum width 12
'000008749203'
>>> "{0:0=12}".format(-8749203) # 0 fill, minimum width 12
'-00008749203'
>>> "{0:012}".format(8749203) # 0-pad and minimum width 12
'000008749203'
>>> "{0:012}".format(-8749203) # 0-pad and minimum width 12
'-00008749203'
前两个实例使用的填充字符为0,填充位置在符号与数字本身之间(=);后两个实例要求最小宽度为12,并使用0进行填充。
下面给出了一些对齐实例:
>>> "{0:*<15}".format(18340427) # * fill, left align, min width 15
'18340427*******'
>>> "{0:*>15}".format(18340427) # * fill, right align, min width 15
'*******18340427'
>>> "{0:*^15}".format(18340427) # * fill, center align, min width 15
'***18340427****'
>>> "{0:*^15}".format(-18340427) # * fill, center align, min width 15
'***-18340427***'
下面给出一些展示符号字符作用的实例:
>>> "[{0: }] [{1: }]".format(539802, -539802) # space or - sign
'[ 539802] [-539802]'
>>> "[{0:+}] [{1:+}]".format(539802, -539802) # force sign
'[+539802] [-539802]'
>>> "[{0:-}] [{1:-}]".format(539802, -539802) # - sign if needed
'[539802] [-539802]'
下面是两个使用某些类型字符的实例:
>>> "{0:b} {0:o} {0:x} {0:X}".format(14613198)
'110111101111101011001110 67575316 deface DEFACE'
>>> "{0:#b} {0:#o} {0:#x} {0:#X}".format(14613198)
'0b110111101111101011001110 0o67575316 0xdeface 0XDEFACE'
为整数指定最大字段宽度是不可能的,这是因为,这样做要求数字是可裁剪的,并可能会使整数没有意义。
如果我们使用Python 3.1,并在格式规范中使用一个逗号,则整数将使用逗号进行分组。例如:
>>> "{0:,} {0:*>13,}".format(int(2.39432185e6))
'2,394,321 ****2,394,321'
最后一个可用于整数(也可用于浮点数)的格式化字符是n。在给定的字符是整数时,其作用与d相同;在给定的字符是浮点数时,其作用与g相同。n的特殊之处在于,充分考虑了当前的场所,并在其产生的输出信息中使用场所特定的十进制字符与分组字符。默认的场所称为C场所,对这种C场所,十进制字符是一个句点,分组字符是一个空字符串。在程序的起始处添加下面两行,并将其作为最先执行的语句,通过这种方式,可以充分考虑不同用户的场所:3
import locale
locale.setlocale(locale.LC_ALL, "")
如果将空字符串作为场所传递,那么Python会尝试自动确定用户的场所(比如,通过检查LANG环境变量),并以C场所为默认场所。下面给出一些实例,展示了对整数与浮点数使用不同场所的影响:
x, y = (1234567890, 1234.56)
locale.setlocale(locale.LC_ALL, "C")
c = "{0:n} {1:n}".format(x, y) # c == "1234567890 1234.56"
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
en = "{0:n} {1:n}".format(x, y) # en == "1,234,567,890 1,234.56"
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
de = "{0:n} {1:n}".format(x, y) # de == "1.234.567.890 1.234,56"
虽然n对于整数非常有用,但是对于浮点数的用途有限,因为随着浮点数的增大,就会使用指数形式对其进行输出。
对于浮点数,通过格式规约,可以控制填充字符、字段对齐、符号、最小字段宽度、十进制小数点后的数字个数,以及是以标准形式、指数形式还是以百分数的形式输出数字。
用于浮点数的格式规约与用于整数的格式规约是一样的,只是在结尾处有两个差别。在可选的最小宽度后面,通过写一个句点并在其后跟随一个整数,我们可以指定在小数点后跟随的数字个数。我们也可以在结尾处添加一个类型字符:e表示使用小写字母e的指数形式,E表示使用大写字母E的指数形式,f表示标准的浮点形式,g表示“通常”格式——这与f的作用是相同的,除非数字特别大(在这种情况下与e的作用相同——以及几乎与g等同的G,但总是使用f或E)。另一个可以使用的是%——这会导致数字扩大100倍,产生的数字结果使用f并附加一个%字符的格式输出。
下面给出几个实例,展示了指数形式与标准形式:
>>> amount = (10 ** 3) * math.pi
>>> "[{0:12.2e}] [{0:12.2f}]".format(amount)
'[ 3.14e+03] [ 3141.59]'
>>> "[{0:*>12.2e}] [{0:*>12.2f}]".format(amount)
'[****3.14e+03] [*****3141.59]'
>>> "[{0:*>+12.2e}] [{0:*>+12.2f}]".format(amount)
'[***+3.14e+03] [****+3141.59]'
第一个实例中最小宽度为12个字符,在十进制小数点之后有2个数字。第二个实例构建在第一个实例之上,添加了一个填充字符*,由于使用了填充字符就必须同时也使用对齐字符,因此指定了右对齐方式(虽然对数字而言这是默认的)。第三个实例构建在前两个实例之上,添加了符号操作符+,以便在输出中使用符号。
在Python 3.0中,decimal.Decimal数值被str.format()当做字符串,而不是数值。这需要一定的技巧来格式化其输出,从Python 3.1开始,decimal.Decimal数值能够被格式化为floats,也能对逗号(,)提供支持,以获得用逗号进行隔离的组。在下面这个例子中,由于在Python 3.1中不再需要字段名,所以这里将其删除。
>>> "{:,.6f}".format(decimal.Decimal("1234567890.1234567890"))
'1,234,567,890.123457'
如果我们省略格式字符f(或使用格式字符g),则数值将被格式化为'123457E+9'。
Python 3.0并不直接支持复数的格式化,从Python 3.1开始,才对其提供支持。不过,我们可以很容易地解决这一问题,这是通过将复数的实数部分与虚数部分分别作为单独的浮点数进行格式化来实现的,比如:
>>> "{0.real:.3f}{0.imag:+.3f}j".format(4.75917+1.2042j)
'4.759+1.204j'
>>> "{0.real:.3f}{0.imag:+.3f}j".format(4.75917-1.2042j)
'4.759-1.204j'
在上面的实例中,我们分别存取复数每一部分的属性,并都将其格式化为浮点数,在小数点后面带有3个数字。我们还强制对虚数部分输出符号,并添加字母j。
Python 3.1用来格式化复数的语法与用于floats的语法相同:
>>> "{:,.4f}".format(3.59284e6-8.984327843e6j)
'3,592,840.0000-8,984,327.8430j'
该方法有一个轻微的缺点,即复数的实部和虚部使用的格式化方法完全相同。但是,如果我们想分别格式化复数的实部和虚部时,则总是可以使用Python 3.0技术来分别访问复数的属性。
2.4.4.4 实例:print_unicode.py
在前面的小节中,我们详细地研究了str.format()方法的格式规约,并给出了很多展示特别之处的代码snippets。在这里,我们给出一个虽然小但很有用的实例,该实例使用了str.format()方法,因此我们可以在真实的上下文中查看相关的格式规约。该实例还使用了我们在前面几节中讲述的其他字符串方法,并引入了一个来自unicod- edata模块的函数4。
该程序只包含25行可执行代码,导入了两个模块(sys与unicodedata),并定义了一个自定义函数print_unicode_table()。我们首先看一个运行实例,以便了解该程序的行为,之后我们将查看程序末尾处实际的处理代码,最后将查看自定义函数。
如果不带参数运行,那么该程序会生成一个表格,其中包含每个Unicode字符,从空格字符开始,直到带有最高可用字元的字符。如果给定了一个参数,比如实例中所展示的,那么只打印表格中那些小写的Unicode字符名包含该参数的列。
word = None
if len(sys.argv) > 1:
if sys.argv[1] in ("-h", "--help"):
print("usage: {0} [string]".format(sys.argv[0]))
word = 0
else:
word = sys.argv[1].lower()
if word != 0:
print_unicode_table(word)
在完成导入与print_unicode_table()函数的创建后,程序执行到了上面所展示的代码。这里假定用户尚未在命令行中给定一个用于匹配的字。如果给定了一个命令行参数,并且是-h或--help,就打印出该程序的使用帮助信息,并将word设置为0,以作为程序结束的指示标记;否则,将word设置为用户输入的参数的小写版。如果word不为0,就打印表格。
在打印使用帮助信息时,我们使用的格式规约只包括格式名——这里是参数的位置编号。我们也可以写成如下的形式:
print("usage: {0[0]} [string]".format(sys.argv))
使用上面的方法时,第一个0代表我们要使用的参数的索引位置,[0]代表该参数内的索引位置——这种写法是有效的,因为sys.argv是一个列表。
def print_unicode_table(word):
print("decimal hex chr {0:^40}".format("name"))
print("------- ----- --- {0:-<40}".format(""))
code = ord(" ")
end = sys.maxunicode
while code < end:
c = chr(code)
name = unicodedata.name(c, "*** unknown ***")
if word is None or word in name.lower():
print("{0:7} {0:5X} {0:^3c} {1}".format(
code, name.title()))
code += 1
为了保证代码的清晰,我们使用了两个空行。该函数的suite的头两行用于打印标题行。第一个str.format()用于在40个字符宽的字段中间位置打印“name”,第二个用于在40个字符宽的字段中间位置打印空白字符串,使用的填充字符为-,对齐方式为左对齐。(注意,如果指定了填充字符,就必须也指定对齐方式。)对于第二行,实际上也可以使用如下的替代方法完成:
print("------- ----- --- {0}".format("-" * 40))
这里使用了字符串赋值操作符*来创建合适的字符串,并简单地将其插入到格式化字符串中。第三种替代方法则只是简单地输入40个字符“-”,并使用字符串字面值。
我们在code变量中保持对Unicode字元的追踪,最初将其初始化为用于空格(0x20)的字元,并将变量end设置为最高的可用Unicode字元——这里的设置是可以变化的,依赖于Python是否使用UCS-2或UCS-4字符编码。
在while循环内部,我们使用chr()函数获取与字元对应的Unicode字符。unicodedata.name()函数用于返回给定的Unicode字符的名称,如果没有定义字符名,那么该函数的第二个参数(可选的)是要使用的名称。
如果用户没有指定一个字(word为None),或指定了并且是Unicode字符名的小写版,就打印对应的列。
虽然只将code变量传递给str.format()方法一次,但是在格式化字符串中,该变量实际上使用了三次,第一次是将code作为一个整数在7字符宽的字段内打印(填充字符默认为空格,因此不需要明确指定),第二次是将code作为大写的十六进制数在5字符宽的字段内打印,第三次是打印与code对应的Unicode字符——使用c格式指定符,并在宽度最小值为3个字符的字段中间。注意,在第一种格式规约中,并不是必须要指定类型d,这是因为对整数参数这是默认的。第二个参数是字符的Unicode字符名,使用首字母大写的方式打印,也就是说,每个字的首字母大写,所有其他字符都小写。
我们熟悉了功能非常丰富的str.format()方法,在本书中,我们将对其进行充分的介绍和使用。
2.4.5 字符编码
本质上说,计算机只能存储字节,即8比特的值,如果是无符号数,那么取值范围从0x00到0xFF,每个字符必须都以某种形式的字节表示。在计算机技术的早期,研究者们设计的编码机制是使用一个特定字节表示某个特定的字符。例如,使用ASCII编码,就用0x41表示A,用0x42表示B,依此类推。在西欧,通常使用的是Latin-1编码,其前127个字符与7比特ASCII相同,其余部分则用于重音字符与欧洲人需要的其他符号。在计算机科学的发展中,研究者还设计了很多种其他编码方式,其中的大部分仍然在使用中。
遗憾的是,存在太多的编码方式会带来很多不便,在编写国际化软件时更是如此。一个几乎被最广泛采纳的标准是Unicode编码,在这种编码中,Unicode为每个字符分配一个整数,即字元,就像早期的编码方式一样,但是Unicode不局限于使用一个字节表示每个字符,因而有能力使用这种编码方式表示每种语言中的每个字符。并且,作为对其表示能力的一种增强,其中的前127个Unicode字符与7比特ASCII表示的前127个字符是相同的。
Unicode是如何存储的?当前,定义了超过100万个Unicode字符,因此,即便使用有符号数字,一个32位整数也足以存放任何Unicode 字元,因此,最简单的用于存储Unicode字符的方式是使用32位整数序列,每个整数代表一个字符。在内存中,这是非常便利的,因为我们可以设计一个32位整数数组,数组中的每个元素与某个字符有一对一的对应关系。但对文件或通过网络连接发送的文本而言,尤其是在文本几乎都是7比特ASCII时,每个整数的4个字节中最多有3个字节会是0x00。为避免这样的浪费,Unicode自身有几种表示方式。
在内存中,Unicode通常以UCS-2格式(实质上是16比特的无符号整数)表示前65535个字元,或者以USC-4格式(32位整数)表示所有的字元——本书写作时,共有1114111个。在Python编译时,会设置为使用某一种格式(如果sys.maxunicode为65535,Python编译时就使用UCS-2)。
对存放在文件中或通过网络连接传送的数据,情况会更加复杂。如果使用了Unicode,那么字元可以使用UTF-8进行编码——这种编码中,对前127个字元,每个字符使用一个字节表示;对其他字元,则使用两个或更多的字节数来表示每个字符。对英文文本而言,UTF-8是非常紧凑的,如果只使用了7比特字符,则UTF-8文件与ASCII文件实质上是一样的。另一种常见的编码方式是UTF-16编码,这种编码方式中,对大多数字符使用两个字节表示,对其他的一些字符则使用4个字节表示。对某些亚洲语言,这种编码方式比UTF-8更紧凑,但与UTF-8不同的是,UTF-16文本应该以一个字节顺序标记开始,以便用于读取该文本的代码可以判定字节对是big-endian还是little-endian。此外,所有旧的编码格式,比如GB2312、ISO-8859-5、Latin-1等,实际上都在常规的使用中。
str.encode()方法可以返回一个字节序列——实际上是一个bytes对象,在第7章中将进行讲述——编码时根据我们提供的编码参数进行编码。使用这一方法,可以更好地理解不同编码格式之间的差别,以及为什么进行错误的编码假设或导致错误。
>>> artist = "Tage Åsén"
>>> artist.encode("Latin1")
b'Tage \xc5s\xe9n'
>>> artist.encode("CP850")
b'Tage \x8fs\x82n'
>>> artist.encode("utf8")
b'Tage \xc3\x85s\xc3\xa9n'
>>> artist.encode("utf16")
b'\xff\xfeT\x00a\x00g\x00e\x00 \x00\xc5\x00s\x00\xe9\x00n\x00'
在引号之前使用一个字母b,表示使用的是字节字面值,而非字符串字面值。作为一种便利,在创建字节字面值时,我们可以混合使用可打印的ASCII字符与十六进制转义字符。
我们不能使用ASCII编码方式对Tage Åsén的名称进行编码,因为这种编码不包括Å字符或任意重音字符,因此,这样做会导致产生一个UnicodeEncodeError异常。Latin-1编码(即ISO-8859-1)是一种8比特的编码格式,其中包含了这一名称所需要的所有字符。另一方面,Ern B´nk这一artist不够幸运,因为字符不是一个Latin-1字符,不能成功进行编码。当然,这两个名称都可以使用Unicode编码格式进行编码。要注意的是,对UTF-16而言,头两个字节表示的字节顺序标记——解码函数会根据这一标记确定数据是big-endian还是little-endian,以便分别进行相应的处理。
对str.encode()方法,还有两点值得注意。第一个参数(编码名称)是大小写不敏感的,连字符与下划线在其中是等同对待的,因此,“us-ascii”与“US_ASCII”被认为是相同的。还有很多别名,比如,“latin”、“latin1”、“latin_1”、“ISO-8859-1”、“CP819”,其他一些都是“Latin-1”。该方法也可以接受可选的第二个参数,其作用是指定错误处理方式。例如,如果第二个参数为“ignore”或“replace”,那么可以将任何字符串编码为ASCII格式——当然,这会导致数据丢失——也可以无丢失的情况,如果我们使用“backslashreplace”,就会使用x、u与U等转义字符替换非ASCII字符。例如, artist.encode("ascii", "ignore")会产生b'Tage sn',artist.encode("ascii", "replace")会产生b'Tage ?s?n',而artist.encode("ascii", "backslashreplace")会产生b'Tage xc5sxe9n'。(我们也可以使用"{0!a}".format(artist)得到一个ASCII字符串'Tage xc5sxe9n'。)
str.encode()方法的complement是bytes.decode()(以及bytearray.decode()),该方法将返回一个字符串,其中使用给定的编码格式对字节进行解码,例如:
>>> print(b"Tage \xc3\x85s\xc3\xa9n".decode("utf8"))
Tage Åsén
>>> print(b"Tage \xc5s\xe9n".decode("latin1"))
Tage Åsén
8比特Latin-1、CP850(一种IBM PC编码格式)以及UTF-8编码之间的细弱差别使得猜测编码格式不太可行,幸运的是,UTF-8正在成为明文文本文件编码格式的事实标准,因此,后人只需要熟悉这一格式,甚至不需要知道曾经存在过其他编码格式。
Python的.py文件使用UTF-8编码,因此,Python总是知道字符串字面值要使用的编码格式。这意味着,我们可以在字符串中输入任意的Unicode字符——只要使用的编辑器支持5。
在从外部源(比如socket)读取数据时,Python无法知道其使用的编码格式,因此会返回字节序列,并由程序员对其进行相应的解码。对文本文件,Python采用一种更软化的方法,即使用本地编码——除非明确指定编码格式。
幸运的是,有些文件格式会指定其编码格式。比如,我们可以假定XML文件使用的是UTF-8编码,除非<?xml ?>指令明确地指定了不同的编码格式。因此,阅读XML时,我们可以提取比如前1000个字节,寻找其中的编码规约,如果找到,就使用指定的编码格式对文件进行解码,否则使用默认的UTF-8编码。对使用Python所支持的单字节编码的任意XML文件或明文文本文件,除基于EBCDIC的编码(CP424、CP500)与一些其他编码(CP037、CP864、CP865、CP1026、CP1140、HZ、SHIFT-JIS-2004、SHIFT-JISX0213)之外,这一方法应该都可以正常工作。遗憾的是,对多字节编码(比如UTF-16与UTF-32),这一方法不能有效工作。在Python Package Index,pypi.python.org/pypi中,至少有两个Python包可用于检测文件的编码格式。