正则表达式 - 交互式学Python | 莫烦Python

正则表达式

作者: 莫烦 编辑: 莫烦 2021-05-09

在文件处理和文字处理的时候,少不了要根据特定规则来处理对应文字的情况。 正则表达式 regex 就是为了让你尽情匹配文字,用一些规则或者模板来帮你找到文字,替换文字的工具。

在我们假定的文件管理系统当中,当然也少不了正则,比如我要批量修改很多文件中出现某种固定模式的文字时。 举个例子,我在迁移莫烦Python网页的时候,教程里的网址描述通通都要修改,如果我是打开文件一个个修改, 我得花一天的时间改完这 3-400 篇的教学。而用正则,我只要设定一个字符串的匹配方案, 用一个 for 循环,加上开发,轻松 1 小时搞定。这就是正则表达式的威力。

不用正则的判断

当你还没有接触到正则表达式的时候,如果碰上要在文字中寻找某个信息时,你很可能会做下面这样的判断与尝试。

这样好用是好用,也能解决燃眉之急,不过种类变多了之后,总写这样的判断,处理的能力是十分有限的。正则表达式就来拯救你了。

再举个例子,你的文件系统如果要做注册管理,那么你肯定会要用正则表达式的,比如验证用户的邮箱是否有效。 使用正则做一个通用的邮箱地址判断,就是正则最一般的能力。下面我就举个简单的邮箱判断例子,你可以先不管下面的代码是怎么工作的, 因为我后面会详细介绍,你就点击看看会有什么效果吧。

正则给额外信息

正则除了帮你判断有没有某个 pattern 模式,你还可以做很多事情,上面的例子也显示,如果你使用正则识别出一个模式的字符串, 那么它返回的远不止一个 True,而是很多额外的信息。比如上面的 email 判断。

先不管我写的那些正则 pattern 规则,你可以看到在返回的信息中,有一个 span=(13,30),有一个 match='mofan@mofanpy.com 这类的信息。 他们分别代表着在原始字符串中,我们找到的 pattern 是从哪一位到哪一位,pattern 找到的具体字符串又是什么。下面我们再来看看一个简单的例子。

我们看到了很多次,我都是用 r"xxx" 来写一个 pattern,这是为什么呢?因为正则表达式很多时候都要包含\r 代表原生字符串, 使用 r 开头的字符串是为了不让你混淆 pattern 字符串中到底要写几个 \,你只要当成一个规则来记住在写 pattern 的时候,都写上一个 r 在前面就好了。

你想必会思考,我拿着这个 match 变量,改怎么使用呢?怎么才能拿出来里面匹配好的字符串呢?目前这个阶段,你就记着用 .group() 能取出来里面找到的字符。 我们后面在详细介绍 group 的功能

上面的识别 run 的例子很简单,如果要被判断的字符串中有 run,你可以在字符串中找到 run 的位置。

同时满足多种条件

在上面匹配 run 字符中,我们看到了可以在字符串中找固定的词,如果我想让这种匹配模式多样化,一种模式包含多种字符的可能呢? 你首先可能尝试的是下面这种方法。比如判断中我们不区分它是不是过去式。

正则更厉害的一个地方就是它能把这两种情况写在一个判断里。只需要使用一个 | 就可以搞定啦。这个 | 就代表或者的意思。

我们观察,在 runran 之间,其实它们的差别就只是中间这个字母,我们还能使用 [au] 来简化一下。 让它同时接受中间字母是 a 或者 u 的情况。

还有一种情况,就是我的前后都是固定的,但是我要同时满足多个字符的不同匹配,比如我想同时找到 findfound。我该怎么办呢?

按类型匹配

上面的匹配模式还是有局限性的,比如如果我想找 email 里面的固定模式,我总不可能把所有的 email 地址都写进去当做模式吧。 当然还需要有更好的通用匹配方式。下面我们就来介绍一些通用匹配方式。

