Python 的 re 模块内置函数几乎都有一个 flags 参数,规定了正则匹配时的各种策略模式,其中有两个模式:单行(re.DOTALL, 或者re.S)和多行(re.MULTILINE, 或者re.M)模式。本文简单介绍下这两种模式的用法和使用场合。
TL;DR
太长不看版:
- 单行模式和多行模式,都增强了对多行文本的解析能力。\
- 单行模式突破换行符 \n 的阻碍,将匹配视野扩大到整个字符串\
- 多行模式实现换行符 \n 的分隔,将匹配视野缩小到一行之内,并且按行分别匹配。
文件的保存形式:字节流
首先我们需要清楚,你看到的多行文本在视觉上是二维的,有行和列的表达:比如我们新建一个文件,名字叫 a.txt
,内容是:
cat
dog
用文本编辑器打开,看到的是两行文字,每行有三个字符,所以这是个二维的表示。
但是,文件的保存形式只能是一维的字节流,之所以能让编辑器表示为二维形状,都是因为字节流中包含了换行符。
我们用 xxd
命令看下这个 a.txt
文件的二进制表达:
$ xxd -b a.txt
0000000: 01100011 01100001 01110100 00001010 01100100 01101111 cat.do
0000006: 01100111 00001010 g.
这就是文件 a.txt
真实保存的形式,其中 00001010
就是换行符。
为了能看得更清楚,用16进制查看文件:
$ xxd a
0000000: 6361 740a 646f 670a cat.dog.
Linux环境下, 0a
就是换行符,在编程语言里用 \n
表示,不同操作系统下的换行符有少许不同,见这篇文章 Python 换行符和多行模式
结论:一段多行文本,尽管在文本编辑器中显示为二维的形状,但是在正则表达式解析器看来,文件是一维的字符串。在碰到包含换行符的字符串时,有多种匹配模式,分别能得到不同的结果。
在线测试工具
首先我们使用一款在线工具以得到感性认识。(建议在 PC 端 Chrome 浏览器中使用,其他浏览器效果不能保证)。
复制下面一段多行文字:
空山新雨后,
天气晚来秋。
明月松间照,
清泉石上流。
并粘贴到下方文本框中:
在普通匹配模式 ".+"
下,得到如下的结果,匹配到第一行末尾即停止。
空山新雨后,
单行匹配模式 ".+", re.DOTALL
下,得到如下结果,可见匹配出了包括换行符在内的所有字符
空山新雨后,\n天气晚来秋。\n明月松间照,\n清泉石上流。
在多行匹配模式 "^.+$" re.MULTILINE
下,得到如下结果,可见匹配结果本身也是多行:
空山新雨后,
天气晚来秋。
明月松间照,
清泉石上流。
普通模式
正则表达式(Python2,Python3)里, 点号(.)能匹配除换行符以外的所有字符。文档中的原话是:
当用 .*
匹配到换行符的前面时,匹配即停止。例如下面这样的字符串:
This is the first line.
This is the second line.
This is the third line.
直接使用 .*
匹配,匹配过程如下面图所示(点击左上角的"点击播放"):
执行代码的例子,如下面的执行过程。从匹配结果可以看出来,仅有第一行出现在结果里,而且不包含换行符。
>>> a = 'This is the first line.\nThis is the second line.\nThis is the third line.'
>>> print a
This is the first line.
This is the second line.
This is the third line.
>>> import re
>>> p = re.match(r'This.*line\.' ,a)
>>> p.group(0)
'This is the first line.'
>>>
结论:
单行模式 re.DOTALL
在上面的例子里,即使是默认的贪婪(greedy)模式,仍然在第一行的结尾初停止了匹配。如果想完整匹配出字符串,就需要进入 单行模式 。 Python 文档中这么描述:
在单行模式下,匹配的行为模式如下图,点击左上角的"点击播放"查看匹配过程:
从上面的动图里可以看出,当使用 re.DOTALL
时,点号将同时匹配换行符,实现了跨行匹配。代码的执行过程如下,从下面的记过可以看出,匹配结果里包含了换行符
\n
和 全部的三行。
>>> q = re.match(r'This.*line\.', a, flags=re.DOTALL)
>>> q.group(0)
'This is the first line.\nThis is the second line.\nThis is the third line.'
结论:
这里引申出比较奇怪的结论就是:为了能匹配出多行文本,你应该使用单行模式。
多行模式 re.MULTILINE
有时候我们想找出一篇文章里符合特定条件一共有几行。比如在下面的例子里,我们希望找出 所有 以 This 开头,以 line 结尾的行。
>>> a = 'This is the first line.\nThis is the second line.\nThis is the third line.'
>>> print a
This is the first line.
This is the second line.
This is the third line.
>>> import re
>>> re.findall(r'^This.*line\.$', a)
[]
>>>
匹配结果为空的原因是:从 上一节
我们知道,点号不匹配换行符,最多只能匹配到第一个 line,但是第一个 line 后面并没有行尾符 $
,假如我们改用 单行模式
>>> re.findall(r'^This.*line\.$', a, flags=re.DOTALL)
['This is the first line.\nThis is the second line.\nThis is the third line.']
>>>
匹配模式中的 line 就匹配到了第三个line,结果就是匹配出了整个字符串,但这并不是我们想要的, 因为原字符串的三行都满足匹配条件,我们希望有三条结果。
再用问号 ? 切换成非贪婪模式试试:
>>> re.findall(r'^This.*?line\.$', a, flags=re.DOTALL)
['This is the first line.\nThis is the second line.\nThis is the third line.']
>>>
仍然是整个字符串
真正的原因是因为正常情况下,行首符 ^
和行尾符 $
仅仅匹配整个字符串的起始和结尾。Python
文档中这么描述:
为了扩大 ^
和 $
的匹配范围,引入了多行模式。在这种模式下:
- 将换行符
'\n'
后面的位置也看作行首,可以用^
匹配 - 将换行符
'\n'
前面的位置也看作行尾,可以用$
匹配
点击下图左上角的"点击播放",观看多行模式的匹配过程:
Python 文档中这么描述:
再回到前面的例子中,使用多行模式得到的结果如下:
>>> re.findall(r'^This.*line\.$', a, flags=re.MULTILINE)
['This is the first line.', 'This is the second line.', 'This is the third line.']
结论:
当需要在一个文本文件里跨行匹配时,单行和多行模式尤其有用。
二者不冲突
单行模式和多行模式,从名字上看是互斥的,但是实际上,两者可以共存。因为它们二者分别扩展不同的匹配符:点号
.
和 ^
、 $
如果您对本文有疑问或者寻求合作,欢迎 联系邮箱 。邮箱已到剪贴板
精彩评论
本站 是个人网站,采用 署名协议 CC-BY-NC 授权。
欢迎转载,请保留原文链接 https://www.lfhacks.com/tech/python-re-single-multiline/ ,且不得用于商业用途。