深入 Python 正则表达式:Python 正则表达式的换行处理、替换、贪婪以及非兰贪婪算法

Tags: python regex

这篇文章继续介绍 Python 正则表达式,这篇文章基于前两篇文章中我们介绍的相关内容,其他两篇请参考:

Python 正则表达式例子 - Re Match Search FindAll

如何使用 Python 正则表达式解析文本文件

在这篇文章中,我们将主要讨论:

  1. 处理多行字符串

  2. 对比贪婪算法和非贪婪算法

  3. 使用正则表达式替换字符串

1. 处理多行字符串

当你处理多行字符串时(使用换行符分隔 '\n'),会出现一堆情况,比如:希望匹配的内容横跨多行,考虑下面的 html 片段:

  >>> paragraph = \
  ... '''
  ... <p>
  ... This is a paragraph.
  ... It has multiple lines.
  ... </p>
  ... '''
  >>>

如果我们打算取得段落标签(

...

)中的所有内容,使用下面的方式是无法工作的:

  >>> re.search(r'<p>.*</p>', paragraph)
  >>>

上面的搜索正则表达式的问题是,特殊字符 "." 是不能匹配换行符 \n 的。

这个问题很容易解决,re 包的查询方法有一个接受预定义的常量的可选参数,可以用来定义特殊字符的行为。常量 re.DOALL 可以让特殊字符 '.' 匹配任意字符,包括换行符 \n,比如:

  >>> match = re.search(r'<p>.*</p>', paragraph, re.DOTALL)
  >>> match.group(0)
  '<p>\nThis is a paragraph.\nIt has multiple lines.\n</p>'
  >>>

使用 re.DOTALL 常量,我们完美的实现了目标,我们可以匹配跨越多行的内容了。

另一个可能遇到的情况是,当处理多行文本时,我们洗完针对某一特定行的开始和结束来进行匹配。继续使用上面的段落示例,我们希望匹配第三行的文字("it has multiple lines")。然而,下面的方式同样不能工作:

  >>> re.search(r'^It has.*', paragraph)
  >>>

在 Python 正则表达式中,使用 ^$ 来分别匹配整个字符串的行首和行尾。幸运的是,这里同样有方法来修个这个行为。常量 re.MULTILINE 可以让 ^& 分别匹配整个字符串中每行的行首和行尾,参考下面代码:

  >>> match = re.search(r'^It has.*', paragraph, re.MULTILINE)
  >>> match.group(0)
  'It has multiple lines.'
  >>>

很棒吧!

2. 贪婪和非贪婪匹配

有些时候,如果我们不谨慎的使用特殊字符,我们的正则表达式返回的内容会多余我们所需要的内容。

这是因为,默认情况下正则表达式使用贪婪算法(尽可能多的匹配)。考虑下面例子:

  >>> htmlSnippet = '<h1>This is the Title</h1>'
  >>>

如果我们希望正则表达式仅匹配 html 标签,我们不妨先试试下面的方法:

  >>> re.findall(r'<.*>', htmlSnippet)
  ['<h1>This is the Title</h1>']

然而,这不是我们想要的,它匹配了整个字符串。

这是展示正则表达式默认为贪婪算法的很好的一个例子。表达式的 .* 部分会尽可能多的匹配内容。我们可以在扩展元字符(*,+ ...)后紧跟一个 *? 让 Python 正则表达式不要使用默认的贪婪算法(找到特定的内容后不继续匹配后续内容而直接返回匹配项):

  >>> re.findall(r'<.*?>', htmlSnippet)
  ['<h1>', '</h1>']

* 后紧跟一个 ?,我们可以使正则表达式匹配尽可能少的内容,我们得到了正确的结果。

3. 使用正则表达式进行替换

re 模块的另一个常用功能是,使用正则进行文本替换。可以像我们之前使用其他查询方法一样使用 sub() 方法,不过我们可以在这个方法中将匹配的内容替换为其他字符串。比如:

  >>> re.sub(r'\w+', 'word', 'This phrase contains 5 words')
  'word word word word word'

上面正则表达式将每个匹配替换为 'word'。你当然也可以用分组来访问素有的匹配(前面的文章讨论过分组):

  >>> re.sub(r'(?P<firstLetter>\w)\w*', r'\g<firstLetter>', 'This phrase contains 5 words')
  'T p c 5 w'

这个例子中,我们捕获每个字的首字母放到分组 'firstLetter' 中,然后用分组 \g<name> 的内容替换所有匹配项。如果我们没有使用命名分组,我们可以使用分组的需要完成同样的事情:

  >>> re.sub(r'(\w)\w*', r'\g<1>', 'This phrase contains 5 words')
  'T p c 5 w'

有些时候,我们无法使用一行正则表达式完成字符串替换,我们需要更复杂的方式来完成。

sub 方法可以接受一个函数来代替之前的替换字符串。这个函数需要一个代表匹配对象(Match 对象)的参数并且返回替换后的字符串。sub() 方法会针对每一个匹配调用这个函数,并且使用函数的返回值替换原字符串。

下面我们将编写一个函数来演示这个功能,我们的函数允许我们将任意字符串转换成适合 url 的形式(比如:全部转换为小写,将所有特殊字符转换为 '_'):

  >>> def slugify(matchObj):
  ...  matchString = matchObj.group(0)
  ...  if matchString.isalnum():
  ...    return matchString.lower()
  ...  else:
  ...    return '_'
  ... 
  >>>

现在我们可以使用 slugify 函数来转换任意字符串了。我们的正则表达式会匹配连续字符或连续空格(| 元字符在正则表达式中代表 '或'' 的意思。为了能个正确匹配到,内容必须至少匹配左边或右边的任意模式)。sub() 方法会将每个匹配对象传递到 slugify() 函数中:

  >>> re.sub(r'\w+|\s+', slugify, 'This iS a   NAME')
  'this_is_a_name'

注意:我们试讲 slugify 函数的引用传递到 sub() 方法中的(我们并没有调用这个函数)。记住,sub() 方法会针对每个匹配项调用 slugify 函数。

花几分钟来理解上面的例子,这个例子并不仅仅介绍了正则表达式的 sub() 方法,还介绍了 Python 的函数式编程。Python 中函数式一等公民(实际上,Python 中函数就是对象)

当然,Python中 re 模块的手册 还是要多看看的。

总结

这篇文章我们稍微深入介绍了一些 Python 正则表达式,我们学习了如何通过 re.DOTALLre.MULTILINE 常量来改变一些元字符的行为,使得我们可以处理多行文本。也讨论了正则表达式的贪婪算法,以及使用 ? 改变正则表达式为非贪婪算法。最后,我们讨论了使用正则表达式通过字符串或替换函数来替换字符串。

本文链接:http://www.4byte.cn/learning/88474/shen-ru-python-zheng-ze-biao-da-shi-python-zheng-ze-biao-da-shi-de-huan-xing-chu-li-ti-huan-tan-lan-yi-ji-fei-lan-tan-lan-suan-fa.html