[编程相关]正则表达式Regex语法

2023-12-24 10:45:37

0. 前言

偶尔会触及到正则的使用,每次有点一知半解,查得到就用了,自己需求有点不一样,改改就错了。终于找个空闲时间研究了一下正则,且记录下来以后可以查略,也希望对你有所收获。

附上无意间逛到的两个网站。一个校验、尝试、学习正则的网站,虽然是英文的,但用起来还挺好用的!推荐!本文挺多内容也是从这里学的。然后,原本还想整理一下常用式,但发现大家伙整理得还挺多的,就不再弄了,附上一个博主的整理。

// 校验、尝试、学习正则的网站(随便逛到的,英文)
https://regexr.com/
// 常用式整理(其他博主的)
https://blog.csdn.net/weixin_39359534/article/details/118676496

好了接下来就开始进入正题吧。

1. 正则语法

正则表达式 (Regular Expression)(regex、regexp或RE),描述满足某一规则的字符串,用于检索、替换字符串。检索替换是关键,所以这里大概可以分成怎么描述要检索的字符,以及怎么描述替换的内容两个大部分,只不过大部分时候我们一般只用到了检索。替换用的少。

而且正则的内容是非常有固定逻辑和规则的,如果把特殊字符都独立来看,列一张元字符表作用这种,看着都头晕。所以还是应该把正则的相关匹配逻辑分开,逐一理解就不容易混乱了。一个简单正则描述,在js/ts中可以这样被表示,两侧的斜杠只是表示这是一个正则表达式,没有实际意义。

let regex = /hello/ //在字符串中查找hello

简单查一个hello的一个正则,如果用于检测在js中我们可以用test,如果用于替换,可以用replace

let s1 = "hello world"
let s2 = "hellA world"
let regex = /hello/ //在字符串中查找hello
regex.test(s1) // true
regex.test(s2) // false
s1.replace(regex, 'hi') // "hi world"
s2.replace(regex, 'hi') // "hellA world"

如果我们要查询的内容规则比较复杂的话,就需要更多的语法来描述了。

2. 正则搜索语法

对于正则的搜索部分呢,有相当多内容,也可以带着独立的4个要求去学内容,检查字符串是否满足正则。如果下面这几种问题都解决了,那正则的内容,也就差不多了。

1 - 只包含数字
2 - 没有“he”
3 - 有“ha”或者“^_^"
4 - 有“HAahahHahahHA”,忽略大小写

(1)字符集 Character_Classes

有些时候,我们某位置要查的内容是数字0到9都行,或者小写字符都行,并不是一个具体的字符,那此时就需要一种语法来描述一下我们要查的非具体的字符(某一个)是啥,所以我们就可以用字符集了!

