第二章 字符串和文本
2.1 使用多个界定符分割字符串
re.split() 方法很适合同时用多个定界符或定界符周围有空格的情形。
import re
line = 'abc def, ghi/ jkl-mno'
re.split('[ ,/-]\s*', line)
# ['abc', 'def', 'ghi', 'jkl', 'mno']
当正则表达式中包含括号时,其捕获的子组也会出现在结果中。需要原字符串的拼接格式时可以这么用,使用 (?😃 来避免子组出现在结果中。
2.2 字符串开头或结尾匹配
字符串的 startswith() 和 endswith() 方法。
想匹配多种可能,需要把所有字符串放到元组中传给方法。
[name for name in namelist if name.endswith(('.xls', '.xlsx'))]
if any(name.endswith(('.c', '.py')) for name in os.dirlist(path)):
pass
2.3 用 shell 通配符匹配字符串
fnmatch 模块的 fnmatch() 和 fnmatchcase() 方法。介于简单匹配和正则表达式之间。
from fnmatch import fnmatch, fnmatchcase
fnmatch('foo.txt', '*.txt')
# True
fnmatch() 用底层操作系统对应的大小写敏感规则;fnmatchcase() 完全使用给定的模式进行匹配。
2.4 字符串匹配和搜索
字面匹配:find(),startswith(),endswith() 等方法。
复杂的匹配:正则表达式和 re 模块。
需要用同一个模式做大量匹配时,可用 re.compile 把模式预编译为模式对象;
括号捕获子组,结果用 group() 方法全部或分别显示;
findall() 返回列表形式;finditer() 返回迭代器形式。
2.5 字符串搜索和替换
简单的替换,用 replace() 方法;复杂的替换,用 re.sub() 方法。
import re
text = 'Today is 11/27/2019.'
re.sub('(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
# 'Today is 2019-11-27.'
sub() 方法第一个参数是匹配模式,第二个参数是替换模式。\3 是指前面的第三个子组。
需要多次替换的话,就编译这个模式。
datepat = re.compile('(\d+)/(\d+)/(\d+)')
datepat.sub(r'\3-\1-\2', text)
对于更复杂的替换,可以传递一个替换回调函数来实现。替换回调函数的参数是匹配得到的 Match 对象,必须返回一个用于替换的字符串。
from calendar import month_abbr
def change_month(m):
mon_name = month_abbr[int(m.group(1))]
return f'{m.group(2)} {mon_name} {m.group(3)}'
datepat.sub(change_month, text)
# 'Today is 27 Nov 2019.'
如果想知道替换了多少次,可以用 re.subn() 方法:
newtext, n = datepat.subn(r'\3-\1-\2', text)
# 'Today is 2019-11-27.', 1
2.6 字符串忽略大小写的搜索替换
使用 re 搜索和替换时提供 re.IGNORECASE 给参数 flags 即可忽略大小写。但是在替换时想要让替换后的大小写模式和原来的一致,就需要一个辅助函数。
import re
text = 'abc ABC Abc'
re.sub('abc', 'xyz', text, flags=re.I)
# 'xyz xyz xyz'
def matchcase(word):
def replace(m):
t = m.group()
if t.isupper():
return word.upper()
elif t.islower():
return word.lower()
elif t.capitalize():
return word.capitalize()
else:
return word
return replace
re.sub('abc', matchcase('xyz'), text, flags=re.I)
# 'xyz XYZ Xyz'
2.7 最短匹配模式
在正则表达式的 + 或 * 后面加 ? 实现。
2.8 多行匹配模式
需要匹配跨越多行的信息,但是 . 符号不能匹配换行符,可以指定一个非捕获子组:
import re
text = '#123\nabc\n123#'
re.findall('#((?:.|\n)*?)#', text)
# ['123\nabc\n123']
如果只是简单的匹配,可以使用 re.DOTALL 标志参数:
re.findall('#(.*?)#', text, flags=re.DOTALL)
# ['123\nabc\n123']
2.9 将 Unicode 文本标准化
Unicode 中的某些字符有多种合法的编码表示,在需要比较字符串时不同的编码表示会出现问题。
可以用 unicodedata 模块使字符串标准化。
import unicodedata
unicodedata.normalize('NFC', text)
unicodedata.normalize('NFD', text)
2.10 在正则式中使用 Unicode
想在模式中包含指定的 Unicode 字符,可以使用这些字符对应的转移序列。
执行匹配和搜索操作时,最好先标准化。
有些特殊情况最好使用第三方的正则式库。
2.11 删除字符串中不需要的字符
strip() 方法删除开头和结尾的指定字符;lstrip() 和 rstrip() 分别删除开头和结尾的字符。默认去除空格,也能指定其他字符。
对于字符串中间的字符处理,需要使用 replace() 或者正则实现。
2.12 审查清理文本字符串
想要将不需要的字符剔除掉。
translate() 方法,传入一个映射,替换字符串中的字符。
目标是获取文本对应的 ASCII 表示的时候,可以先标准化,再使用 encode() 和 decode() 清除 ASCII 以外的字符。
对于简单字符的替换,replace() 是最快的;对于复杂字符的替换和删除操作,translate() 更快。
2.13 字符串对齐
基本的对齐操作,可以使用 ljust(),rjust(),center()。指定宽度和填充字符(默认空格)。
format() 函数用来对齐字符串,format(str, ‘[fillchar]>width’),<、>、^ 分别表示左对齐、右对齐和居中对齐。
格式化同一个字符串的多个值时,这些格式化符号也能用在 format 的花括号中。
format() 对其它值也很通用。
2.14 合并拼接字符串
如果想合并的字符串在一个列表或迭代器中,使用 join() 方法最快。
简单地合并少数几个字符串,使用 ‘+’ 就够了。
字面字符串,直接放到一起就能合并。
使用 ‘+’ 时,会引起内存复制和垃圾回收操作,因此在合并大量字符串时不如 join() 高效。join() 还能使用生成器表达式,处理数据的同时完成连接操作。
2.15 字符串中插入变量
format() 方法可以实现该需求。如果格式化用的变量能在变量域中找到,可以结合使用 format_map() 和 vars() 。 vars() 可适用于对象实例,将对象的属性作为格式化的变量。
s = "{name} is {age}."
class Stu(object):
def __init__(self, name, age):
self.name = name
self.age = age
stua = Stu('Mike', 24)
s.format_map(vars(stua))
# 'Mike is 24.'
format() 和 format_map() 不能很好地处理变量缺失的情况,可以另外定义一个含有 missing() 方法的字典对象来解决。
class safesub(dict):
def __missing__(self, key):
return '{' + key + '}'
del age
s.format_map(safesub(vars()))
# 'Mike is {age}.'
如果需要频繁执行这类操作,可以定义一个用于变量替换的工具函数:
import sys
def sub(text):
return text.format_map(sagesub(sys._getframe(1).l_locals)
sub('Hello, {name}!')
# 'Hello, Mike!'
sub('He is {age}.')
# 'He is 24.'
sub('His hair is {color}.')
# 'His hair is {color}.'
2.16 以指定列宽格式化字符串
textwrap 模块用于对字符串的输出进行格式化。
testwrap.fill(text, width=70, **kwargs)
fill() 中的关键字参数可用于自定义输出的格式。
可以和 os.get_terminal_size() 结合来让输出自动匹配终端大小。
2.17 在字符串中处理 html 和 xml
想替换特定字符如 ‘<’ 或 ‘>’,html 模块的 escape() 可以实现。
需要把非 ASCII 字符对应的编码实体嵌入,可以在 encode 时传入参数 errors=‘xmlcharrefreplace’。
输出时选用合适的解析器把编码实体替换为对应字符。
2.18 字符串令牌解析
# 1.用命名捕获组的正则表达式来定义所有令牌
import re
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ = r'(?P<EQ>=)'
WS = r'(?P<WS>\s+)'
master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
# 2.使用scanner()方法创建scanner对象,调用match()方法,一步步扫描目标文本,每次匹配一次,可以写成生成器
def generate_tokens(pat, text):
Token = namedtuple('Token', ['type', 'value'])
scanner = pat.scanner(text)
for m in iter(scanner.match, None):
yield Token(m.lastgroup, m.group())
for tok in generate_tokens(master_pat, 'foo = 42'):
print(tok)
第一步组装 master_pat 的时候,各个令牌的顺序是有影响的,如果一个模式是另一个更长模式的子字符串,长的模式必须写在前面。
2.19 实现一个简单的递归下降分析
2.20 字节字符串上的字符串操作
字节字符串也支持大部分文本字符串的内置操作。
正则表达式也可以用来匹配字节字符串,但是正则表达式本身也得是字节串。
字节串的索引返回的是整数而不是字符。
字节串不支持格式化操作,只能先用字符串格式化,再编码为字节串。