半小时正则表达式入门

正则表达式基础

今天想要学习一些正则表达式的内容,但是找了好几个教程都不怎么说人话,有点看不懂,为整理混乱的思绪,写一写来做个总结吧

简答介绍与吐槽

在学习正则表达式之前我们应当先知道这是个什么东西:一种字符串匹配规则,用于筛选出满足条件的字符串。正则表达式在Java,JavaScript,Python等语言中得到了广泛的支持。但是,我有一点还是想吐槽的,这玩意的符号体系到底是谁想出来的,这是地球人该有的脑回路吗,这玩意的可阅读性简直了

基础语法

首先在学习基础语法前,我们心中要有一个基础的认知,那就是实际上正则表达式就是我们写了一个特殊的字符串,然后交由程序判断这两个字符串是否相等。接下来先看一些基础的概念

  1. 基础使用

    输入任意的字符(下面提到的元字符除外),则程序在字符串中比较查找对应的字符串

  2. 元字符

    元字符即在正则表达式中有特殊含义的字符,类似于各类语言中的关键字,要想使用它们需要本身而不是它们所代表的特殊含义,那么要添加反义符\。元字符包括:

    • .表示除换行符以外的任意字符
    • \d表示任意的数字
    • \s表示任意的空白符,包括空格与制表符(即Tab)
    • \w表示任意的字母,数字或汉字。
    • ^表示一个字符串的开始
    • $表示一个字符串的结尾
    • \b表示单词边界,即一个空格后下一个字母前的位置

    应用示例:^\d\d\d\$这样的正则表达式所表示的是一个只包含三个数字的字符串,例如111,123,432

    1\d\d所表示的是一个任意的字符串中包含一个以1开头的三位数的地方

  3. 反义字符

    反义字符指的是与上面的元字符含义相反的字符,例如\d表示所有数字,它的反义字符即\D表示所有非数字

    将元字符中的字母由小写改为大写即可获得对应元字符的反义字符

  4. 限定字符

    所谓的限定符实际上是一种简写,用来表明同一字符的出现次数

    • *表示零次或更多次,例如b*就相当于A在正则表达式中写零个或任意多个b(或者说此时同时存在了所有包含某个特定数量的b的狮子)
    • +表示一次或更多次
    • ?表示一次或零次
    • {n}即表示写n次对应的字符
    • {n,}表示写至少n次对应的字符
    • {n,m}表示写n到m次对应的字符。
  5. 范围表达

    在实际情况中,元字符所表示的字符并不能完全满足我们的需求,我们还有一些特殊的限定需求,这时我们就要求使用范围表达[]

    例如[1-9]表示数字1到9,[A-Z]表示大写字母A到Z,[acde]表示字母a,c,d,e中的任意一个,跟进一步的,我们可以这样写

    [a-ks1]即表示满足方括号内的任意一个,包括a到k的小写字母,还有小写字母s,数字1

    同时,如果稍作修改,加上^就能使范围表达反义,如[^asd]表示不是asd的所有字符

    应用实例:

    如果我们要求用户输入自己的邮箱,我们该如何判断它的格式是否正确呢,首先,我们要考虑一个邮箱的格式一般满足如下格式

    xxxxxxx@xx.xxx,查阅资料可知,@之前的部分除了常见的字母与数字之外还允许包含._%+-,@后的部分一定为xx.xxx

    1
    ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

    我们来分解一下这段表达式的含义

    • ^表示字符串的开始,即从字符串的开始就进行比对,而不是筛选字符串中的某一段是否满足
    • [a-zA-Z0-9._%+-]+表示的是至少出现一个的方括号中所要求的字符
    • @即在出现完上一条中所说的任意字符串后应该接一个@
    • [a-zA-Z0-9.-]+表示@之后应当出现方括号内所要求的字符至少一个
    • \.表示反义的点,即我们日常使用的句号.而不是元字符中的.
    • [a-zA-Z]{2,}表示点之后的邮箱的最后一部分
    • $表示字符串结束,后面不会再有任何东西了

    怎么样,将乱七八糟的正则这样拆一次后是不是显得清楚的多了,但这只是正则的入门使用而已,还有很多更复杂的用法

分支与分组

尽管正则表达式已经提供了很强大的筛选能力,但是我们仍然很难至用一个表达式将允许发生的情况将清楚,这时我们可以使用分支来完成呢

  1. 分支

    所谓的分支,即允许满足多个正则表达式中的一个即可,使用|表示,

    应用实例:我们现在需要筛选出一个字符串中的所有的座机号码,但是查阅资料发现,这个字符串中部分号码是以这样的方式

    (012)12345678的形式保存的,还有一些则是010-12345678的格式,那么该怎么写呢

    1
    \d{3}-\d{8}|\(\d{3}\)\d{8}

    在这个正则表达式中提供了两种匹配模式即数字加-加数字与括号内包含三位数,其后再加8为数,当然如果需要的话,你也可以添加更多的分支。同时注意,()同样为元字符,需要是用\进行转义,它的具体作用就是下面的分组

  2. 分组

    所谓的分组,其实就是加括号,对,就是数学运算中的加括号,比如所(\d\w){2}就是对\d\w执行{2},其效果等价于\d\w\d\w,很好理解是吗,那么接下来看一个稍微难一点的例子:

    识别ip地址:ip地址的规则要求有四个0到255的数组成,每个数之间以.连接

    1
    ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

    由于正则表达式中不存在任何的计算能力,所以我们只好通过复杂的分支与分组进行实现,这里我不做过多的解释,相信你可以理解

