Python3 如何优雅地使用正则表达式(详解二)

使用正则表达式

现在我们开始来写一些简单的正则表达式吧。Python 通过 re 模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配。

小甲鱼解释:re 模块是使用 C 语言编写,所以效率比你用普通的字符串方法要高得多;将正则表达式进行编译(compile)也是为了进一步提高效率;后边我们会经常提到“模式”,指的就是正则表达式被编译成的模式对象。

编译正则表达式

正则表达式被编译为模式对象,该对象拥有各种方法供你操作字符串,如查找模式匹配或者执行字符串替换。

  1. >>> import re
  2. >>> p = re.compile('ab*')
  3. >>> p
  4. <_sre.SRE_Pattern object at 0x...>

复制代码

re.compile() 也可以接受 flags 参数,用于开启各种特殊功能和语法变化,我们会在后边一一介绍。

现在我们先来看个简单的例子:

  1. >>> p = re.compile('ab*', re.IGNORECASE)

复制代码

正则表达式作为一个字符串参数传给 re.compile()。由于正则表达式并不是 Python 的核心部分,因此没有为它提供特殊的语法支持,所以正则表达式只能以字符串的形式表示。(有些应用根本就不需要使用到正则表达式,所以 Python 社区的小伙伴们认为没有必要将其纳入 Python 的核心。)相反,re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和 zlib 模块。

使用字符串来表示正则表达式保持了 Python 简洁的一贯风格,但也因此有一些负面影响,下边我们就来谈一谈。

麻烦的反斜杠

上一篇中我们已经提到了,正则表达式使用 '\' 字符来使得一些普通的字符拥有特殊的能力(例如 \d 表示匹配任何十进制数字),或者剥夺一些特殊字符的能力(例如 \[ 表示匹配左方括号 '[')。这会跟 Python 字符串中实现相同功能的字符发生冲突。

小甲鱼解释:挺拗口,接着看例子你就懂了~

现在的情况是,你需要在 LaTeX 文件中使用正则表达式匹配字符串 '\section'。因为反斜杠作为需要匹配的特殊字符,所以你需要再它前边加多一个反斜杠来剥夺它的特殊功能。所以我们会把正则表达式的字符写成 '\\section'。

但不要忘了,Python 在字符串中同样使用反斜杠来表示特殊意义。因此,如果我们想将 '\\section' 完整地传给 re.compile(),我们需要再次添加两个反斜杠......

匹配字符 匹配阶段
\section 需要匹配的字符串
\\section 正则表达式使用 '\\' 表示匹配字符 '\'
"\\\\section" 不巧,Python 字符串也使用 '\\' 表示字符 '\'

简而言之,为了匹配反斜杠这个字符,我们需要在字符串中使用四个反斜杠才行。所以,在正则表达式中频繁地使用反斜杠,会造成反斜杠风暴,进而导致你的字符串极其难懂。

解决方法是使用 Python 的原始字符串来表示正则表达式(就是在字符串前边加上 r,大家还记得吧...):

正则字符串 原始字符串
"ab*" r"ab*"
"\\\\section" r"\\section"
"\\w+\\s+\\1" r"\w+\s+\1"

小甲鱼解释:强烈建议使用原始字符串来表达正则表达式。

实现匹配

当你将正则表达式编译之后,你就得到一个模式对象。那你拿他可以用来做什么呢?模式对象拥有很多方法和属性,我们下边列举最重要的几个来讲:

方法 功能
match() 判断一个正则表达式是否从开始处匹配一个字符串
search() 遍历字符串,找到正则表达式匹配的第一个位置
findall() 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回
finditer() 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回

如果没有找到任何匹配的话,match() 和 search() 会返回 None;如果匹配成功,则会返回一个匹配对象(match object),包含所有匹配的信息:例如从哪儿开始,到哪儿结束,匹配的子字符串等等。

接下来我们一步步讲解:

  1. >>> import re
  2. >>> p = re.compile('[a-z]+')
  3. >>> p
  4. re.compile('[a-z]+')

复制代码

现在,你可以尝试使用正则表达式 [a-z]+ 去匹配各种字符串。

例如:

  1. >>> p.match("")
  2. >>> print(p.match(""))
  3. None

复制代码

因为 + 表示匹配一次或者多次,所以空字符串不能被匹配。因此,match() 返回 None。

我们再尝试一个可以匹配的字符串:

  1. >>> m = p.match('fishc')
  2. >>> m
  3. <_sre.SRE_Match object; span=(0, 5), match='fishc'>

复制代码

在这个例子中,match() 返回一个匹配对象,我们将其存放在变量 m 中,以便日后使用。

接下来让我们来看看匹配对象里边有哪些信息吧。匹配对象包含了很多方法和属性,以下几个是最重要的:

方法 功能
group() 返回匹配的字符串
start() 返回匹配的开始位置
end() 返回匹配的结束位置
span() 返回一个元组表示匹配位置(开始,结束)

大家看:

  1. >>> m.group()
  2. 'fishc'
  3. >>> m.start()
  4. 0
  5. >>> m.end()
  6. 5
  7. >>> m.span()
  8. (0, 5)

复制代码

由于 match() 只检查正则表达式是否在字符串的起始位置匹配,所以 start() 总是返回 0。

然而,search() 方法可就不一样咯:

  1. >>> print(p.match('^_^fishc'))
  2. None
  3. >>> m = p.search('^_^fishc')
  4. >>> print(m)
  5. <_sre.SRE_Match object; span=(3, 8), match='fishc'>
  6. >>> m.group()
  7. 'fishc'
  8. >>> m.span()
  9. (3, 8)

复制代码

在实际应用中,最常用的方式是将匹配对象存放在一个局部变量中,并检查其返回值是否为 None。

形式通常如下:

  1. p = re.compile( ... )
  2. m = p.match( 'string goes here' )
  3. if m:
  4. print('Match found: ', m.group())
  5. else:
  6. print('No match')

复制代码

有两个方法可以返回所有的匹配结果,一个是 findall(),另一个是 finditer()。

findall() 返回的是一个列表:

  1. >>> p = re.compile('\d+')
  2. >>> p.findall('3只小甲鱼,15条腿,多出的3条在哪里?')
  3. ['3', '15', '3']

复制代码

findall() 需要在返回前先创建一个列表,而 finditer() 则是将匹配对象作为一个迭代器返回:

  1. >>> iterator = p.finditer('3只小甲鱼,15条腿,还有3条去了哪里?')
  2. >>> iterator
  3. <callable_iterator object at 0x10511b588>
  4. >>> for match in iterator:
  5. print(match.span())
  6. (0, 1)
  7. (6, 8)
  8. (13, 14)

复制代码

小甲鱼解释:如果列表很大,那么返回迭代器的效率要高很多。迭代器的相关知识请看:《零基础入门学习Python》048 | 魔法方法:迭代器

上一篇:SQL中MAX()


下一篇:pat甲级题解(更新到1013)