[编程相关]正则表达式Regex语法
--目录--
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个标志位,用于表示正则的匹配模式,几个模式一般是独立的,有不可同时一起使用的可以。
符号 | 含义 |
---|---|
i | ignore case,忽略大小写 |
g | global search,全局搜索,不加标志g只会匹配第一个然后就退出 |
m | multiline,多行,会影响锚定符的判定,^和&会分别匹配到行的开头和行的结尾 |
u | unicode, 使用unicode模式,但有些服务器不支持 |
y | sticky,忽略g标志,只会匹配最后一个匹配项,但有些服务器不支持 |
s | dot 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. 结束
写完咯,终于!希望能够对各位有所帮助吧。如果有用的话,就点赞或者评论一下吧,倒也没啥特殊作用。毕竟也不是靠文章过活,真有这念头估计穷困潦倒了。只是絮絮叨叨写了好多字,若是无人反馈,也是遗憾。如果有点赞评论,倒也不至于像说与树洞听。
然后,希望明天的班,不要太清闲也不要太忙,完结~
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!