懒惰匹配与贪婪匹配

所谓的贪婪匹配,即要求筛选出符合要求的尽可能长的字符串,例如对于一个随机的字符串

1
assdfbasdffggb

使用贪婪匹配

1
a.*b

这时的返回值应当有两种可能,即assdfb或者assdfbasdffggb,但是由于贪婪匹配的原则,它将会返回后者

如果我们希望返回前者呢,有一下这些常用语法

1
2
3
4
5
a.*?b重复任意次,但选择最短的
a.+?b重复任意次,最少一次,但只选则最短的
a.??b只重复零次或一次,选择最短的
a.{n,m}?b
a.{n,}?b这两个的含义相信你可以通过上面的表达是中推理出来

但值得一提的是,如果对于aabaab使用a.*?b,匹配的结果会是aab而不是ab,因为在正则的判断中,最先开始的匹配拥有最高的优先权,a会与字符串中的第一个a发生匹配,此后不再变换匹配位置

后向引用

好的,恭喜你走到了这里,不知道你感觉前面的语法难度如何,但是接下来才是真正让人崩溃的部分

我们之前提到了分组,简单的将其解释为数学意义上的括号,但事实并不是这样。在之前的分组中,每个括号都会匹配一段内容,我们称为这段内容被这个分组所捕获了,在之后的表达式内容中,我们可以用组名来表示被捕获的内容

首先,这些被捕获的内容被按照从1开始的整数进行命名,这里有一个简单的例子

1
(\d\d\d)\1

这部分正则表达式所指的是查找六位数,其前三位和后三位的数值是一样的

为了便于阅读,允许对组进行重命名,例如上面的表达式可以写为

1
2
(?<num>\d\d\d)\k<num>
(?"num"\d\d\d)\k<num>这两种语法是等价的

(?表达式)即表示对这部分表达式所匹配的内容进行重命名,上面的表达式中我重命名为num,此后\k即申明使用后向引用为引用的名称

ps:其实正则表达式的分组也是从0开始的,只不过第0组表示的是整个正则表达式。此外,经过重命名的组也是可以用数字编号表示的,但是,数字编号的规则是从左到右遍历两次,第一次为未重命名组分配编号,第二次为已重命名组分配编号,注意使用时选择正确的编号

当然,你也可以要求不为此组分配编号(?:表达式)表示不为这一组分配编号

零宽断言

好的,接下来看一看这一部分,很好,仅从字面意思已经完全不能理解它的功能了,慢慢来

  1. 零宽度正预测先行断言,也叫零宽断言或者正向前瞻,它断言自身出现的地方之后满足某些条件

    语法(?=表达式),但注意,如果条件满足则判断继续,如果条件不满足则停止判断,此段字符串,但哪怕成功,符合断言的部分仍然不会被捕获

    应用实例:现在又一个字符串是用户将要创建的账户的密码,我想要判断密码是否符合要求,密码的具体规则如下

    1.密码长度8到16位 2.同时包含字母,数字,特殊符号(!@#$%^&*),我们可以这样实现

    1
    ^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,16}$

    连续使用三个零宽断言,分别判断是否有字母,数字,特殊符号

    注意理解什么是零宽度:所谓的零宽度,即这部分断言所占据的长度为0,它只是单纯的向后读取一次字符串,并判断是否满足条件,不改变指向字符串的指针位置。此外因为是零宽度,也就不事实上去捕获任何的字符

  2. 零宽度正回顾后发断言,它断言自身出现位置之前能够满足断言条件

    语法((?<=表达式),举个例子,对于字符串I am reading a book,使用如下表达式

    1
    2
    \b\w+(?=ing\b)
    (?<=\bre)\w+\b

    对于第一个表达式,它匹配的过程为先寻找单词边界再往后推进匹配一个或以上的字符数字或汉字,一直到被空格阻断,此时再进行断言,判断断言之前是否为ing加单词边界,如果满足条件则捕获,不满足条件继续推进查询

    对应第二个表达式,先执行断言,读取字符串指针之前的内容直到满足单词边界加re的结构再继续进行匹配

    所以,上述两个式子所匹配的结果分别为read,ading

反向零宽断言

其实就是零宽断言的反义,具体的语法分别是(?!表达式)(?<!表达式),其含义即断言不满足对应的条件,具体的示例就不写了

注释

对,一个表达式中居然特别支持了注释,足以体现这玩意有多不好搞

具体语法(?#你的注释)不管写在表达式的哪一个部分,注释内容都将被忽略,

结束语

好的,你已经了解到了正则表达式的基础,能用来做一些基础的判断,但是!!!你甚至连正则表达是的语法都没有学完,你所了解到的,只是这庞大冰山的一角。这里建议你可以去了解一下什么是平衡组/递归平衡,什么是修饰符等等等等一大堆东西,但至少你可以用正则表达式来实现一些简单的功能了

最后的最后,当然是图图了(这次的图使用github做图床发的,加载不出来属于正常现象)


半小时正则表达式入门
http://soulmate.org.cn/2024/12/11/半小时正则表达式入门/
作者
Soul
发布于
2024年12月11日
许可协议