轻松入门正则表达式

楔子

为什么要写这篇文章?

最近团队内有小伙伴在阅读vue的源代码,然后表示代码中有很多正则表达式的应用,而自己却对正则不太熟悉。后来我发现团队内其他的小伙伴也都表示正则表达式太难了,难学难用又难看懂。所以,正则表达式真的这么难吗?我的答案是:不是难,是真TM难。我自己这么些年也是把正则表达式前前后后学了起码五六遍才算是基本能熟练运用它。可是即便如此,我在准备这篇文章的过程中还是学到了一些新的东西,可见这玩意儿是有多么的反人类了。

既然正则表达式这么难,那我们还要去认真学习它吗?

是的,因为正则表达式功能十分强大,它能极大地提高我们的工作效率和开发效率,所以作为一名开发人员,我们必须熟练掌握它。

那学习正则表达式有没有什么好的办法或者好的学习资料呢?

当然有!不然这篇文章写来干嘛?话不多说,我们赶紧开始吧!

本文目标

我学习正则表达式的主要资源就是这篇非常有名的《正则表达式30分钟入门教程》,这篇文章针对正则表达式的知识点写得很详细很全面,我接下来要讲的主要知识点也都来源于它,不同的是,我会着重分享一些正则表达式的记忆技巧来帮助大家快速掌握这门技能。本文的目标是让你快速地、轻松地掌握正则表达式的基本使用,以便能快速应用到工作中去,所以我推荐你先阅读我的这篇快速入门文章,再去仔细研读《正则表达式30分钟入门教程》,以便达到最佳的学习效果。

正文开始

初识正则表达式

简单来说,正则表达式就是一串用于描述文本规则的代码。举个例子,比方说你要在一段文本中搜索数字0,那么你可以直接使用正则表达式0(你没看错,这就是一个最简单的正则表达式),如果你想要搜索任意的数字,这时你可以使用或符号 | 来做到这一点,就像这样 0|1|2|3|4|5|6|7|8|9,略显繁琐?还有一种办法,就是使用方括号 [] 把你要搜索的字符括起来,就像这样 [0123456789],还是觉得麻烦?没问题,还有更简单的办法,就是使用 \d 搜索即可(d代表digit,即数字)。看到这里,其实你已经学习到了正则表达式的三个知识点了,是不是感觉也没那么难:)。分享一个我常用的网站:regex101,它可以让你在线测试正则表达式的效果(网站的左侧设置icon中可以设置中文界面),学习正则表达式一定要亲自测试,所以我强烈推荐你动手操作一把。

元字符(meta character)

你刚刚已经接触到过元字符\d了,正则表达式中还有一些其他的元字符,如下图所示:

常用的元字符

先看元字符\d,它匹配的是0到9的一个数字。再看元字符\w,这个厉害了!它不仅能匹配数字,还能匹配字母和下划线,甚至在正则表达式引擎支持的情况下还能匹配汉字!Wow~Cool~,这里的w意思是word,也就是一个单字,记住word或者Wow~你就记住\w了。接下来是\s,s代表space,它可以匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等,你可以借助死(s)白(b)死(s)白(b)来记住它,或者你想用sb来记住它我也不反对。然后是元字符.,别小看这个不起眼的点,它是最🐂🍺了,前面的三个元字符都只是它的子集而已,它可以匹配除了换行符以外的所有字符。你可以把这个点想象成一个巨大无比的球,它可以在自己的轨道上碾压一切,但就是不可以脱离轨道,就像下面这张图一样:

暴力的点

接下来是\b和^以及$,它们都只描述一个位置,并不匹配任何的字符。以\b为例,它匹配的是单词的开始或者结束,首先需要注意的是,这里的单词并不是英文单词,而是指一个及以上连续的\w,所以准确一点说,\b匹配的是:它的前一个字符和后一个字符不全是(一个是一个不是或者都不是\w)。举个例子,如果在一篇文章里搜索hi这个词的话,你会搜出来him、history、high,如果你用\bhi\b搜的话,就会精准地搜出来hi了。这里的b代表boundary,你也可以使用边(b)界来记住它。最后是^和$了,它们分别代表字符串的开始和结束,举个例子,如果你用abc搜索abcdefabcdef,你会搜出来abcdefabcdef两个结果,但是如果你用^abc搜索的话,就只会搜出来abcdefabcdef一个结果,如果你用^abc$搜索的话,匹配就会失败,因为它只能完完整整地匹配abc这个字符串。你可以用这个故事来记住这两个元字符:一个勤劳的草帽编织手工艺人,每天早上(字符串开始)出门卖草帽(元字符^),晚上(字符串结束)卖完草帽挣了很多钱($)回家。

限定符(重复)