符号含义
.任意字符
[]表示包含字符中任意的一个
[^]不表示包含字符中任意的一个,^必须紧跟[

要注意的是这里针对的,都是在描述字符,而非字符串,比如“.”,用来指代任意一个字符,“[ABC]”指代“A“、“B“或者”C“。“[^ABC]"这里则相反,不能是“A“、“B“或者”C“,如下。

let regex_all = /hell./
regex_all.test("hello world") // true
regex_all.test("hellA world") // true
regex_all.test("hel world") // false

let regex_set = /hell[ABC]/
regex_set.test("hello world") // false
regex_set.test("hellA world") // true

let regex_nSet = /hell[^ABC]/
regex_nSet.test("hello world") // true
regex_nSet.test("hellA world") // false

如果字符集很多的情况下我们也不可能每个都写,有一些特定的情况可以被简化描述,比如 字符是一串连续的符号

符号含义
[A-C]a-b,指代a和b中间的任意字符,字符码范围

注意这里是字符码范围,比如[1-a],可以指代字符码为49-97的所有字符,[a-c]等同于[abc]。更常用的比如字母、数字、单词等,有更简化的表述,如下也可以在字符集[]或者[^]外直接可以使用,相当于自动套上[]

符号含义
[\w]匹配单词,等同于[A-Za-z0-9_]
[\W]匹配非单词,\w的相反结果
[\d]匹配数字,等同于[1-9]
[\D]匹配非数字,\d的相反结果
[\s]匹配空白符(空格换行制表符等),类似于[\n\r\t]
[\S]匹配单词,等同于’A-Za-z0-9_’

其他的比如\p{L},\P{L},\p{Han},\P{Han},用于匹配一下特殊的Unicode字符,需要注意的是,所有的浏览器都适用。这里也用一些小例子

let regex_a2d = /[a-d]/
regex_a2d.test("windows10") // false
regex_a2d.test("android") // true

let regex_w = /[\w]/
regex_w.test("windows10") // true
regex_w.test("android") // true

let regex_d = /[\d]/
regex_d.test("window10") // true
regex_d.test("android") // false

let regex_W = /[\W]/
regex_W.test("windows10") // false
regex_W.test("android") // false

这里不知是否有伙伴会疑惑,为啥/[\d]/,对"window10"是验证通过的。其实是因为正则是描述一个我们需要的字符串,也就是说对于/[\d]/,它表达的意思是我们需要一个0-9的字符,那“window10”中可还有两个了,当然满足了,所以结果不出意外的是true。

那对于想要整个字符串都值包含数字咋办,光靠描述字符可做不到,所以还要看下一节。

(2)锚定符 Anchors

当我们想查找特定位置为hello的字符串时,单纯描述字符已经无法满足这种需求了,所以需要描述位置的语法,锚定符

符号含义
^锚定字符串的开头
$锚定字符串的结尾
\b锚定单词的两边,单词判断于\w相同
\B锚定非单词的两边,单词判断于\w相同

锚定符,顾名思义,像锚一样固定了某个位置,比如 ^ 就是指明了这个位置是开始,比如 /^hello/,就是指,开头后面是hello,就是要一个以hello为开头的字符串。同理,\b也就说这个位置是个单词,/\bhello/,单词的开头是hello,/hello\b/,指单词以hello结尾。如果是/\bhello\b/,就是要需要hello单词本身,因为开头到结尾只有hello,注意哦,这里"hellohello"也是不行的,只有hello本身可以。ok来点例子

let regex_start = /^hello/
regex_start.test("hello world") // true
regex_start.test("world hello") // false

let regex_end = /hello$/
regex_end.test("hello world") // false
regex_end.test("world hello") // true

let regex_b = /\bhello/
regex_b.test("hello world") // true
regex_b.test(" hello world") // true
regex_b.test("_hello world") // false

上节讲到,要匹配一个字包含数字的字符串,现在貌似有点办法,/^\b$/,开头到结尾都是得数字!不过有个问题,就是能有一个数字,就是说“1”可以通过,“11”就不行了。数量的问题,就需要下一节的计数来解决了。

(3)计数 Quantifiers

有些时候不只字符内容不确定,有可能字符数量也不确定,比如要1-3个数字。甚至有时候数量也不是确定的,那这个时候,这个时候,就需要计数!来解决这个问题、

符号含义
{n,m}{n,m}:匹配n到m个描述项,{n,m}前面可以为字符、字符集或者字符组
{n,}{n,}: 匹配n或者更多个描述项

计数这个最好理解了,我们直接上例子。

let regex_oneMore = /\d{1,}/
regex_oneMore.test("123") // true
regex_oneMore.test("abc1") // true
regex_oneMore.test("abc") // false

let regex_numOnly = /^\d{1,}$/
regex_oneMore.test("123") // true
regex_oneMore.test("abc1") // false
regex_oneMore.test("abc") // false

这里的话,是的我们成功解决了上边一直提的问题,要一个只包含数字的字符串,就是/^\d{1,}$/,这个正则现在应该很好理解了,就是从头到尾,有任意多个字符。

这里也可以反应一个问题,就是正则的比较好的思考思路是单元化(个人感觉),比如说你要一个“都是字符的字符串”,应该单元的去描述他每个单元的规则。”都是”说明要从开头到结尾/^$/,有任意多个单元所以是{1,},单元是要求字符数字是\d,这样就比较好处理。

一个情况是,数量是不确定时,会优先匹配更多的数量,是贪婪模式的。比如当/xa{2,4}/时,“xaaaaa”,就会被匹配为xaaaa。那如果希望尽量少的匹配字符结果,就可以给计量后加上?,就可以让其变为懒惰模式。比如当/xa{2,4}?/时,“xaaaaa”,就会被匹配为xaa,够了就收手了,相当lazy。检查的时候没有影响,但替换单词的时候就会有影响了,看例子!

let regex_greedy = /\d{2,}/
"12345abcde".replace(regex_greedy,"%") // %abcde

let regex_lazy = /\d{2,}?/
"12345abcde".replace(regex_lazy ,"%") // '%345abcde'

然后这里有没有发现每次都要比较多的字符描述,比如/\d{1,}/,我字符才一个,但计数就得3/4个符号了,这样不够方便、不好理解,看着乱,所以有一些简化的描述。如下!

符号含义
{n}等价于{n,n}
+等价于{1,}
*等价于{0,}
?等价于{0,1}

稍微绕一点的就是??,比如/xa??/,其实等价于/xa{0,1}?/,其他的就不过多描述了。

计量是仅表述前一个项的,比如,/\d{2,3}/就表示要2到3个字符,/ha{2,3}/就表示要“haa”或者“haaa”。但我这人生性爱笑,只想听“hahahahahaha”。又到了处理不了的地方,现在我们只能描述单个字符,所以呢,是的,又到得下一节了。

(4)分组与索引 Group_And_Reference

对于特定的字符序列组合,描述一个字符是不足够的,可以用就()了,字符组!这里套上括号的通常是为了再对这对字符做处理比如,重复匹配多少次这种, 如果只是/ha/和/(ha)/是几乎没有区别的,但比如说要比配多个ha,那就可以用/(ha)+/的这种正则来表述了!所以分组是把固定的字符序列打包成一组,方便我们后续再描述这个字符组

除了计数这种直接描述前一项的方式,我们也可以通过索引来调用分组,比如 \3 表示第3个分组的内容。表来!~

符号含义
()特定的字符组
(?: )特定的字符组,但不创建所以不会被索引
\n索引到第n个字符组。索引从1开始,比如 /(ha)a\1/相当于/(ha)a(ha)

有个不被索引的这个分组,不常用,其实主要是为了有时候我们确实需要对某个组做处理,但是后续不想再索引到他了,就可以用了。比如原本需求是/(x)\1\1\1/,需求改了要和气点,需要前面带点“ha”才行。那现在正则也得改,如果直接加/(ha)+(x)\1\1\1/,那就坏了,\1都索引到ha了,所以就可以用改成/(?ha)+(x)\1\1\1/,这样这个组就不会被后面的索引到了,正常运行!

需要注意的是,字符集[][^]中描述的是字符,而()描述的是字符串。所以**()内可以包含[]。但()分组的语法在[]内是无效的**,会被视为普通字符"(“和”)"。就直接上例子了,来点笑脸。

let regex_ha = /(ha)+/
"hahahahahahaxiaohehe".replace(regex_ha ,"^_^") // '^_^xiaohehe'

此外(?<name>ABC) 创建可以被特定名字索引的字符组,但不是所有的浏览器都适用,也没试出所以然,就不多说了。

不过呢,作为ha党,是不喜欢hehe的,所以我希望匹配一个不带“he”的字符串,问题又来了,这个处理不了。因为没有非字符组这个概念。直接下一章!

(5)周围 Look_Around

有些字符我们是希望其周围有某个字符组,但不要匹配到他,比如匹配png图标名,“test1.png,test2.txt”,只希望匹配到test1这个名字,就可以使用(?=)或者(?!)去查询到结果后,剔除掉周围(前或者后看具体位置)不包含所需结果的匹配项。此外(?<=)和(?<!)用于在查询之前先匹配周围字符组,但不是所有的浏览器都适用,不讨论了。表来。

符号含义
(?=)查询到结果后,剔除周围不包含所需结果的匹配项
(?!)查询到结果后,剔除周围包含所需结果的匹配项

这里看起来是稍微有点绕,但看例子就很容易理解了。.下面例子中会用到/g,其实是一个标志位,表示全局匹配,找到所有的匹配项才结束。标志位将会在后面(*)再具体介绍。

"1pt 2px 3em 4px".replace(/\d(?=px)/g,"-") //'1pt -px 3em -px'
"1pt 2px 3em 4px".replace(/\d(?!px)/g,"-") //'-pt 2px -em 4px'

在拥有了这个语法之后,现在我们可以用不包含字符组来描述我们的需求了。比如我们希望,匹配一个不包含字符组"he"的字符串,用来过滤一下我们不想看到的日志,就可以用/^((?!he).)*$/,其实是描述了我们通过描述期望的字符单元,来达到不需要某个字符串的结果。(?!he)表达字符前面不能包含"he",那外面/^()*$/就表示字符串从开始到结尾要匹配上组(),所以整个字符串,也就包含不了"he"

同理,字符串不包含"ABC"或者"XYZ",那就是/^((?!ABC)(?!XYZ).)*$/

let regex = /^((?!he).)*$/
regex.test("hahahaha123") // true
regex.test("hahahahe123") // false

现在呢,学得多一些了,想法也多一些,不满于“hahahahaha”了,要加点颜表情,“^ _ ^”,那可和锚定字符冲突了呀,咋搞?这个估计大家都懂,转义!有请超级字符,转义符 "\“ 登场。

(6)转义符 Escape_Characters

转义符,感觉用得都很频繁了,就是当有些字符是有特殊意义的,而我们需要匹配这个字符本身的时候就可以使用转义符,将其变为字符本身的含义

此外,有些字符是不好被打出来的,那么就可以8进制或者16进制来表示,比如\141,\x61,都是指代字符码97也就是a。一些老牌搭档,比如\r,\n这种也是可以正常使用的。表来~

符号含义
+*?^$.[]{}()|/正则用到的特殊字符,都可以加\转义为字符原本的意义
\000指代8进制表示字符码对应的字符,从0-255,为\000)到\377,比如\141表示a
\xFF指代8进制表示字符码对应的字符,比如\x61表示a
\cZ指代控制字符1-26对应A-Z(a-z),比如\cI或者\ci,等价于\x09(字符码9)
\t\n\v\f\r\0制表\t,换行\n,垂直制表\v,换页\f,光标换行\r,空字符\0
\u{FFFF}指代unicode编码的字符,但不是所有浏览器都支持,比如/[\u4e00-\u9fa5]/

