第二章: 字符串和文本¶
2.1 使用多个界定符分割字符串¶
re.split()
当你使用 re.split() 函数时候,需要特别注意的是正则表达式中是否包含一个括号捕获分组。 如果使用了捕获分组,那么被匹配的文本也将出现在结果列表中.
如果你不想保留分割字符串到结果列表中去,但仍然需要使用到括号来分组正则表达式的话, 确保你的分组是非捕获分组,形如 (?:...) 。
2.2 字符串开头或结尾匹配¶
str.startswith() 或者是 str.endswith()
如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去, 然后传给 startswith() 或者 endswith() 方法:
>>> import os
>>> filenames = os.listdir('.')
>>> filenames
[ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
>>> [name for name in filenames if name.endswith(('.c', '.h')) ]
['foo.c', 'spam.c', 'spam.h'
>>> any(name.endswith('.py') for name in filenames)
True
>>>
你可以能还想使用正则表达式去实现,比如:
>>> import re
>>> url = 'http://www.python.org'
>>> re.match('http:|https:|ftp:', url)
<_sre.SRE_Match object at 0x101253098>
>>>
这种方式也行得通,但是对于简单的匹配实在是有点小材大用了,本节中的方法更加简单并且运行会更快些。
最后提一下,当和其他操作比如普通数据聚合相结合的时候 startswith() 和 endswith() 方法是很不错的。 比如,下面这个语句检查某个文件夹中是否存在指定的文件类型:
if any(name.endswith(('.c', '.h')) for name in listdir(dirname)):
...
2.3 用Shell通配符匹配字符串¶
fnmatch 模块提供了两个函数—— fnmatch() 和 fnmatchcase()
fnmatch() 函数匹配能力介于简单的字符串方法和强大的正则表达式之间。 如果在数据处理操作中只需要简单的通配符就能完成的时候,这通常是一个比较合理的方案。
如果你的代码需要做文件名的匹配,最好使用 glob 模块。
2.7 最短匹配模式¶
>>> str_pat = re.compile(r'\"(.*)\"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']
>>>
在这个例子中,模式 r’”(.*)”’ 的意图是匹配被双引号包含的文本。 但是在正则表达式中*操作符是贪婪的,因此匹配操作会查找最长的可能匹配。 于是在第二个例子中搜索 text2 的时候返回结果并不是我们想要的。
为了修正这个问题,可以在模式中的*操作符后面加上?修饰符,就像这样:
>>> str_pat = re.compile(r'\"(.*?)\"')
>>> str_pat.findall(text2)
['no.', 'yes.']
>>>
这样就使得匹配变成**非贪婪模式**,从而得到最短的匹配,也就是我们想要的结果。
2.8 多行匹配模式¶
为了修正这个问题,你可以修改模式字符串,增加对换行的支持。比如:
>>> comment = re.compile(r'/\*((?:.|\n)*?)\*/')
>>> comment.findall(text2)
[' this is a\n multiline comment ']
>>>
re.compile() 函数接受一个标志参数叫 re.DOTALL ,在这里非常有用。 它可以让正则表达式中的点(.)匹配包括换行符在内的任意字符。比如:
>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\n multiline comment ']
对于简单的情况使用 re.DOTALL 标记参数工作的很好, 但是如果模式非常复杂或者是为了构造字符串令牌而将多个模式合并起来(2.18节有详细描述), 这时候使用这个标记参数就可能出现一些问题。 如果让你选择的话,最好还是定义自己的正则表达式模式,这样它可以在不需要额外的标记参数下也能工作的很好。
2.9 将Unicode文本标准化¶
unicodedata 模块
2.10 在正则式中使用Unicode¶
混合使用Unicode和正则表达式通常会让你抓狂。 如果你真的打算这样做的话,最好考虑下安装第三方正则式库, 它们会为Unicode的大小写转换和其他大量有趣特性提供全面的支持,包括模糊匹配。
2.11 删除字符串中不需要的字符¶
通常情况下你想将字符串 strip 操作和其他迭代操作相结合,比如从文件中读取多行数据。 如果是这样的话,那么生成器表达式就可以大显身手了。比如:
- with open(filename) as f:
lines = (line.strip() for line in f) for line in lines:
print(line)
在这里,表达式 lines = (line.strip() for line in f) 执行数据转换操作。 这种方式非常高效,因为它不需要预先读取所有数据放到一个临时的列表中去。 它仅仅只是创建一个生成器,并且每次返回行之前会先执行 strip 操作。
对于更高阶的strip,你可能需要使用 translate() 方法。
2.13 字符串对齐¶
对于基本的字符串对齐操作,可以使用字符串的 ljust() , rjust() 和 center() 方法。
函数 format() 同样可以用来很容易的对齐字符串。 你要做的就是使用 <,> 或者 ^ 字符后面紧跟一个指定的宽度。比如:
>>> format(text, '>20')
' Hello World'
>>> format(text, '<20')
'Hello World '
>>> format(text, '^20')
' Hello World '
>>>
如果你想指定一个非空格的填充字符,将它写到对齐字符的前面即可:
>>> format(text, '=>20s')
'=========Hello World'
>>> format(text, '*^20s')
'****Hello World*****'
>>>
但是,在新版本代码中,你应该优先选择 format() 函数或者方法。 format() 要比 % 操作符的功能更为强大。 并且 format() 也比使用 ljust() , rjust() 或 center() 方法更通用, 因为它可以用来格式化任意对象,而不仅仅是字符串。
如果想要完全了解 format() 函数的有用特性, 请参考 在线Python文档
2.14 合并拼接字符串¶
如果你想要合并的字符串是在一个序列或者 iterable 中,那么最快的方式就是使用 join() 方法。比如:
如果你想在源码中将两个字面字符串合并起来,你只需要简单的将它们放到一起,不需要用加号(+)。比如:
>>> a = 'Hello' 'World'
>>> a
'HelloWorld'
>>>
最重要的需要引起注意的是,当我们使用加号(+)操作符去连接大量的字符串的时候是非常低效率的, 因为加号连接会引起内存复制以及垃圾回收操作。 特别的,你永远都不应像下面这样写字符串
同样还得注意不必要的字符串连接操作。有时候程序员在没有必要做连接操作的时候仍然多此一举。比如在打印的时候:
print(a + ':' + b + ':' + c) # Ugly
print(':'.join([a, b, c])) # Still ugly
print(a, b, c, sep=':') # Better
当混合使用I/O操作和字符串连接操作的时候,有时候需要仔细研究你的程序。 比如,考虑下面的两端代码片段:
# Version 1 (string concatenation)
f.write(chunk1 + chunk2)
# Version 2 (separate I/O operations)
f.write(chunk1)
f.write(chunk2)
如果两个字符串很小,那么第一个版本性能会更好些,因为I/O系统调用天生就慢。 另外一方面,如果两个字符串很大,那么第二个版本可能会更加高效, 因为它避免了创建一个很大的临时结果并且要复制大量的内存块数据。 还是那句话,有时候是需要根据你的应用程序特点来决定应该使用哪种方案。
最后谈一下,如果你准备编写构建大量小字符串的输出代码, 你最好考虑下使用生成器函数,利用yield语句产生输出片段。比如:
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'
这种方法一个有趣的方面是它并没有对输出片段到底要怎样组织做出假设。 例如,你可以简单的使用 join() 方法将这些片段合并起来:
text = ''.join(sample())
或者你也可以将字符串片段重定向到I/O:
for part in sample():
f.write(part)
2.15 字符串中插入变量¶
``format() ``
如果要被替换的变量能在变量域中找到, 那么你可以结合使用 format_map() 和 vars() 。
vars() 还有一个有意思的特性就是它也适用于对象实例。比如:
>>> class Info:
... def __init__(self, name, n):
... self.name = name
... self.n = n
...
>>> a = Info('Guido',37)
>>> s.format_map(vars(a))
'Guido has 37 messages.'
>>>
format 和 format_map() 的一个缺陷就是它们并不能很好的处理变量缺失的情况
一种避免这种错误的方法是另外定义一个含有 __missing__() 方法的字典对象,就像下面这样:
class safesub(dict):
"""防止key找不到"""
def __missing__(self, key):
return '{' + key + '}'
现在你可以利用这个类包装输入后传递给 format_map() :
>>> del n # Make sure n is undefined
>>> s.format_map(safesub(vars()))
'Guido has {n} messages.'
>>>
如果你发现自己在代码中频繁的执行这些步骤,你可以将变量替换步骤用一个工具函数封装起来。就像下面这样:
import sys
def sub(text):
return text.format_map(safesub(sys._getframe(1).f_locals))
现在你可以像下面这样写了:
>>> name = 'Guido'
>>> n = 37
>>> print(sub('Hello {name}'))
Hello Guido
>>> print(sub('You have {n} messages.'))
You have 37 messages.
>>> print(sub('Your favorite color is {color}'))
Your favorite color is {color}
>>>
2.16 以指定列宽格式化字符串¶
textwrap 格式化字符串
s = "Look into my eyes, look into my eyes, the eyes, the eyes, \
the eyes, not around the eyes, don't look around the eyes, \
look into my eyes, you're under."
>>> import textwrap
>>> print(textwrap.fill(s, 70))
Look into my eyes, look into my eyes, the eyes, the eyes, the eyes,
not around the eyes, don't look around the eyes, look into my eyes,
you're under.
>>> print(textwrap.fill(s, 40))
Look into my eyes, look into my eyes,
the eyes, the eyes, the eyes, not around
the eyes, don't look around the eyes,
look into my eyes, you're under.
textwrap 模块对于字符串打印是非常有用的,特别是当你希望输出自动匹配终端大小的时候。 你可以使用 os.get_terminal_size() 方法来获取终端的大小尺寸。比如:
>>> import os
>>> os.get_terminal_size().columns
80
>>>
fill() 方法接受一些其他可选参数来控制tab,语句结尾等。
textwrap.TextWrapper文档
2.18 字符串令牌解析¶
关于更高阶的令牌化技术,你可能需要查看 PyParsing 或者 PLY 包。