1. <em id="2qvri"><tr id="2qvri"></tr></em>
      1. 首页»JavaScript»正则全攻略使用手册,你确定不进来看看吗

        正则全攻略使用手册,你确定不进来看看吗

        来源:Croc_wend 发布时间:2019-02-25 阅读次数:

        前言

        正则表达式是软件领域为数不多的伟大创作。与之相提并论是分组交换网络、Web、Lisp、哈希算法、UNIX、编译技术、关系模型、面向对象等。正则自身简单、优美、功能?#30475;蟆?#22937;用无穷。

        学习正则表达式,语法并不难,稍微看些例子,多可照葫芦画瓢。但三两篇快餐文章,鲜能理解深刻。再遇又需一番查找,竹篮打水一场空。不止正则,其他技术点同样,需要系统的学习。多读经典书籍,站在巨人肩膀前行。

        这里涉及的东西太多,我就着重讲日常开发中可能会用到的内容,如果像深入理解的话推荐翻阅书籍《精通正则表达式》

        (所以简单来说,学习正则就是投入高,收益低)(起初一看简单易懂,深入了解过后感叹正则的?#30475;螅?/p>

        全文略长,可以选择?#34892;?#36259;的部分看

        1、介绍正则

        正则表达式严谨来讲,是一种描述字符串结构模式的形式化表达方法。起始于数学领域,流行于 Perl 正则引擎。JavaScript 从 ES 3 引入正则表达式,ES 6 扩展

        对正则表达式支持。

        正则原理

        对于固定字符串的处理,简单的字符串匹配算法(类KMP算法)相较更快;但如果进行复杂多变的字符处理,正则表达式速度则更胜一筹。那正则表达式具体匹配原理是什么?#31354;?#23601;涉及到编译原理的知识(编译原理着实是我大三里面最头疼的课程了)

        正则表达式引擎实现采用一种特殊理论模型:有穷自动机(Finite Automata)也叫有限状态自动机(finite-state machine)具体的细节见文章底部的参考文档

        字符组

        字符组 含义
        [ab] 匹配 a 或 b
        [0-9] 匹配 0 或 1 或 2 ... 或 9
        1 匹配 除 a、b ?#25105;?#23383;符
        字符组 含义
        d 表示 [0-9],数字字符
        D 表示 [^0-9],非数字字符
        w 表示 [_0-9a-zA-Z],单词字符,注意下划线
        W 表示 [^_0-9a-zA-Z],非单词字符
        s 表示 [ tvnrf],空白符
        S 表示 [^ tvnrf],?#24378;?#30333;符
        . 表示 [^nru2028u2029]。通配符,匹配除换行符、回车符、行?#25351;?#31526;、段?#25351;?#31526;外?#25105;?#23383;符

        ?#30475;?/h4>
        匹配优先?#30475;?/th> 忽略优先?#30475;?/th> 含义
        {m,n} {m,n}? 表示至少出现 m 次,至多 n 次
        {m,} {m,}? 表示至少出现 m 次
        {m} {m}? 表示必须出现 m 次,等价 {m,m}
        ? ?? 等价 {0,1}
        + +? 等价 {1,}
        * *? 等价 {0,}

        锚点与断言

        正则表达式中有些结构并不真正匹配文本,只负责判断在某个位置左/?#20063;?#30340;文本是否符合要求,被称为锚点。常见锚点有三类:行起始/结束位置、单?#26102;?#30028;、环视。在 ES5 中共有 6 个锚点。

        锚点 含义
        ^ 匹配开头,多行匹配中匹配行开头
        $ 匹配结尾,多行匹配中匹配行结尾
        b 单?#26102;?#30028;,w 与 W 之间位置
        B 非单?#26102;?#30028;
        (?=p) 该位置后面字符要匹配 p
        (?!p) 该位置后面字符不匹配 p

        需要注意,\b 也包括 \w 与 ^ 之间的位置,以及 \w 与 $ 之间的位置。如图所示。

        修饰符

        修饰符是指匹配时使用的模式规则。ES5 ?#20889;?#22312;三种匹配模式:忽略大小写模式、多行模式、全局匹配模式,对应修饰符如下。

        修饰符 含义
        i 不区分大小写匹配
        m 允许匹配多行
        g 执行全局匹配
        u Unicode 模式,用来正确处理大于\uFFFF的 Unicode 字符,处理四个字节的 UTF-16 编码。
        y 粘连模式,和g相似都是全局匹配,但是特点是:后一次匹配都从上一次匹配成功的下一个位置开始,必须从剩余的第一个位置开始,这就是“粘连”的涵义。
        s dotAll 模式,大部分情况是用来处理行终止符的

        2、正则的方法

        字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()和split()。

        ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

        String.prototype.match 调用 RegExp.prototype[Symbol.match]

        String.prototype.replace 调用 RegExp.prototype[Symbol.replace]

        String.prototype.search 调用 RegExp.prototype[Symbol.search]

        String.prototype.split 调用 RegExp.prototype[Symbol.split]

        String.prototype.match

        String.prototype.replace

        字符串的replace方法,应该是我们最常用的方法之一了,这里我给详细的说一下其中的各种使用攻略。

        replace函数的第一个参数可以是一个正则,或者是一个字符串(字符串没有全局模式,仅匹配一次),用来匹配你想要将替换它掉的文本内容

        第二个参数可以是字符串,或者是一个返回字符串的函数。这里请注意,如果使用的是字符串,JS 引擎会给你一些 tips 来攻略这段文本:

        变量名 代表的值
        $$ 插入一个 "$"。
        $& 插入匹配的子串。
        $` 插入当前匹配的子串左边的内容。
        $' 插入当前匹配的子串右边的内容。
        $n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示?#26680;?#24341;是从1开始,注意这里的捕获组规则

        如果你不清楚捕获组的顺序,给你一个简单的法则:从左到右数 >>> 第几个 '(' 符号就是第几个捕获组

        (特别适用于捕获组里有捕获组的情况)(在函数模式里,解构?#25345;?#26102;会特别好用)

        $`:就是相当于正则匹配到的内容的左侧文本

        $':就是相当于正则匹配到的内容?#20063;?#25991;本

        $&:正则匹配到的内容

        $1 - $n :对应捕获组

        如果参数使用的是函数,则可以?#20113;?#37197;的内容进行一些过滤或者是补充

        下面是该函数的参数:

        变量名 代表的值
        match 匹配的子串。(对应于?#40092;?#30340;$&。)
        p1,p2, ... 假如replace()方法的第一个参数是一个RegExp 对象,则代表第n个括号匹配的字符串。(对应于?#40092;?#30340;$1,$2等。)例如, 如果是用 /(\a+)(\b+)/这个来匹配, p1就是匹配的 \a+, p2 就是匹配的 \b+。
        offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是“abcd”,匹配到的子字符串是“bc”,那么这个参数将是1)
        string 被匹配的原字符串。

        一个示例,从富文本里面,匹配到里面的?#35745;?#26631;签的地址

        可以说,使用函数来替换文本的话,基本上你想干嘛就干嘛

        String.prototype.search

        String.prototype.split

        RegExp.prototype.test

        和String.prototype.search 的功能很像,但是这个是返回布尔值,search返回的是下标,这个从语义化角度看比?#40092;?#21512;校检

        RegExp.prototype.exec

        3、正则常见的使用

        主要内容是ES6 里?#30053;?#30340;修饰符(u,y,s)(g,m,i 就不说了)、贪婪?#22836;?#36138;婪模式、先行/后行断言

        'u' 修饰符

        ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。少说废话,看图

        但是很可惜的是 MDN给出的浏览器兼容性如下:(截止至2019.01.24),所以离生产环?#25104;?#20351;用还是有点时间

        'y' 修饰符

        除了u修饰符,ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。

        y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置?#20889;?#22312;匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

        var s = 'aaa_aa_a';
        var r1 = /a+/g;
        var r2 = /a+/y;
        
        r1.exec(s) // ["aaa"]
        r2.exec(s) // ["aaa"]
        
        r1.exec(s) // ["aa"]
        r2.exec(s) // null

        上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是_aa_a。由于g修饰没?#24418;?#32622;要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。

        如果改一下正则表达式,保证?#30475;?#37117;能头部匹配,y修饰符就会返回结果了。

        var s = 'aaa_aa_a';
        var r = /a+_/y;
        
        r.exec(s) // ["aaa_"]
        r.exec(s) // ["aa_"]

        上面代码?#30475;?#21305;配,都是从剩余字符串的头部开始。

        使用lastIndex属性,可以更好地说明y修饰符。

        const REGEX = /a/g;
        
        // 指定从2号位置(y)开?#35745;?#37197;
        REGEX.lastIndex = 2;
        
        // 匹配成功
        const match = REGEX.exec('xaya');
        
        // 在3号位置匹配成功
        match.index // 3
        
        // 下一次匹配从4号位开始
        REGEX.lastIndex // 4
        
        // 4号位开?#35745;?#37197;失败
        REGEX.exec('xaya') // null

        上面代码中,lastIndex属性指定?#30475;?#25628;索的开始位置,g修饰符从这个位置开始向后搜索,直到发现匹配为止。

        y修饰符同样遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。

        const REGEX = /a/y;
        
        // 指定从2号位置开?#35745;?#37197;
        REGEX.lastIndex = 2;
        
        // 不是粘连,匹配失败
        REGEX.exec('xaya') // null
        
        // 指定从3号位置开?#35745;?#37197;
        REGEX.lastIndex = 3;
        
        // 3号位置是粘连,匹配成功
        const match = REGEX.exec('xaya');
        match.index // 3
        REGEX.lastIndex // 4

        实际?#24076;瑈修饰符号隐含了头部匹配的标志^。

        /b/y.exec('aba')
        // null

        上面代码由于不能保证头部匹配,所以返回null。y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都?#34892;А?/p>

        下面是字符串对象的replace方法的例子。

        const REGEX = /a/gy;
        'aaxa'.replace(REGEX, '-') // '--xa'

        上面代码中,最后一个a因为不是出现在下一次匹配的头部,所以不会被替换。

        单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。

        'a1a2a3'.match(/a\d/y) // ["a1"]
        'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

        y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符。

        const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
        const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;
        
        tokenize(TOKEN_Y, '3 + 4')
        // [ '3', '+', '4' ]
        tokenize(TOKEN_G, '3 + 4')
        // [ '3', '+', '4' ]
        
        function tokenize(TOKEN_REGEX, str) {
          let result = [];
          let match;
          while (match = TOKEN_REGEX.exec(str)) {
            result.push(match[1]);
          }
          return result;
        }

        上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。

        tokenize(TOKEN_Y, '3x + 4')
        // [ '3' ]
        tokenize(TOKEN_G, '3x + 4')
        // [ '3', '+', '4' ]

        上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。

        很遗憾,这个的浏览器兼容性也不咋地

        但是,如果你的项目里有集成了babel,就可以使用以上的两个修饰符了,他们分别是

        @babel-plugin-transform-es2015-sticky-regex

        @babel-plugin-transform-es2015-unicode-regex

        's' 修饰符

        正则表达式中,点(.)是一个特殊字符,代表?#25105;?#30340;单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

        所谓行终止符,就是该字符表示一行的终结。以下四个字符属于”行终止符“。

        • U+000A 换行符(\n)
        • U+000D 回车符(\r)
        • U+2028 行?#25351;?#31526;(line separator)
        • U+2029 段?#25351;?#31526;(paragraph separator)

        虽然这个浏览器兼容性也很差,但是我们有方法来模拟它的效果,只是语义化上有点不友好

        /foo.bar/.test('foo\nbar')    // false
        /foo[^]bar/.test('foo\nbar')    // true
        /foo[\s\S]bar/.test('foo\nbar')        // true 我?#19981;?#36825;种
        贪婪模式?#22836;?#36138;婪模式(惰性模式)

        贪婪模式:正则表达式在匹配时会尽可能多地匹配,直到匹配失败,默认是贪婪模式。

        非贪婪模式:让正则表达式仅仅匹配满足表达式的内容,即一旦匹配成功就不再继续往下,这就是非贪婪模式。在?#30475;?#21518;面加?即可。

        在某些情况下,我们需要编写非贪婪模式场景下的正则,比如捕获一组标签或者一个自闭合标签

        这时捕获到了一组很奇怪的标签,如果我们的目标是只想捕获img标签的话,显然是不理想?#27169;?#36825;时非贪婪模式就可以用在这里了

        只需要在?#30475;?#21518;加 ? 就会启用非贪婪模式,在特定情况下是特别?#34892;?#30340;

        先行/后行(否定)断言

        有时候,我们会有些需求,具体是:匹配xxx前面/后面的xxx。很?#38480;?#30340;是,在很久之前,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind),在ES2018 之后才引入后行断言

        名称 正则 含义
        先行断言 /want(?=asset)/ 匹配在asset前面的内容
        先行否定断言 /want(?!asset)/ want只有不在asset前面才匹配
        后行断言 /(?<=asset)want/ 匹配在asset后面的内容
        后行否定断言 /(?<!asset)want/ want只有不在asset后面才匹配

        ?#40092;?#35828;,根据?#19994;?#32463;验,后行断言的使用场景会更多,因为js 有很多的数据存储是名值对的形式保存,所以很多时候我们想要通过"name="来取到后面的值,这时候是后行断言的使用场景了

        先行断言:只匹配 在/不在 百分号之前的数字

        后行断言:

        这里引例 @玉伯也叫射雕 的一篇 博文的内容

        这里可以用后行断言

        (?<=^|(第.+[?#24405;痌)).*?(?=$|(第.+[?#24405;痌))

        “后行断言”的实现,需要先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。

        首先,后行断言的组匹配,与正常情况下结果是不一样的。

        /(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
        /^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

        上面代码中,需要捕捉两个组匹配。没有“后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105和3。而“后行断言”时,由于执行顺序是从?#19994;?#24038;,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1和053。

        其次,“后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。

        /(?<=(o)d\1)r/.exec('hodor')  // null
        /(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

        上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从?#19994;?#24038;完成反斜杠引用。

        另外,需要提醒的是,断言部分是不计入返回结果的。

        具名组匹配

        ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

        上面代码中,“具名组匹配”在圆括号内部,模式的头?#21051;?#21152;“问号 + 尖括号 + 组名”(?<year>),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然?#34892;А?/p>

        具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。

        如果具名组没有匹配,那么对应的groups对象属性会是undefined。

        具名组匹配 × 解构?#25345;?/p>

        具名组引用

        如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

        4、常用正则

        我这里比较推荐一个正则可视化的网站:https://regexper.com/ 在上面贴上你的正则,会以图形化的形式展示出你的正则匹配规则,之后我们就可以大致上判断我们的正则是否符合预期(貌似需要科学上网)

        如果想通过字符串来生成正则对象的话,有两种方式,一种是字面量方式,另一种是构造函数

        构造函数:new Regexp('content', 'descriptor')

        字面量模式(请做好try-catch处理):

        const input = '/123/g'
        const regexp = eval(input)
        校验密码强度

        密码的强度必须?#21069;?#21547;大小写字?#36127;?#25968;字的组合,不能使用特殊字符,长度在8-10之间。

        ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$

        非全数字 全字母的 6-15位密码 先行否定断言

        /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,15}$/
        校验中文

        字符串仅能是中文。

        ^[\u4e00-\u9fa5]{0,}$
        校验身份证号码

        15位

        ^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$

        18位

        ^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$
        校验日期

        “yyyy-mm-dd“ 格式的日期校验,已考虑平闰年。

        ^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$
        提取URL链接

        下面的这个表达式可以筛选出一段文本中的URL。

        ^(f|ht){1}(tp|tps):\/\/([\\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?
        提取?#35745;?#26631;签的地址

        假若你想提取网页中所?#22411;计?#20449;息,可以利用下面的表达式。

        /<img [^>]*?src="(.*?)"[^>]*?>/g;
        \*[img][^\>]*[src] *= *[\"\']{0,1}([^\"\'\ >]*)

        5、注意事项

        使用非捕获型括号

        如果不需要引用括号内文本,请使用非捕获型括号 (?:...)。这样不但能够节省捕获的时间,而?#19968;?#20943;少回溯使用的状态数量。

        消除不必要括号

        非必要括号有时会阻止引擎优化。比如,除非需要知道 .* 匹配的最后一个字符,否则请不要使用 (.)* 。

        不要滥用字符组

        避免单个字符的字符组。例如 [.] 或 [*],可以通过转义转换为 \. 和 \*\。

        使用起始锚点

        除非特殊情况,否则以 .* 开头的正则表达式都应该在最前面添加 ^。如果表达式在字符串的开头不能匹配,显然在其他位置也不能匹配。

        从?#30475;手?#25552;取必须元素

        用 xx* 替代 x+ 能够保留匹配必须的 “x”。同样?#35272;恚?{5,7} 可以写作 -----{0,2}。(可读性可能会差点)

        提取多选结构开头的必须元素

        用 th(?:is|at) 替代 (?:this|that),就能暴露除必须的 “th”。

        忽略优先还是匹配优先?

        通常,使用忽略优先?#30475;剩?#24816;性)还是匹配优先?#30475;剩?#36138;婪)取决于正则表达式的具体需求。举例来说,/^.*:/ 不同于 ^.*?:,因为前者匹配到最后的冒号,而后者匹配到第一个冒号。总的来说,如果目标字符串很长,冒号会比较接近字符串的开头,就是用忽略优先。如果在接近字符串末?#21442;?#32622;,就是用匹配优先?#30475;省?/p>

        拆分正则表达式

        有时候,应用多个小正则表达式的速度比单个正则要快的多。“大而全”的正则表达式必须在目标文本中的每个位置测?#36816;?#26377;表达式,效?#24335;?#20026;低下。典型例子可以参考前?#27169;?去除字符串开头和结尾空白。

        将最可能匹配的多选分支放在前头

        多选分支的摆放顺序非常重要,上文有提及。总的来说,将常见匹配分支前置,有可能获得更迅速更常见的匹配。

        避免指数级匹配

        从正则表达式角度避免指数级匹配,应尽可能减少 + * ?#30475;实?#21152;,比如 ([^\\"]+)* 。从而减少可能匹配情形,加快匹配速度。

        6、小结

        正则表达式想要用好,需要一定的经验,个人经验来看,需要把你想法中的需要写出来,然后通过搭积木的形式,把一个个小的匹配写出来,然后再组合出你想要的功能,这是比较好的一种实现方法。

        如果说遇到了晦涩?#35759;?#30340;正则,也可以贴到上面提到的正则可视化网站里,看下它的匹配机制。

        对于前端来说,正则的使用场景主要是用户输入的校检,富文本内容的过滤,或者?#23884;?#19968;些url或者src的过滤,还有一些标签的替换之类?#27169;?#25484;握好了还是大有裨益?#27169;?#36215;码以前雄霸前端的 jQ 的选择器 sizzle 就是用了大量正则。

        最后,如果大家觉得我有哪里写错了,写得不好,有其它什么建议(夸?#20445;?#38750;常欢迎大家指出和斧正。也非常欢迎大家跟我一起讨论和分享!

        QQ群:WEB开发者官方群(515171538),验证消息:10000
        微信群:?#26377;?#32534;微信 849023636 邀请您加入,验证消息:10000
        提示:更多精彩内容关注微信公众号:全栈开发者?#34892;模╢sder-com)
        网友评论(共0条评论) 正在载入评论......
        理智评论文明上网,拒绝恶意谩骂 发表评论 / 共0条评论
        登录会员?#34892;?/span>
        江苏快3投注技巧