“正则用到的特殊字符”中特殊的是,字符集[]或者[^]中,只有其中只有 \-] 可以被转义,因为其他字符在字符集中不能发挥作用 ,没有特殊意义。

有一个和之前冲突的点是,当\1在有1分组的时候,希望被当成字符使用,比如用\1查询字符码为1的字符,可以用[\1]指代,利用分组在字符集中不生效的方法来实现。此外,当没有对应字符组的时候,会直接当转义符用,比如\2,但只有一个或者更少的字符组,那么就是指代字符码为2的字符。

无压力,来点颜表情~

let regex_xiao = /\^_\^/
"^_^xiao".replace(regex_xiao ,"haha") // 'hahaxiao'

现在总算可以很完美的使用颜文字了。那有些时候呢,我们是希望能找到haha或者颜表情^_^,就,下一节。

(7)交替/或 Alternation

上述的操作经常是处于一种 并且 的逻辑中,比如/(win)(mac)/,就只能匹配上winmac这个字符了,因为是win并且mac的一种逻辑,那如果我们想要win或者mac都能匹配上,就是 或 逻辑,此时"|"就应运而生。这个在正则中为Alternation,交替,应该原意是指用左右交替匹配。

符号含义
|匹配左或者右任意中一个成立,左右是一个正则表达式

