字符串查找替换
查找
在字符串中匹配和搜索指定文本的常用方案是使用字符串的find, startswith, endswith等方法。如下示例:
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> # Exact match
>>> text == 'yeah'
False
>>> # Match at start or end
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> # Search for the location of the first occurrence
>>> text.find('no')
10
>>>
对于更为复杂的匹配和搜索文本场景,通常需要使用正则表达式和re模块。值得注意的是,大多数正则表达式操作都可使用re模块级函数或编译的正则表达式对象(compiled regular expressions)。 这些函数相当于快捷方式,不需要先编译正则表达式对象,但会失去一些微调参数的能力。
例如,我们要搜索字符串中的一段日期文本,如“11/27/2012”,可以按如下方式:
>>> text1 = '11/27/2012'
>>> text2 = 'Nov 27, 2012'
>>>
>>> import re
>>> # Simple matching: \d+ means match one or more digits
>>> if re.match(r'\d+/\d+/\d+', text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if re.match(r'\d+/\d+/\d+', text2):
... print('yes')
... else:
... print('no')
...
no
>>>
值得注意的是,match函数只会查找整个字符串的开头,若未找到,则返回False。若要进行全局搜索,以查找所有位置的匹配,可以使用findall函数。
>>> datepat = re.compile(r'\d+/\d+/\d+')
>>> if datepat.match(text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if datepat.match(text2):
... print('yes')
... else:
... print('no') ...
no
>>>
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>>
在定义正则表达式时,通常通过将模式的一部分括在括号中来引入捕获组。 例如:
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object at 0x1005d2750>
>>> # Extract the contents of each group
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
>>> month, day, year = m.groups()
>>>
>>> # Find all matches (notice splitting into tuples)
>>> text
'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for month, day, year in datepat.findall(text):
... print('{}-{}-{}'.format(year, month, day))
...
2012-11-27
2013-3-13
>>>
findall()方法搜索文本并查找所有匹配项,并将它们作为列表返回。 如果要迭代地查找匹配项,请改用finditer()方法。 例如:
>>> for m in datepat.finditer(text):
... print(m.groups())
...
('11', '27', '2012')
('3', '13', '2013')
>>>
但请注意,如果要执行大量匹配或搜索,通常需要先编译模式并反复使用它。 模块级函数保留了最近编译模式的缓存,因此没有大的性能损失,但是使用自己的编译模式对象节省一些查找和额外处理。
替换
对于简单的文本替换,可以使用str的replace方法,如下:
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'
>>>
对于更为复杂的场景,可以使用re模块的sub函数。要将“11/27/2012”替换为“2012-11-27”,如下代码所示:
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>
sub()的第一个参数是要匹配的模式,第二个参数是替换模式。 诸如\ 3之类的反斜杠数字指的是模式中的捕获组编号。
如果要执行相同模式的重复替换,请考虑首先编译它以获得更好的性能。 例如:
>>> import re
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> datepat.sub(r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>
对于更复杂的替换,可以指定替换回调函数。 例如:
>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'
>>>
作为输入,替换回调的参数是matchj对象,由match()或find()返回。 使用.group()方法提取匹配的特定部分。 该函数应返回替换文本。
如果想知道除了获取替换文本之外还进行了多少次替换,请使用re.subn()代替。 例如:
>>> newtext, n = datepat.subn(r'\3-\1-\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2
>>>
## 忽略大小写的搜索替换 ## 要执行不区分大小写的文本操作,需要使用re模块并将re.IGNORECASE标志应用于各种函数操作。 例如: ```Python >>> text = 'UPPER PYTHON, lower python, Mixed Python' >>> re.findall('python', text, flags=re.IGNORECASE) ['PYTHON', 'python', 'Python'] >>> re.sub('python', 'snake', text, flags=re.IGNORECASE) 'UPPER snake, lower snake, Mixed snake' >>> ``` 最后一个例子的问题是,替换后的文本并没有保留原文本的大小写样式。对于这种情景,可以使用自定义函数来解决: ```Python def matchcase(word): def replace(m): text = m.group() if text.isupper(): return word.upper() elif text.islower(): return word.lower() elif text[0].isupper(): return word.capitalize() else: return word return replace
re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
对于简单的情况,只需提供re.IGNORECASE就足以执行不区分大小写的匹配。 但是,请注意,对于涉及大小写折叠的某些类型的Unicode匹配,这可能是不够的.
</br>
## 正则表达式的贪心与非贪心匹配 ##
这个问题经常出现在试图匹配一对起始和结束分隔符内的文本的模式中,如下:
```Python
>>> 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.']
>>>
这就是非贪心的匹配模式,且产生最短的匹配。
匹配多行的正则表达式模式
尝试使用正则表达式匹配文本块,但需要匹配跨越多行,如下所示:
>>> comment = re.compile(r'/\*(.*?)\*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiline comment */ ... '''
>>>
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
>>>
为了解决这个问题,需要添加对换行符(\n)的支持。例如:
>>> 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 ']