所谓限定符就是用于把前面的表达式重复N次。比方说我们想匹配11位的手机号码,我们可以这样写\d{11},估计你也能猜到,这里的{11}就代表前面的数字\d重复11次。我们在匹配URL协议时可以这么写^https?,这里的问号?代表前面的s可有可无,所以它可以匹配http和https。我们还有很多种限定符可以用,具体请参考以下表格:

限定符(重复)

贪婪与懒惰

正则表达式默认采取贪婪匹配模式,什么是贪婪匹配模式呢?就是在使得整个表达式得以匹配的前提下,匹配尽可能多的字符。举个例子,如果用a.*b来匹配aabab的话,它会匹配整个aabab字符串,而如果在*后面添加一个问号?使其变成a.*?b再去匹配的话,就会启用懒惰匹配模式,此时就会匹配到aabab和aabab了。上面讲的限定符都可以在后面添加一个?号使其变成懒惰匹配模式,如下图所示:

懒惰限定符

字符转义

有时候我们就是想搜索元字符本身的话该怎么办呢?这个时候我们就得用到反斜线\来取消这些元字符的特殊含义了,例如想搜索www.beibei.com的话就得使用www\.beibei\.com这样的表达式,具体规则如下图所示:

字符转义

字符类

字符类刚刚其实我们已经接触过了,就是用方括号 [] 把目标可选字符括起来就OK了,例如[0123456789]就相当于\d,我们还可以指定一个范围,比方说[0-9]就相当于\d,英文字母就是[a-zA-z],而在不考虑中文的情况下,\w就相当于[0-9a-zA-Z_]。

需要特别说明的是,很多元字符在方括号内可以不用转义直接使用,例如[.*+?^$()]可以直接匹配到.*+?^$()中的任意一个字符,如下图所示:

反义

有的时候,我们想搜索不在某个范围内的字符时该怎么办呢?方法很简单,对于\w、\s、\d和\b的反义只需要把字母大写就可以了,例如:\D匹配任意的非数字,\S匹配任意的非空白符。对于字符类的反义只需要在方括号内用^开头就OK了,这时的^指的就是不在该字符类范围内的意思,而不是字符串的开始,例如[^a-z]匹配的就是非小写英文字母。另外,如果^在方括号内的非开头位置,那么它就代表一个普通的^字符,且不需要转义(还记得上一小节刚刚提到的吗?),我们来看看效果:

常用的反义代码

分组

分组简单来说,就是一个用小括号 () 括起来的子表达式。这么做的用途很多,比方说可以在分组后加上限定符使得整个分组重复N次。例如,我们要匹配一个像192.168.0.100这样的IP地址的话,我们就可以这么写 (\d{1,3}\.){3}\d{1,3}。

我们来分析一下(\d{1,3}\.){3}\d{1,3}这个表达式,第一部分(\d{1,3}\.)是一个分组,里面是1 ~ 3位的数字后面跟着一个点,然后是{3}把前面的分组重复了3次,最后是一个1 ~ 3位的数字。匹配效果如下图所示:

分枝条件

分枝条件就是使用或符号 | 来指定几个表达式,只要其中任意一个表达式匹配成功的话,整个表达式也就匹配成功了。比方说你想要找Chris或者Christopher的话,你就可以使用Chris|Christopher去匹配。

另外,使用分枝条件时顺序也是非常重要的。举个例子,\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。

如果你把它改成\d{5}|\d{5}-\d{4}的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

后向引用

从这里开始,情况就稍稍变得麻烦一点了。比方说你想要搜索go go或者kitty kitty这样的有重复情况的字符串,那该怎么办呢?如果只用前面讲到的知识点是无法做到的,这时候就需要用到后向引用了。

正则表达式\b(\w+)\b\s+\1\b即可实现上述需求,我们来分析一下:首先是\b(\w+)\b代表一个左右两边都是边界的单词,然后是\s+匹配不少于1个的空白符,接下来的\1也是我们的重点,它代表前面的\b(\w+)\b中括号内的\w+匹配到的内容,这样就能满足需求了,我们来看看它的效果吧:

解释一下为什么我们可以这么做,正则表达式引擎在解析正则表达式时,会从左到右扫描每一个分组,以左括号出现的顺序依次为它们分配组号1、组号2……依此类推,各个分组捕获的内容会被保存下来,然后就可以使用\1、\2……来引用前面分组捕获的内容了。补充说一句,分组0会保存整个表达式匹配到的字符串。

另外,我们还可以通过(?:exp)这样的方式取消该分组对组号的分配权,这么做有什么应用场景呢?当然有!比方说,我要用JavaScript的正则匹配和提取一个简单URL的协议和域名部分,其中协议部分可以省略,我一开始使用的正则是这样的:((https?):)?(\/\/)?([^?]+)

第一次尝试

