Python正则表达式

Last Updated At: 2015-01-28

原始材料:

Google Python Class 廖雪峰的Python教程

正则表达式(Regular Expressions)是匹配文本模式(patterns)的有力工具。

本文对Python编程实战中需要的正则表达式概念进行简要介绍。

Python的re模块提供了正则表达式支持,使用前首先需要导入该模块:

import re

在Python中正则表达式搜索的典型语法为:

match = re.search(pat, str)

re.search()方法接收一个正则表达式模式和一个字符串作为参数,并在给定字符串中搜索给定模式。

如果搜索成功,search()返回一个匹配对象,否则返回None

因此,通常搜索会紧跟一个if语句来测试搜索是否成功,如下例所示,搜索word:后紧跟三个字母的模式:

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
    # 'found word:cat'
    print 'found', match.group()
else:
    print 'did not find'

代码match =re.search(pat,str)将搜索结果存储在match变量中,然后if语句对match变量进行测试。

  • 如果为真,则match.group()为匹配文本(e.g. 'word:cat')。

  • 否则match为假(更具体地说是None),即匹配不成功,不存在匹配文本。

字符串开头的r指定一个raw字符串,即对转义字符\不进行解析1,这对正则表达式是很便利的。

因此建议养成习惯在使用正则表达式时始终采用r指定模式。(Java非常需要这个特性!)。

基本模式

