学习正则表达式
学习正则表达式
本文写于 2020 年 9 月 8 日
“抄自”正则表达式 30 分钟入门。
最重要的是——请给我 30 分钟,如果你没有使用正则表达式的经验,请不要试图在 30 秒内入门——除非你是超人 😃 —— 出自原文
如果你用过 Windows,你很可能见过这样一个东西:*.doc
。这是用来匹配所有的 doc 文档的,*
叫做「通配符」。顾名思义,就是所有以 .doc
结尾的文件,通通匹配。
正则表达式与这类似,即通过符号规则,对字符串进行更为精确的匹配。
寻找一个单词
he
,这就是一个非常简单的正则表达式,但他匹配的是 he
,hello
, hey
, her
, he
, she
......真的不太精确。
我们如果只想匹配 he
,就需要加上一些条件:\bhe\b
。
这里的 \b
就是一个正则表达式的规则(叫做元字符),代表单词的开头或者结尾。
需要注意的是,这里 \b
匹配的并不是空格或者逗号之类的字符,而仅仅是匹配单词的开头、结尾的「位置」。
寻找两个相邻单词
我们想要找到一句话中,相邻的两个单词:hello world
。这该怎么做呢?
使用 \bhello\b *\bworld\b
。
别急着晕!这其实并不难。
前半部分和后半部分我们都认识,两对 \b
分别框住了 hello 与 world,是刚刚学到的匹配单词始末的符号。
那么问题就是中间的 *
,空格我们不陌生,主要就是这个 *
。
*
是元字符(\b
就是元字符),但它跟前面的两者不同——它不代表符号,也不代表位置,代表的是数量。它意味着在自己之前的内容可以连续重复任意次数,使得整个表达式得到匹配。
也就是说这就意味着我们可以匹配 hello world
, hello world
, hello 一百万个空格 world
!中间任意多的空格我们都可以忽略。
+
也有同样的作用,但与 *
不同的是,+
号只能匹配重复一次或以上;*
则可以匹配 0 次。
寻找首字母
我想寻找 a 开头的字符串该如何?
\ba\w*\b
即可。\w
顾名思义,代表一个 word。我们寻找「字符起始-a-任意个任意字符-结束」就可以找到 a 开头的字符串了。
寻找始末
经常会有 start 啦啦啦,啦啦啦 end
这样的段落,前面一个标签、后面一个标签。
如果我们想要匹配这两个标签以及之间的所有东西该怎么办?
用 .
,\bstart\b.*\bend\b
即可。
.
也是一个元字符,它匹配的是除了换行符意外的任意字符。比如字母 a
, b
;符号 &
, 。
, !
;空格
......他都能匹配。
比如 .*
就可以匹配整段文字!因为 .
代表非换行符,*
代表重复任意次数,那么此时就会匹配最大次数的非换行符,也就是整段文字。
对于这里,就会匹配 start 与 end 以及中间的所有字符。
我与重复不共戴天
\b
表示一个数字,那我如果需要匹配电话号码,岂不是需要 \d\d\d\d\d\d\d\d\d\d\d
?
当然不可以。
我们可以写成 \d{11}
就可以了。
有时候我们需要模糊匹配,例如昵称不得超过 12 个字符,但也不得少于 3 个字符,我们就可以使用 ^.{3,12}$
了。逗号后不要加空格
这里 ^
代表句子开头,$
代表句子结尾,.
代表任意换行符外的字符,{3,12}
代表 .
匹配的字符数量为 3 到 12 个。
那我们来匹配 QQ 号试一试吧。
对于 QQ 号而言,首先有一个最短位数,姑且当他是 5 吧。其次 QQ 号不可以以 0 开头。
那我们开头就可以写 [1-9]
,后面写上 \d{4,}
就可以了。
这里的 []
可以理解是一个集合。你想要匹配什么符号,放进去就可以了。
例如 [,!?]
可以匹配三个符号,[0-9]
的意思和 \d
是一样的。
但这只是单个符号的重复,我们希望几个符号成组的重复,就需要 (\w\d-5003){5}
这种写法了。
他的意思就是,将「一个字母 一个数字 -5003」重复五次。
两种情况
如果需要同时匹配多个国家的手机号码,需要怎么办呢?
正则A|正则B
就可以了。不要带空格
我不要 xxx 情况
我们还会经常遇到一个问题,比如:不带 a 字符的单词。
正则表达式当然可以轻易的完成这种需求,例如 \W
匹配任意不是字母,数字,下划线,汉字的字符;\S
匹配任意不是空白符的字符;\D
匹配任意非数字的字符\B
匹配不是单词开头或结束的位置;[^x]
匹配除了 x 以外的任意字符;[^aeiou]
匹配除了 aeiou 这几个字母以外的任意字符。
我需要重复的两个字母
这个和之前的 {2}
可不一样,对于 {2}
而言,我们是无法判断两个重复的任意字符的。
这个时候就需要后向引用了。
后向引用用于重复搜索前面某个分组匹配的文本。什么意思呢?
还是看标题的问题,我们如何用正则做到这一点?
([a-zA-Z])\1
。
首先不看小括号,[a-zA-Z]
指匹配一个字母,用括号扩起来就是一个「组」。整个正则从左到右,由小括号扩起来的就是一个组,编号是从 1 开始计算。(分组 0 对应整个正则表达式)
我们用 \1
表示之前组 1 匹配到的东西——也就是组 1 匹配到的字符。这就重复了一个字母两次了。
当然这样一个个数组数是很痛苦的,而且不可读。正则表达式可以通过 (?<组名>正则表达式)
将该组命名为尖括号内的名字了。(也可以用 ''
代替 <>
)
零宽断言
我们会经常去查找在某些内容之前或之后的东西,但并不包括该内容,类似于 \b
^
$
。
(?=表达式)
也叫零宽度正预测先行断言,是不是看不懂?他的意思就是我要匹配:该条件出现的位置的后面能匹配表达式。
还是看不懂?
例子来了:匹配以 ing 结尾的单词,但是不要 ing。
\b\w+(?=ing\b)
。
现在理解了吧,也就是我们将不想要的结尾,丢到 (?=)
里面,就可以即匹配到,又不带上它。
除了结尾,我们还有开头:(?<=exp)
,也叫零宽度正回顾后发断言。
还是例子,我们需要匹配 re 开头的单词,但我不要 re。
(?<=\bre)\w+\b
。
我们将不想要的开头,丢到 (?<)
里面,就可以即匹配到,又不带上它。
同样,和「反义」一样,我们一样拥有负向零宽断言,可以确保某个字符没有出现,但并不去匹配它。
(?!表达式)
\d{3}(?!\d)
即为匹配三位数字,并且这三位数字的后面不能是数字。
(?<!exp)
(?<![a-z])\d{7}
匹配前面不是小写字母的七位数字。
贪婪与懒惰
当正则表达式中包含能接受重复的限定符时,他会去匹配尽可能多的字符。
以这个表达式为例:a.*b
。
它将会匹配最长的以 a 开始,以 b 结束的字符串。
如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab。
这被称为「贪婪匹配」。
但有时,我们更需要「懒惰匹配」,也就是匹配尽可能少的字符。
前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。
这样 .*?
就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
例如 a.*?b
匹配最短的,以 a 开始,以 b 结束的字符串。
如果把它应用于 aabab 的话,它会匹配 aab(第一到第三个字符)和 ab(第四到第五个字符)。
我们还可以用到重复上,{n,}?
,意为重复 n 次以上,但尽可能少重复。
注释
(?#comment)
,小括号加上 ?# 就能写上注释。
实践一下
如何匹配身份证号?
^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$
我们如何匹配 email 地址?
[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)_@(?:[\w](?:[\w-]_[\w])?\.)+[\w](?:[\w-]*[\w])?
我们如何匹配网址?
[a-zA-z]+://[^\s]*
(完)