/(win)|(mac)/,这样就达到我们要的效果了。当然也不只能用一次,比如可以用多个或逻辑结合,比如(win)|(mac)|(ios)|(android)。

let regex_mobile = /(ios)|(android)/
regex_mobile.test("win11") // false
regex_mobile.test("ios") // true
regex_mobile.test("mac") // false
regex_mobile.test("android") // true

let regex_more = /(win)|(mac)|(linux)|(ios)|(android)/
regex_more.test("win11") // true
regex_more.test("ios") // true
regex_more.test("mac") // true
regex_more.test("android") // true

那ha或颜文字的问题也就解决了

let regex = /(ha)|(\^_\^)/
regex.test("hahaha") // true
regex.test("^_^++") // true
regex.test("-_-+++") // false

(8)标志 flags

标志位一共是有igmuys这6个标志位,用于表示正则的匹配模式,几个模式一般是独立的,有不可同时一起使用的可以。

符号含义
iignore case,忽略大小写
gglobal search,全局搜索,不加标志g只会匹配第一个然后就退出
mmultiline,多行,会影响锚定符的判定,^和&会分别匹配到行的开头和行的结尾
uunicode, 使用unicode模式,但有些服务器不支持
ysticky,忽略g标志,只会匹配最后一个匹配项,但有些服务器不支持
sdot All,使得符号.可以适配所有的字符,包括换行符,但有些服务器不支持

在js/ts中,斜杠后面加上标志就要可以了,比如/ha/g。用的最多的应该就是g,i。注意了,g是查找所有,不是要求所有,/ha/g并不是要求整个字符串都是“ha”,只是要找到所有的“ha”,通常在需要替换全部的时候才会用到。

let regex = /ha/gi
"hahaha1".replace(regex,"^_^") //^_^^_^^_^1
"haHahA".replace(regex,"^_^") //^_^^_^^_^

3. 正则替换语法

讲完了正则描述的部分,有些时候我们需要做替换处理都比较简单的时候,比如将abc,替换成ABC,直接"zabcd".replace(/abc/,“ABC”) ,输出zABCd。如果我们还需要用到我们匹配中涉及的内容,就需要**替换(substitution)**语法,用$来做相关的处理。