正则表达式的强大之处在于可以匹配模式(patterns),而不是固定的字符。 以下是匹配单个字符的最基本的模式:

  • a,X,9,< – 普通字符仅匹配其自身。因为具有特殊含义而不会匹配自身的字符包括:^$*+?{[]\|()(细节如下)
  • .(句点) – 匹配除换行符\n之外的任何字符
  • \w(小写) – 匹配一个单词字符(word char):字母(a-z,A-Z)、数字(0-9)、下划线(_)2\W(大写)匹配任意非单词字符
  • \b – 单词边界,单词字符和非单词字符的边界3
  • \s(小写) – 匹配空白字符:空格,换行符(\n),回车(\r),制表符(\t),换页符(\f)。\S(大写)匹配任意非空白字符
  • \t,\n,\r – 制表符,换行符,回车
  • \d – 数字(0-9)
  • ^(开头),$(结尾) – 匹配字符串的开头和结尾
  • \ – 转义字符,用于匹配模式字符(关键字)。例如,采用\.来匹配句点,\\匹配斜杠4

基本示例

Joke: What do you call a pig with three eyes? piiig!

正则表达式匹配字符串中的模式的基本规则为5

  • 匹配从左到右,在完成第一次匹配后停止
  • 必须匹配所有给定模式才算一次成功匹配
  • 如果match = re.search(pat, str)匹配成功,则match变量非空,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"

重复模式

可以采用+*来指定重复出现的模式

  • + – 匹配1个或更多次出现左边的模式(一定出现),如'i+'表示一个或多个i
  • * – 匹配0个或更多次出现左边的模式(可能出现)
  • ? – 匹配0个或1个左边的模式(最多出现一次)

“Leftmost” & “Largest”

首先找到最先出现(Leftmost)的匹配,再尽可能长的(Largest)匹配字符串。

i.e. +*会尽可能往后匹配字符串(+*的“贪心性”,见下文)。

重复模式示例

# 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"

示例:提取邮箱地址

假设想找到字符串'xyz alice-b@google.com purple monkey'中的邮件地址。

采用模式r'\w+@\w+'的尝试:

str = 'purple alice-b@google.com monkey dishwasher'
match = re.search(r'\w+@\w+', str)
if match:
    print match.group()  # 'b@google'

搜索没有获得整个邮件地址,因为\w不会匹配邮件地址中的'-''.'。我们将采用下面的特性来修正这个问题。

方括号([])

方括号可以用于指定一个字符集合,如[abc]匹配'a''b''c'

\w\s同样可以放入方括号中,但是.只表示实际上的'.'6

对于邮件地址,使用方括号将'.''-'加入需要匹配的字符集合,模式r'[\w.-]+@[\w.-]+'可以获得整个邮件地址。

match = re.search(r'[\w.-]+@[\w.-]+', str)
if match:
    print match.group()  # 'alice-b@google.com'

[]范围

可以使用-来表示范围,即[a-z]匹配所有小写字母。

若要使用-而不表示范围(通常情况),将-放在模式集合的最后,如[abc-]

^放在方括号内开头位置表示取反集,即[^ab]表示除’a’和’b’之外的任意字符。

提取Group

正则表达式的group方法允许程序选取匹配文本的一部分。

假设在邮件地址问题中我们需要提取用户名和主机名。

为了这样做,将用户名模式和主机名模式用()包围起来,例如r'([\w.-]+)@([\w.-]+)'

在这种情况下,()并不影响匹配规则,而是在匹配文本中建立一个分组(group)。对于成功的匹配:

  • match.group(1)表示第一个括号内的模式匹配的文本
  • match.group(2)表示第二个括号内的模式匹配的文本。
  • match.group()仍然表示整个匹配文本7
  • match.groups()返回包括所有分组匹配结果的元组。
str = 'purple alice-b@google.com monkey dishwasher'
match = re.search('([\w.-]+)@([\w.-]+)', str)
if match:
    print match.group()   # 'alice-b@google.com' (the whole match)
    print match.group(1)  # 'alice-b' (the username, group 1)
    print match.group(2)  # 'google.com' (the host, group 2)

通常使用正则表达式的工作流程是首先为需要查找的东西写出相应的模式,然后再添加括号分组以便提取感兴趣的部分。

findall

findall()可能是re模块中最有用的函数。之前我们采用re.search()来查找关于给定模式的Leftmost匹配。

不同的是,findall()将找到所有的匹配并以字符串列表(list)的形式返回,每一个字符串表示一次匹配。

# Suppose we have a text with many email addresses
str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

# Here re.findall() returns a list of all the found email strings
emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
for email in emails:
    # do something with each found email string
    print email

对文件findall

对于文件进行模式匹配,你可能会习惯于写一个循环来在文件的行间迭代,在每一次迭代中调用findall()

与其这样,不如让findall()替你完成迭代过程–这样当然更好!

只需将文件内的文本传递给findall()让其一次性以列表的形式返回所有匹配8

# 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和分组

分组括号()同样可以用于和findall()组合使用。

如果模式包含两个或多个分组括号,则findall()将返回元组(tuple)列表而不是字符串(str)列表。

每个元组表示一次匹配,在元组内则是group(1),group(2),…数据9

所以,如果在邮件地址模式中加入两个分组括号,则findall()将返回一个元组列表,每一个元组包含用户名和主机名。

str = 'purple alice@google.com, blah monkey bob@abc.com 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()返回对应于单个分组的字符串列表。

一条较晦涩的可选特性:

有时模式中包含分组括号(),且并不希望提取它,在括号内以?:开始, e.g.(?:),则不会将匹配文本归入分组结果中。

正则表达式工作流程和调试

正则表达式将许多不同的含义封装进少数字符中,非常稠密,可能需要使用许多时间来调试模式。

将运行时(runtime)设置为可简单地匹配模式并输出匹配文本,例如可以在一个小测试文本上运行并输出findall()的结果。

  • 当什么都不匹配时,因为没有实质性的结果可以查看,很难取得进展。
  • 如果模式没有匹配任何文本,尝试简化模式,移除部分模式以获得更多匹配。
  • 当得到了过多的匹配结果时,可以递增地整理组合模式以便得到最终需要匹配的模式。

选项

re模块中的函数可接收选项来改变模式匹配的规则。

选项作为附加参数放在search()findall()等函数中10,e.g. re.search(par, str, re.IGNORECASE)

  • IGNORECASE – 在匹配中忽略大小写,如'a'同时匹配'a''A'
  • DOTALL – 允许句点.匹配换行符11
  • MULTILINE – 在由许多行组成的字符串内,允许^$匹配每一行的开头和结尾12

贪心与否

假设有一个包含html标签的文本:<b>foo</b><i>so on</i>, 并且尝试采用'(<.*>)'匹配每一个标签 – 匹配结果如何?

结果可能会比较让人吃惊,但是.*的“贪心性”确实会导致模式将<b>foo</b><i>so on</i>分别作为一次匹配。

因为.*会尽可能多地往后匹配,而不是在匹配到>时停止匹配(即其”贪心性”)。

正则表达式的一个扩展,可以在模式结尾加一个?,比如.*?.+?13,可屏蔽匹配规则的贪心性。

这时匹配会在能够完成匹配的情况下尽早结束。

'(<.*?>)'第一次将仅匹配<b>,第二次匹配</b>,则能按顺序获得每个标签。

一种较古老但是很流行的对

除了在字符X处停止匹配外,匹配任意字符(all of these chars except stopping at X)

进行编程的方法是采用方括号。采用[^>]*而不是.*来作为模式,将会跳过除>之外的所有字符14

替换

re.sub(pat, replacement, str)函数在给定字符串中搜索所有匹配模式的结果,并进行替换。

替换字符串可以包含'\1','\2',...,分别引用原始匹配文本的group(1),group(2),...

下面是一个示例,搜索所有邮件地址,并将其变为保留用户名(\1)但是将”yo-yo-dyne.com”作为主机名。

str = 'purple alice@google.com, blah monkey bob@abc.com 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'\1@yo-yo-dyne.com', str)
## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

加强版切分字符串

正常的字符串切分(.split()):

>>> 'a b   c'.split(' ')
['a', 'b', '', '', 'c']

可见其无法应对连续出现的空格字符,正则表达式的re.split()可以解决这个问题:

>>> re.split(r'\s+', 'a b   c')
['a', 'b', 'c']

预编译正则表达式

Python正则表达式re模块工作流程:

  • 编译正则表达式,如果正则表达式模式不合法,报错;
  • 用编译后的正则表达式去匹配字符串。

如果一个正则表达式要重复使用几千次,出于效率的考虑,可以预编译该正则表达式, 之后重复使用时就不需要每次都编译

# 匹配区号+电话号码
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')
  1. 由于Python的字符串本身也用\转义,指定r后实际上是交给正则表达式解析,见下文的基本模式。 

  2. 需要注意的是虽然这个模式的助记词为“word”,但其只匹配一个单词字符,而不是整个单词。 

  3. 正则表达式中所谓的“单词”,就是由\w所定义的字符所组成的字符串。 

  4. 若不确定一个字符是否具有特殊含义,如@,可以采用\@来确保匹配@而不是它可能会对应的特殊模式。 

  5. 这里特指re.search(),关于re.findall()见下文。 

  6. 前文提到,对于不确定是否为模式关键字的情况,可以在前面加一个\,并不会影响结果;-)。 

  7. 试试match.group(0),;-) 

  8. f.read()以单个字符串的形式返回文本文件所有内容。 

  9. 注意这里比较容易出错,findall()返回的就是列表类型,不存在.group()方法,这里表示其对应的数据。 

  10. 可以用dir(re)来查看re模块中的所有参数,以及函数。 

  11. 通常情况下.匹配除换行符外的所有字符。这可能会让你认为.*可以匹配所有文本(这是错的),因为不会进行跨行匹配。注意到\s包含换行符,所以如果你想匹配一连串可能包含换行符的空白符,可以简单地采用\s*。 

  12. 通常情况下,^$只匹配整个字符串的开头和结尾。 

  13. *?最早用于Perl,Perl扩展正则表达式称为Perl Compatible Regular Expressions(PCRE)。Python包括PCRE支持,许多命令行应用都有接收PCRE模式的标志位。 

  14. ^放在模式集合开头时将会对集合取反,即匹配不对应方括号内模式的任何字符。