原文:https://developers.google.com/edu/python/regular-expressions
正则表达式是一种用于匹配文本模式的强大的语言。这部分对正则表达式本身做了基本介绍,这些内容足以帮助我们做 Python 练习,并且介绍了在 Python 中如何使用正则表达式。Python 的 “re” 模块提供了对正则表达式的支持。
在 Python 中,一个典型的正则表达式搜索可以写成:
match = re.search(pat, str)
re.search() 方法的参数分别是一个正则表达式模式和一个字符串,该方法根据模式搜索字符串。如果搜索成功,search() 返回一个匹配的对象。否则返回 None。因此,该函数后面通常会紧跟着一个 if 语句来检查搜索是否成功,如下面的例子,根据 ‘word:’ 后面跟着 3 个字母(后面会详细介绍)的模式去搜索:
str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
print 'found', match.group() ## 'found word:cat'
else:
print 'did not find'
代码 match = re.search(pat, str)
将搜索结果保存在一个名叫 “match” 的变量中。然后用 if 语句检查 match——如果为 true,那么搜索成功并且 match.group() 表示匹配到的文本(例如,’word:cat’)。否则如果 match 为 false,那么搜索失败,并且没有匹配文本。
模式字符串开头的 ‘r’ 指定了 python 中“原始”(”raw”)字符串,它防止字符被反斜杠转义,这样做对于正则表达式是很方便的(Java 非常需要这个特性!)。我建议你可以把在模式字符串前面加上 ‘r’ 当成是一种习惯。
正则表达式的强大之处在于可以指定模式,而不仅仅是不变的字符。下面是匹配单个字符的最基本的模式:
a, X, 9, < —— 精确匹配它们自身的普通字符。元字符由于有特殊的意义,所以它们不会匹配自身:. ^ $ * + ? { [ ] \ | ( )(下面有详细介绍)
.(一个句点) —— 匹配任意单个字符,除了换行符 ‘\n’
\w —— (小写的 w)匹配一个“单词”(”word”)字符:一个字母或者数字或者下划线 [a-zA-Z0-9_]。注意到这里的“单词”(”word”)虽然有助于记忆,但是这里只是匹配单个字符,不是整个单词。\W(大写的 W)匹配任意非单词的字符。
\b —— 单词和非单词间的边界
\s —— (小写的 s)匹配一个空白字符——空格、换行符、回车、制表符、换页 [ \n\r\t\f]。\S(大写的 S)匹配任意一个非空白字符。
\t, \n, \r —— 制表、换行、回车
\d —— 十进制数字 [0-9](一些老的正则表达式工具中不支持 \d,但是它们都支持 \w 和 \s)
^ = start, $ = end —— 匹配开头或者结尾的字符串
\ —— 抑制一个字符的“特殊性”。所以,例如,使用 . 匹配一个句点或者 \ 匹配一个斜杠。如果你不确定一个字符是否有特殊意义,例如 ‘@’,那么你可以在该字符前面加上一个斜杠,\@,来确保它被当作一个字符。
正则表达式在一个字符串中搜索模式的基本规则是:
搜索是从字符串的开头到结尾进行,遇到第一个匹配项时停止
所有的模式必须要匹配到,但不是所有字符串都要匹配
如果 match = re.search(pat, str)
成功,那么匹配结果 match 不是 None,而 match.group() 是匹配到的文本
## Search for pattern 'iii' in string 'piiig'.
## All of the pattern must match, but it may appear anywhere.
## On success, match.group() is matched text.
match = re.search(r'iii', 'piiig') => found, match.group() == "iii"
match = re.search(r'igs', 'piiig') => not found, match == None
## . = any char but \n
match = re.search(r'..g', 'piiig') => found, match.group() == "iig"
## \d = digit char, \w = word char
match = re.search(r'\d\d\d', 'p123g') => found, match.group() == "123"
match = re.search(r'\w\w\w', '@@abcd!!') => found, match.group() == "abc"
在模式中用 + 和 * 指定重复
+ —— 左边的模式出现一次或多次,例如,’i+’ = 一个或者多个 i
* —— 左边的模式出现 0 次或者多次
? —— 匹配左边模式 0 次或者 1 次
首先 search 方法会查找最左边匹配模式的字符串,然后它会尝试查找能够匹配的尽可能多的字符串——例如,+ 和 * 会尽可能多的匹配字符(+ 和 * 被称为贪婪模式)
## i+ = one or more i's, as many as possible.
match = re.search(r'pi+', 'piiig') => found, match.group() == "piii"
## Finds the first/leftmost solution, and within it drives the +
## as far as possible (aka 'leftmost and largest').
## In this example, note that it does not get to the second set of i's.
match = re.search(r'i+', 'piigiiii') => found, match.group() == "ii"
## \s* = zero or more whitespace chars
## Here look for 3 digits, possibly separated by whitespace.
match = re.search(r'\d\s*\d\s*\d', 'xx1 2 3xx') => found, match.group() == "1 2 3"
match = re.search(r'\d\s*\d\s*\d', 'xx12 3xx') => found, match.group() == "12 3"
match = re.search(r'\d\s*\d\s*\d', 'xx123xx') => found, match.group() == "123"
## ^ = matches the start of string, so this fails:
match = re.search(r'^b\w+', 'foobar') => not found, match == None
## but without the ^ it succeeds:
match = re.search(r'b\w+', 'foobar') => found, match.group() == "bar"
假设你想从字符串 ‘purple [email protected] monkey dishwasher’ 中找到邮件地址。我们将会使用这个例子来说明更多的正则表达式特性。下面的代码尝试使用模式 r’\w+@\w+’:
str = 'purple [email protected] monkey dishwasher'
match = re.search(r'\w+@\w+', str)
if match:
print match.group() ## 'b@google'
该搜索获取不到完整的邮件地址,因为 \w 不会匹配邮件地址中的 ‘-’ 或者 ‘.’。我们将会在下面使用正则表达式的特性来解决这个问题。
方括号可以用于表示一组字符,所以 [abc] 可以匹配 ‘a’ 或者 ‘b’ 或者 ‘c’。像 \w、\s 等这些特殊字符也可以在方括号里面工作,除了小圆点(.)仅仅表示字面意义的小圆点。对于上述电子邮件的问题,可以通过模式 r’[\w.-]+@[\w.-]+’ 来得到完整的邮件地址:
match = re.search(r'[\w.-]+@[\w.-]+', str)
if match:
print match.group() ## '[email protected]'
(更多的方括号特性)你可以使用一个破折号来表示一个范围,所以 [a-z] 匹配所有小写字母。如果想要使用一个破折号又不想让它指示一个范围,那么将破折号放到最后,例如,[abc-]。方括号开头使用(^)表示反转,所以 [^ab] 表示匹配除了 ‘a’ 或者 ‘b’ 意外的任意字符。
正则表达式的 “group” 特性让我们挑出匹配文本的部分内容。假设对于电子邮件那个问题,我们想要分开提取用户名和 host。为了达到这个目的,在模式中用户名和 host 两边添加小括号 (),即:r’([\w.-]+)@([\w.-]+)’。这种情况下,小括号不会改变模式所要匹配的东西,反而,小括号会在匹配文本中建立合乎逻辑的“组”(”groups”)。对于一个成功的搜索,match.group(1) 的匹配文本对应左边第 1 个小括号的内容,match.group(2) 的匹配文本对应左边第 2 个小括号的内容。而 match.group() 还是完整的匹配文本。
str = 'purple [email protected] monkey dishwasher'
match = re.search(r'([\w.-]+)@([\w.-]+)', str)
if match:
print match.group() ## '[email protected]' (the whole match)
print match.group(1) ## 'alice-b' (the username, group 1)
print match.group(2) ## 'google.com' (the host, group 2)
正则表达式的一个通用流程是你为想要查找的东西写一个模式,然后添加小括号来提取你想要的部分。
在 re 模块中,findall() 很可能是一个最强大的函数。上面我们使用 re.search() 来查找一个模式中的第一个匹配内容。findall() 则查找全部匹配的内容并且将匹配的结果以字符串的形式放到一个列表中,每个字符串代表一个匹配项。
## Suppose we have a text with many email addresses
str = 'purple [email protected], blah monkey [email protected] blah dishwasher'
## Here re.findall() returns a list of all the found email strings
emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['[email protected]', '[email protected]']
for email in emails:
# do something with each found email string
print email
对于文件,你可能会习惯写一个循环一行一行地遍历文件,然后为每行调用 findall()。然而,可以让 findall() 为你做遍历——这样做更好!只需要将整个文件文本传进 findall(),然后用一个步骤让它返回一个包含所有匹配内容的列表(f.read() 返回一个包含一个文件里所有文本的字符串):
# Open file
f = open('test.txt', 'r')
# Feed the file text into findall(); it returns a list of all the found strings
strings = re.findall(r'some pattern', f.read())
小括号 () 分组机制可以与 findall() 联合起来使用。如果模式包含 2 个或更多的小括号分组,那么不是返回一个由字符串组成的列表,而是用 findall() 返回一个由元组组成的列表。每个元组代表一个模式匹配,并且在元组里面是 group(1)、group(2)……数据。所以如果添加 2 个小括号分组到电子邮件模式中,那么 findall() 会返回一个由元组组成的列表,每个长度为 2 的元组包含用户名和 host,例如:(‘alice’, ‘google.com’)。
str = 'purple [email protected], blah monkey [email protected] blah dishwasher'
tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
print tuples ## [('alice', 'google.com'), ('bob', 'abc.com')]
for tuple in tuples:
print tuple[0] ## username
print tuple[1] ## host
一旦你有了由元组组成的列表,你可以用循环来为每个元组做一些计算。如果模式没有小括号,那么 findall() 会返回一个像之前的例子中由找到的字符串组成的列表。如果模式包含一对小括号,那么 findall() 会返回由对应该单组的字符串组成的列表。(模糊可选特性:有时在模式中会有括号 ()用于分组,但是你不想提取分组的内容。在这种情况下,在括号的开头写上 ?:,例如,(?: ),那个左括号就不会被算作一个分组结果。即非捕获匹配,在不需要捕获只需要指定分组的地方就可以使用)。
正则表达式模式将大量的含义打包进几个字符里面,但是它们是如此紧密,使得你可以花费大量的时间去调试你的模式。设置你的运行时,使得你可以运行一个模式并且很容易地打印它匹配到的内容,例如,可以在一个小的测试文本中运行这个模式,并且用 findall() 打印出结果。如果模式匹配不到东西,尝试弱化模式,删掉一部分从而得到大量的匹配结果。当它匹配不到东西,由于没有实际的内容用于查找,你不能继续做任何处理。当匹配结果太多时,你可以渐渐地缩小范围来找到你想要的。
re 模块里面的函数用选项来修改模式匹配的行为。选项标志被当作附加参数添加到 search() 或者 findall() 等函数中,例如,re.search(pat, str, re.IGNORECASE)。
IGNORECASE —— 在匹配中不区分大小写,即 ‘a’ 可以匹配 ‘a’ 和 ‘A’。
DOTALL —— 允许小圆点 (.) 匹配换行符 —— 通常小圆点匹配除了换行符的任意字符。这会使你犯错 —— 你会认为 .* 匹配所有字符,但是默认它不会越过一行的末尾。注意到 \s(空格)包括换行符,所以如果你想匹配一连串可能包含换行符的空格,那么使用 \s* 即可。
MULTILINE —— 一个由很多行组成的字符串,允许用 ^ 和 $ 来匹配每行的开头和结尾。通常 ^/$
只会匹配整个字符串的开头和结尾。
这一节课介绍了一种更高级的正则表达式技术。
假设有一组带有标签的文本:foo and so on
。
假设你尝试用模式 ‘(<.*>)’ 去匹配每个标签 —— 它首先会匹配什么?
结果可能会让人感到惊讶,但是 .* 贪婪的一面导致它匹配了整个字符串 ‘foo and so on
‘。这里的问题是 .* 会尽可能多地匹配字符,而不是在遇到第一个 > 的时候就停止(又名为“贪婪”)。
此时,你可以在末尾添加一个 ?,如 .*? 或者 .+?,将它们编程非贪婪。现在它们会尽可能快地停止匹配。所以模式 ‘(<.*?>)
’ 只会获取 ‘’ 作为第一个匹配结果,而 ‘
’ 作为第二个匹配结果,并且依次获取每个 <..> 对。你常常会使用 .*? ,然后马上查找具体的标志(这里是 >)来强制 .*? 的结束。
*? 扩展起源于 Perl,而包含 Perl 扩展的正则表达式被称为兼容 Perl 的正则表达式库(Perl Compatible Regular Expressions —— pcre)。Python 包含 pcre 的支持。很多命令行工具等会有一个标志来表示是否支持 pcre 模式。
一种更老的却又被广泛使用的技术使用方括号风格来用代码实现 “all of these chars except stopping at X” 这个想法。对于上面的模式,可以使用 [^>]* 来跳过那些不是 > 的字符(方括号开头的 ^ “反转”,使得它匹配任意除了括号里面的字符)。
re.sub(pat, replacement, str) 函数搜索一个给定字符串中所有模式的实例,并且替换它们。字符串 replacement 可以包括 ‘\1’、’\2’,即引用 group(1)、group(2) 等等从原始匹配文本中获得的文本。
下面的例子搜索所有电子邮件地址,并且保持用户名 (\1) 不变,改变 host 为 yo-yo-dyne.com。
str = 'purple [email protected], blah monkey [email protected] blah dishwasher'
## re.sub(pat, replacement, str) -- returns new string with all replacements,
## \1 is group(1), \2 group(2) in the replacement
print re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\[email protected]', str)
## purple [email protected], blah monkey [email protected] blah dishwasher
尝试解决 google-python-exercises 中 babynames/ 目录下的问题。