特定标识 含义 范围
\d 任何数字 [0-9]
\D 不是数字的
\s 任何空白字符 [ \t\n\r\f\v]
\S 空白字符以外的
\w 任何大小写字母,数字和 _ [a-zA-Z0-9_]
\W \w 以外的
\b 匹配一个单词边界 比如 er\b 可以匹配 never 中的 er,但不能匹配 verb 中的 er
\B 匹配非单词边界 比如 er\B 能匹配 verb 中的 er,但不能匹配 never 中的 er
\\ 强制匹配 \
. 匹配任何字符 (除了 \n)
? 前面的模式可有可无
* 重复零次或多次
+ 重复一次或多次
{n,m} 重复 n 至 m 次
{n} 重复 n 次
+? 非贪婪,最小方式匹配 +
*? 非贪婪,最小方式匹配 *
?? 非贪婪,最小方式匹配 ?
^ 匹配一行开头,在 re.M 下,每行开头都匹配
$ 匹配一行结尾,在 re.M 下,每行结尾都匹配
\A 匹配最开始,在 re.M 下,也从文本最开始
\B 匹配最结尾,在 re.M 下,也从文本最结尾

再回到 email 匹配的例子,我们就不难发现,我用了 \w 这个标识符,来表示任意的字母和数字还有下划线。因为大多数 email 都是只包含这些的。 而且我还是用了 +? 用来表示让 \w 至少匹配 1 次,并且当我识别到 @ 的时候做非贪婪模式匹配,也就是遇到 @ 就跳过当前的重复匹配模式, 进入下一个匹配阶段。

除了邮箱在我们的文件管理系统中常见,我们再举一个常见的电话号码的识别例子。比如手机号的识别,假设我们只识别 138 开头的手机号码。 下面的 \d{8} 就是用来表示任意的数字,重复 8 遍。

中文

我们是汉字的使用者,当然在正则中少不了汉字识别,下面我来用上面学到的东西举几个例子,你看看就差不多明白汉字的用法。

哈哈,看起来汉字识别也挺简单的呀。不过你有没有想过,我怎么才能把汉字全部匹配出来呢?就像 r"[a-zA-Z]" 识别出所有英文那样? 当然也可以,汉字通常是用 Unicode 来表示的,如果把汉字变成 Unicode,我们可以看到汉字在计算机中裸体的样子。

你看到 字用 Unicode 表示的样子后,会不会突然发现,这不就是一串英文吗?只要我把汉字的 Unicode 全写出来就好了, 好在 Unicode 是可以连续的,我们可以用英文那样类似的办法来处理。

这挺符合我们的预期,它将里面的中文都识别出来了,剔除掉了英文和标点。那有时候我们还是想留下对标点的识别的,怎么办? 我们只需要将中文标点的识别范围,比如 [!?。,¥【】「」] 补进去就好了。

查找替换等更多功能

当然,如果正则只有 re.search 那么它就弱爆了。幸好写 Python 的人也没那么傻,他们还开发了多种多样的正则功能,方便我们的使用。 比如你可以用正则直接找到所有匹配模式的字符,用正则分开字符,替换字符等。我们在下面的表里具体看看。

功能 说明 举例
re.search() 扫描查找整个字符串,找到第一个模式匹配的 re.search(rrun, I run to you) > 'run'
re.match() 从字符的最开头匹配,找到第一个模式匹配的即使用 re.M 多行匹配,也是从最最开头开始匹配 re.match(rrun, I run to you) > None
re.findall() 返回一个不重复的 pattern 的匹配列表 re.findall(rr[ua]n, I run to you. you ran to him) > ['run', 'ran']
re.finditer() 和 findall 一样,只是用迭代器的方式使用 for item in re.finditer(rr[ua]n, I run to you. you ran to him):
re.split() 用正则分开字符串 re.split(rr[ua]n, I run to you. you ran to him) > ['I ', ' to you. you ', ' to him']
re.sub() 用正则替换字符 re.sub(rr[ua]n, jump, I run to you. you ran to him) > 'I jump to you. you jump to him'
re.subn() 和 sub 一样,额外返回一个替代次数 re.subn(rr[ua]n, jump, I run to you. you ran to him) > ('I jump to you. you jump to him', 2)