符号含义
$&指代匹配到的内容
$`指代匹配到内容的前面字符串
$’指代匹配到内容的后面字符串
$n指代匹配到内容的第n个组
$$转义,$$指代$本身

注意了,这里已经不是用在正则表达式中了,而是用在替换内容中,来点例子。

// $&指代匹配到的内容
"zabcd".replace(/abc/,"$&ABC") // 输出zabcABCd,$`替换成了"abc"
// $`指代匹配到内容的前面字符串
"zabcd".replace(/abc/,"$`ABC") // 输出zzABCd,$`替换成了"z"
// $'指代匹配到内容的后面字符串
"zabcd".replace(/abc/,"$'ABC") // 输出zdABCd,$'替换成了"d"
// $n指代匹配到内容的第n个组
"zabcd".replace(/(a)(b)/,"x$2") // 输出zxbcd,$'替换成了第二组,就是b,所以将ab,替换为xb

转义$$,解决一下想替换为$但识别成替换操作的内容的问题。如果不被误识别,也可以直接用$

"zabcd".replace(/(a)(b)/,"$$1") // 输出z$1cd,$$为$字符本身,所以将ab,替换为$1
"zabcd".replace(/(a)(b)/,"$3") // 输出z$3cd,没有字符组3,$识别为$字符本身,将ab,替换为%$-
"zabcd".replace(/(a)(b)/,"%$-") // 输出z%$-cd,$为$字符本身,将ab,替换为%$-

到这里,正则语法已经基本讲完了,不知不觉已经1w字了。正则用在编程中可以很方便的用来,检查或者替换字符,但注意的是,过于复杂的表达式是理解成本很高的,多加点注释吧,不然明天再看到就得再思考好一会,这串东西是用来干嘛的。

4. 正则性能

语法结束,我们研究一下性能问题,用正则方便了,但会不会开销挺大的?虽然js考虑啥性能…但还是写段小代码测试一下。

// 生成a-z的随机字符,长度10000
let len = 10000
let s = ''
let aCode = 'a'.charCodeAt(0)
for (let i = 0; i < len; i++) {
    let code = aCode + Math.floor(Math.random() * 26)
    s += String.fromCharCode(code)
}

// 测试时间,查询10w次abc
let count = 100000
{
    let regex = /abc/
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        regex.test(s)
    }
    let t2 = new Date().getTime()
    console.log('/abc/ - 正则查询:', t2 - t1)
}
{
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        let regex = /abc/
        regex.test(s)
    }
    let t2 = new Date().getTime()
    console.log('/abc/ - 正则查询(每次新建正则):', t2 - t1)
}
{
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        let regex = new RegExp(/abc/)
        regex.test(s)
    }
    let t2 = new Date().getTime()
    console.log('/abc/ - 正则查询(每次新建regExp):', t2 - t1)
}
{
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        for (let j = 0; j < len; j++) {
            if (s[j] == 'a' && s[j + 1] == 'b' && s[j + 2] == 'c') {
                break
            }
        }
    }
    let t2 = new Date().getTime()
    console.log('/abc/ - 遍历查询:', t2 - t1)
}

// 测试时间,查询10w次a
{
    let regex = /a/
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        regex.test(s)
    }
    let t2 = new Date().getTime()
    console.log('/a/ - 正则查询:', t2 - t1)
}
{
    let t1 = new Date().getTime()
    for (let i = 0; i < count; i++) {
        for (let j = 0; j < len; j++) {
            if (s[j] == 'a') {
                break
            }
        }
    }
    let t2 = new Date().getTime()
    console.log('/a/ - 遍历查询:', t2 - t1)
}

结果就是

/abc/ - 正则查询: 210
/abc/ - 正则查询(每次新建正则): 206
/abc/ - 正则查询(每次新建regExp): 218
/abc/ - 遍历查询: 1832
/a/ - 正则查询: 1
/a/ - 遍历查询: 5

可见比起我们自己手动去遍历,正则的通常性能是要更优的,至少在js引擎中,其他引擎可能差不多。此外,正则的创建开销不大,每次都创建可以可行的,甚至每次都每次新建正则,反而更快,真是奇妙的js…。总的来说就是,无内鬼放心用。稍微注意下可读性问题,要注释,不然后天就忘记了,出bug又得再想一遍。

5. 结束

写完咯,终于!希望能够对各位有所帮助吧。如果有用的话,就点赞或者评论一下吧,倒也没啥特殊作用。毕竟也不是靠文章过活,真有这念头估计穷困潦倒了。只是絮絮叨叨写了好多字,若是无人反馈,也是遗憾。如果有点赞评论,倒也不至于像说与树洞听。

然后,希望明天的班,不要太清闲也不要太忙,完结~

文章来源:https://blog.csdn.net/Blue_carrot_/article/details/135142422
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。