尝试后发现,匹配的结果包含0 ~ 4共5个分组,刨去分组0,其实我只需要分组2(https)和分组4(www.beibei.com),分组1(https:)和分组3(//)都是我不需要的,那么这个时候我就可以取消分组1和分组3对组号的分配权,以便更精准地拿到我想要的结果。改过之后的正则是这样的:(?:(https?):)?(?:\/\/)?([^?]+),注意我们给分组1和分组3的括号内添加了?:的前缀,最终的效果如下:

第二次尝试

另外,我们还可以给分组设置自定义的组名,具体方法我就不累述了,请自行参阅《正则表达式30分钟入门教程》的后向引用章节。

注释

正则表达式也可以通过 (?#comment) 这样的语法来添加注释,不过我个人并不建议使用它,因为需要添加注释的正则就够复杂了,再在里面加注释就会让正则变得愈加难懂,所以还是把注释加在外面吧。

零宽断言

正则表达式的内容还有很多,但是在实战中基本上到这里就够用了,所以零宽断言是我们学习的最后一个也是最复杂的一个知识点。

首先,如果你看过其他文章对于它们的描述的话,我敢保证你绝对会被那些拗口的、钢铁直男翻译的名词所打败。什么“零宽度正预测先行断言”、“零宽度负回顾后发断言”……WTF??

可是当你明白了它们的含义,再看看它们的英文单词之后,你可能会有一股想打人的冲动,这么简单的概念为什么被这些人整得这么复杂?这简直比把Socket翻译成套接字更坑爹啊~不扯这些没用的了,我们直接来看它们到底是个啥东东?

简单来说,它们跟\b、^、$一样,只是描述一个位置,但并不匹配任何的字符。例如,我想要查找James这个单词,但是要求James前面是Lebron加一个空格,也就是说我的目标是Lebron James中的James这个单词,如果我直接搜索James的话,很可能搜出来Harry James、Chris James这样我不想要的东西,那么这个时候我就可以使用(?<=Lebron )James这样的正则来进行搜索,站在(?<=Lebron )的角度,它描述的是我要找到Lebron 这样一个位置,它的后面跟着的是James,所以这个表达式就能帮我准确地找到Lebron James中的James了。

站在表达式的角度,按照前面或者后面以及是或者不是,我们有以下四种情况:

Positive Lookahead (?=)  Find expression A where expression B follows: A(?=B)

Negative Lookahead (?!) — Find expression A where expression B does not follows: A(?!B)

Positive Lookbehind (?<=) — Find expression A where expression B precedes: (?<=B)A

Negative Lookbehind (?<!) — Find expression A where expression B does not precedes: (?<!B)A

看它们的英文单词应该就能明白它们的含义了,不过想要记住它们确实有些困难,所以接下来我就分享一下我的记忆技巧:

1、首先是一句询问(?)

2、然后确定位置,你可以把<想象成一个方向箭头,表达式默认放在目标的后面xxx(?),加了方向箭头<就被拉到了目标的前面(?<)target

3、最后确定是要使用等号=还是使用感叹号!。等号=代表“是”的话那么感叹号“!”就代表不是了。这样四种情况我们就能够很容易地记下来了:)

再来看一个完整的例子:

需要特别说明的是,JavaScript早些时候的版本是不支持Positive Lookbehind (?<=)和Negative Lookbehind (?<!)的,也就是带方向箭头<的都不支持,但是我发现Chrome现在也已经支持它们了,但是因为用户的运行环境各异,所以建议还是谨慎使用吧。

写在最后

其实我还有“处理选项”和“平衡组/递归匹配”没有提到,“处理选项”建议直接去《正则表达式30分钟入门教程》中查看,“平衡组/递归匹配”我试了一下发现没有哪个在线测试工具是支持的,所以也就不讲了。

除了我推荐的regex101可以用来在线测试正则表达式之外,还有两个可以以图形化的方式展示正则表达式的网站也很不错,它们分别是:https://regexper.com/https://jex.im/regulex。另外,还有一个不错的交互式学习正则表达式的网站也值得一试:Try Regex

由于时间仓促以及作者能力有限,文中若有任何错误或者未尽之处,恳请留言批评指正,感谢您的阅读~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 正则表达式到底是什么东西?字符是计算机软件处理文字时最基本的单位,可能是字母,数字,标点符号,空格,换行符,汉字等...
    狮子挽歌阅读 2,134评论 0 9
  • 几个正则表达式编辑器 Debuggex :https://www.debuggex.com/ PyRegex:ht...
    没技术的BUG开发攻城狮阅读 4,581评论 0 23
  • 初衷:看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印...
    DCbryant阅读 3,981评论 0 20
  • 版本:v2.3.5 (2017-6-12) 作者:deerchao 转载请注明来源 目录 跳过目录 本文目标 如何...
    readilen阅读 945评论 2 13
  • Module 1 谈论你的人生 1. 人生事件 一生中的大事请用这些词来谈谈一生中的大事动词 名词be ...
    AsaGuo阅读 563评论 0 0