在模式中获取特定信息

对,还有很多使用方法,用于处理更个性化的情况,比如我有一个正则模式,想要提取出匹配模式当中的一些字段,而不是全字段。举个例子, 我的文件名千奇百怪,我就想找到 *.jpg 图片文件,而且只返回给我去掉 .jpg 之后的纯文件名。如果用上面学到的方法,我们可以这样做。

上面这种做法虽然可行,但是还不够简单利索,因为同时用到了两个功能 finditersub,正则还可以更简单, 我们能用 group 获取到特定位置的信息。上面的例子中已经使用了 group,但是我们还没有发挥 group 的全部力量。 在介绍 group 之前我在介绍一下和 group 一起用的 ()。只要我们在正则表达中,加入一个 () 选定要截取返回的位置, 他就直接返回括号里的内容。

那如果我想获取更详细的信息呢?比如年月日分开获取?答案是多做几个括号就好了,然后用 group 功能获取到不同括号中匹配到的字符串。

我还提供你另一种途径实现上面的功能,你看看,下面这个 findall 也可以达到同样的目的。只是它没有提供 file.group(0) 这种全匹配的信息。

有时候我们 group 的信息太多了,括号写得太多,让人眼花缭乱怎么办?我们还能用一个名字来索引匹配好的字段, 然后用 group("索引") 的方式获取到对应的片段。注意,上面方案中的 findall 不提供名字索引的方法, 我们可以用 search 或者 finditer 来调用 group 方法。为了索引,我们需要在括号中写上 ?P<索引名> 这种模式。

多模式匹配

上面我们有提到过 re.M 这个标识,但还没有具体讲解。在正则中还有一些特别的 flags,可以在 re.match(),re.findall() 等功能中使用。 主要目的也是方便我们编写正则,和用更简单的方法处理更复杂的表达式。

模式 全称 说明
re.I re.IGNORECASE 忽略大小写
re.M re.MULTILINE 多行模式,改变'^'和'$'的行为
re.S re.DOTALL 点任意匹配模式,改变'.'的行为, 使".“可以匹配任意字符
re.L re.LOCALE 使预定字符类 \w \W \b \B \s \S 取决于当前区域设定
re.U re.UNICODE 使预定字符类 \w \W \b \B \s \S \d \D 取决于unicode定义的字符属性
re.X re.VERBOSE 详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。以下两个正则表达式是等价的

下面我们举几个例子。第一个是 re.I 忽略大小写的例子。

第二个我们想在每行文字的开头匹配特定字符,如果用 ^ran 固定样式开头,我是匹配不到第二行的 ran to you 的,所以我们得加上一个 re.M flag。 注意我们提到过的 re.search()re.match() 有一丢丢不一样,re.match() 是不管你有没有 re.M flag,我的匹配都是按照最头头上开始匹配的。 所以在下面的实验中,re.match() 匹配不到任何东西。

如果你想用多种 flags,也是可以的,比如我想同时用 re.M, re.I,你只需要这样书写re.M|re.I

其实还有一种写法可以直接在 ptn 里面定义这些 flags,有的人会比较喜欢下面这样的写法,在模式 ptn 的开头,著名我要采用哪几个 flags: (?im) 这就是说要用 re.I, re.M

还有很多其他的 flags 没有讲到,你可以自己在执行框里试一试。

更快的执行

如果你要重复判断一个正则表达式,我们通常不会直接在 re.search(ptn) 这里里面写 ptn,而是在外面先定义要,解析好一个正则 pattern,然后直接用这个 pattern 循环执行查找。 这样可以更有效率,比如你要重复查找 100 万次,我们先 compile 正则再查找能节省可观的时间。


降低知识传递的门槛

莫烦很常从互联网上学习知识,开源分享的人是我学习的榜样。 他们的行为也改变了我对教育的态度: 降低知识传递的门槛免费 奉献我的所学正是受这种态度的影响。 通过 【赞助莫烦】 能让我感到认同,我也更有理由坚持下去。