Previous

2.4 字符串匹配和搜索

问题

你想匹配或者搜索特定模式的文本

解决方案

如果你想匹配的是字面字符串,那么你通常只需要调用基本字符串方法就行, 比如 str.find() , str.endswith() , str.startswith() 或者类似的方法:


In [1]:
text = "yeah, but no, but yeah, but no, but yeah"
# Exact match
text == "yeah"


Out[1]:
False

In [2]:
# Match at start or end
text.startswith("yeah")


Out[2]:
True

In [3]:
text.endswith("yeah")


Out[3]:
True

In [4]:
text.endswith("no")


Out[4]:
False

In [5]:
text.find("no")


Out[5]:
10

对于复杂的匹配需要使用正则表达式和 re 模块。 为了解释正则表达式的基本原理,假设你想匹配数字格式的日期字符串比如 11/27/2012 ,你可以这样做:


In [6]:
text1 = "11/27/2012"
text2 = "Nov 27, 2012"

In [7]:
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

In [8]:
if re.match(r"\d+/\d+/\d+", text2):
    print("yes")
else:
    print("no")


no

如果你想使用同一个模式去做多次匹配,你应该先将模式字符串预编译为模式对象。比如:


In [9]:
datepat = re.compile(r"\d+/\d+/\d+")
if datepat.match(text1):
    print("yes")
else:
    print("no")


yes

In [10]:
if datepat.match(text2):
    print("yes")
else:
    print("no")


no

match() 总是从字符串开始去匹配,如果你想查找字符串任意部分的模式出现位置, 使用 findall() 方法去代替。比如:


In [11]:
text = "Today is 11/27/2012, PyCon starts 3/13/2013."
datepat.findall(text)


Out[11]:
['11/27/2012', '3/13/2013']

在定义正则式的时候,通常会利用括号去捕获分组。比如:


In [12]:
datepat = re.compile(r"(\d+)/(\d+)/(\d+)")

捕获分组可以使得后面的处理更加简单,因为可以分别将每个组的内容提取出来。比如:


In [13]:
m = datepat.match("11/27/2012")
m


Out[13]:
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>

In [14]:
# Extract the contents of each group
m.group(0)


Out[14]:
'11/27/2012'

In [15]:
m.group(1)


Out[15]:
'11'

In [16]:
m.group(2)


Out[16]:
'27'

In [17]:
m.group(3)


Out[17]:
'2012'

In [18]:
m.groups()


Out[18]:
('11', '27', '2012')

In [19]:
month, day, year = m.groups()
print(month, day, year)


11 27 2012

In [20]:
# Find all matches (notice splitting into tuples)
text


Out[20]:
'Today is 11/27/2012, PyCon starts 3/13/2013.'

In [21]:
datepat.findall(text)


Out[21]:
[('11', '27', '2012'), ('3', '13', '2013')]

In [22]:
for month, day, year in datepat.findall(text):
    print("{}-{}-{}".format(year, month, day))


2012-11-27
2013-3-13

findall() 方法会搜索文本并以列表形式返回所有的匹配。 如果你想以迭代方式返回匹配,可以使用 finditer() 方法来代替,比如:


In [23]:
for m in datepat.finditer(text):
    print(m.groups())


('11', '27', '2012')
('3', '13', '2013')

讨论

关于正则表达式理论的教程已经超出了本书的范围。 不过,这一节阐述了使用 re 模块进行匹配和搜索文本的最基本方法。 核心步骤就是先使用 re.compile() 编译正则表达式字符串, 然后使用 match() , findall() 或者 finditer() 等方法。

当写正则式字符串的时候,相对普遍的做法是使用原始字符串比如 r'(\d+)/(\d+)/(\d+)' 。 这种字符串将不去解析反斜杠,这在正则表达式中是很有用的。 如果不这样做的话,你必须使用两个反斜杠,类似 '(\\d+)/(\\d+)/(\\d+)'

需要注意的是 match() 方法仅仅检查字符串的开始部分。它的匹配结果有可能并不是你期望的那样。比如:


In [24]:
m = datepat.match("11/27/2012abcdef")
m


Out[24]:
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>

In [25]:
m.group()


Out[25]:
'11/27/2012'

如果你想精确匹配,确保你的正则表达式以 $ 结尾,就像这么这样:


In [26]:
datepat = re.compile(r"(\d+)/(\d+)/(\d+)$")
datepat.match("11/27/2012abcdef")

In [27]:
datepat.match("11/27/2012")


Out[27]:
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>

最后,如果你仅仅是做一次简单的文本匹配 / 搜索操作的话,可以略过编译部分,直接使用 re 模块级别的函数。比如:


In [28]:
re.findall(r"(\d+)/(\d+)/(\d+)", text)


Out[28]:
[('11', '27', '2012'), ('3', '13', '2013')]

但是需要注意的是,如果你打算做大量的匹配和搜索操作的话,最好先编译正则表达式,然后再重复使用它。 模块级别的函数会将最近编译过的模式缓存起来,因此并不会消耗太多的性能, 但是如果使用预编译模式的话,你将会减少查找和一些额外的处理损耗。

Next