JS11 - RegExp正则表达式规则、贪婪与非贪婪、模式修正、非捕获元、零宽断言、截取时间字符串、判断密码强度

1,271 阅读11分钟

基本概念

  • 正则表达式:一种匹配字符串中字符组合的模式
  • 数据类型:在 JavaScript 中,正则表达式也是对象。
  • 常用方法:正则常用于 RegExp 的 exec 和 test 方法,以及 String 的 match、matchAll、replace、search 和 split 方法。
  • 发展历史:正则表达式的祖先可以一直上溯至对人类神经系统如何工作的早期研究。1956年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为"神经网事件的表示法"的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为"正则集的代数"的表达式,因此采用"正则表达式"这个术语。随后,发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究,Ken Thompson 是 Unix 的主要发明人。正则表达式的第一个实用应用程序就是 Unix 中的 qed 编辑器。
  • 解决问题:典型的搜索和替换操作要求提供与预期的搜索结果匹配的确切文本,虽然这种技术对于对静态文本执行简单搜索和替换任务可能已经足够了,但它缺乏灵活性,若采用这种方法搜索动态文本,即使不是不可能,至少也会变得很困难。

创建方式

字面量创建

  • 表达式写在双斜线中 / /
var reg = /[1,2,3;4,5;"a"]/g; //这里面的数字、逗号、分号、字母、引号,都是备选字符 

内置构造函数

var reg2 = new RegExp(/[1,2,3;4,5;"a"]/g);  

JavaScript 正则方法

方法描述
exec一个在字符串中执行查找匹配的 RegExp 方法,它返回一个数组(未匹配到则返回 null)。
test一个在字符串中测试是否匹配的 RegExp 方法,它返回 true 或 false。
match一个在字符串中执行查找匹配的 String 方法,它返回一个数组,在未匹配到时会返回 null。
matchAll一个在字符串中执行查找所有匹配的 String 方法,它返回一个迭代器(iterator)。
search一个在字符串中测试匹配的 String 方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace一个在字符串中执行查找匹配的 String 方法,并且使用替换字符串替换掉匹配到的子字符串。
split一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

test() 方法

  • 功能test() 检测字符串是否匹配正则,匹配返回 true,不匹配返回 false
  • 语法let testRes = regexObj.test(str)
  • 说明:如果正则表达式设置了全局标志,test() 会改变正则表达式 lastIndex 属性,如果连续几次执行 test() 方法,后续的执行将会从 lastIndex 处开始匹配字符串,exec() 同样改变正则本身的 lastIndex

贪婪模式 VS 非贪婪模式

  • 正则表达式默认采用贪婪匹配模式(Greedy Matching),在这种模式下正则会尽可能多的匹配文字,*+ 限定符都是贪婪模式
  • 非贪婪模式(惰性匹配 Lazy Matching)尽可能少的匹配所搜索的字符串,减少匹配消耗,解决办法也很简单,即在 *+ 等限定符的后面加上一个?就可以实现非贪婪或最小匹配。当?紧跟在任何一个其他限制符 *, +, ?, {n}, {n,}, {n,m} 后面时,匹配都是非贪婪模式。同时,要注意exec方法默认在捕获的时候也是非贪婪模式,只会捕获遇到的第一个匹配字符,可以将正则表达式加上 g 全局模式修正符,让exec在捕获的时候变成贪婪模式。

匹配语法 - 元字符

点运算 .

匹配除了换行符\n \r之外的任何单个字符,相当于[^\n\r]

var reg = /./g;

点运算.png

var reg = /[^\n\r]/g;

匹配任意单个字符(除了换行符).png

或运算 |

两项之间的一个选择;要匹配|,使用\|

var reg = /((G|g)oogle)|((Y|y)ahoo)|((B|b)ing)|\|/gm;

或运算可用于不区分大小写的功能.png

重复符 + * ? { }

`+`    ---> 匹配前面的子表达式1次或多次   
`*`    ---> 匹配前面的子表达式0次或多次   
`?`    ---> 匹配前面的子表达式0次或1`{m}`  ---> 匹配前面的子表达式指定 "m"`{m,}` ---> 匹配前面的子表达式至少 "m"`{m,n}`---> 匹配至少 "m" 次,至多 "n"
  • + 匹配前面的子表达式1次或多次
var reg1 = /ht+p/g;    // 能匹配 htp、http、htttp 等
var reg2 = /ht{1,}p/g; // 同 reg1:+ 等价于 {1,}
var reg3 = /[ace]\+/g; // 匹配后面紧跟一个+号的ace的任意一个字符,匹配+字符,使用\+

+匹配至少一次及转义+字符.png

  • * 匹配前面的子表达式0次或多次
var reg1 = /ad*/g;        //ad* 能匹配 a ad add 等
var reg2 = /[a-z]*/g;     //[a-z]* 能匹配一行中所有小写字母的字符串
var reg3 = /[a-z]{0,}/g;  //* 等价于 {0,}
var reg4 = /^[1][0-9]{2}\*{4}/g;  //匹配电话中间四位加星号的;匹配 * 字符,请使用 *

image.png

  • ? 匹配前面的子表达式0次或1次
var reg1 = /r(es)?|does?/g;   // do(es)? 可以匹配 do ,或匹配 does 中的 does,或匹配 doxy 中的 do ,? 等价于 {0,1}
var reg2 = /100{2,4}?|\?/;    //匹配 ? 字符,请使用 ?

image.png

  • {} 量词,常用来限定一个或一组字符可以重复出现的次数,有{n} {n,} {n,m} 3种方式
var reg1 = /10{2}/g;         // 0 只能出现 2 次
var reg1 = /10{2,}/g;        // 0 至少出现 2 次
var reg1 = /10{0,1}/g;       // 0 只能出现 0 次或 1 次,等同于 ?
var reg4 = /\({1}|\[{1}|\{{1}/g;  //匹配 { ,使用 \{

image.png

边界符 ^ $

  • 在正则表达式中,经常需要定位匹配特定位置的字符,这里就有两个重要的定位锚点(边界符),开头和结尾。
  • ^指定开头,$制定结尾。
var reg1 = /^[1-9]/g; //开头需为1-9的数字 --> 当^不在中括号内,指定开头
var reg2 = /[^1-6]/g; //不能匹配1-6的数字 --> 当^在中括号内部,表示不接受中括号内的字符集合
var reg3 = /(\(\^)|\^)_(\^|-\)/g; //匹配出颜文字^_^ (^_-) --> 匹配 ^ 字符本身,使用 ^

image.png

// $ 匹配输入字符串的结尾位置
// 如果设置了正则对象的 Multiline 属性(处理多行),则 $ 也匹配 \n\r
var reg1 = /^[1-9][^1-6]|((\(\^)|\^)_(\^|-\))$/gm;  
//匹配 $ 字符本身,请使用 $
var reg2 = /\$\x20[0-9]{3}\.[0-9]{2}/

image.png

字符集 [...] [^...]

  • 匹配仅一次:字符集有中括号[]包裹,内部是单个匹配,即在[]内,每个字符只匹配一次

  • 特殊变普通: 特殊字符写在[]会被当成普通字符,例如 [(a)],会匹配 (a)这三个字符

  • 命名字符集[0-3] 表示找到这一位置上的字符只能是 0 到 3 这四个数字,与(0|1|2|3) 作用比较类似,但圆括号可以匹配多个连续的字符,而一对方括号只能匹配单个字符。

  • 排除字符集[^0-3] 表示找到这一位置上的字符只能是除了 0 到 3 之外的所有字符。[^aeiou]匹配字符串"google tabao ibis"中除了a e i o u字母的所有字母。[^m]%2F匹配不以m开头的,且后面紧跟%2F的字符串。

  • 字符集范围[A-Z]表示一个区间,匹配所有大写字母,[a-z]表示所有小写字母。例如,var reg1 = /[a-u]/g; ;字符集的数字区间不能使用从大到小的顺序,否则视为语法错误;如果数字超过一位则会分成两个数字处理

  • 顺序不紧要:如果不使用连字符,那么方括号字符的顺序不重要,例如,表达式[Tte]he会正常匹配theThe

特征群 ( )

  • 又称组匹配,() 本身不匹配任何东西,也不限制匹配任何东西,只是把括号内的内容作为一个整体来处理
var reg1 = /(ab){1,3}/; // ab 整体捕获最少1次,最多3次;没有括号,ab{1,3} 就表示b捕获最少1次,最多3次

匹配语法 - 转义字符

转义字符匹配功能等价\x0型等价\c型等价表达式
\\匹配\字符
\f换页符\x0c\cL
\n换行符\x0a\cJ
\r回车符\x0d\cM
\t制表符\x09\cl
\v垂直制表符\x0b\cK
\p匹配CR/CF,用来匹配DOS行终止符\r\n
\d匹配一个数字字符[0-9]
\D匹配一个非数字字符[^0-9]
\w匹配字母、数字、下划线[A-Za-z0-9_]
\W匹配非字母、数字、下划线[^A-Za-z0-9_]
\b定位符,匹配一个单词边界(字与空格之间的位置)
\B定位符,非单词边界匹配(非单词边界是任何其他位置)
\s匹配所有空白符\f\n\r\t\v
\S匹配非空白符(不包括换行)[^\f\n\r\t\v]
\x20只匹配空格(不包括换行、回车、制表、分页)
  • \b的书写位置非常敏感:如果位于开始,就在开始处匹配;如果位于结尾,就在结尾处匹配
  • 表达式/\bCha/匹配单词 Chapter 的开头三个字符,因为这三个字符出现在单词边界后面
  • 表达式/\Bapt/匹配单词 Chapter 中的字符串 apt,但不匹配aptitude中的字符串apt

匹配语法 - 模式修正

  • 模式修正符也称为正则标识符,用于指定匹配策略
  • 书写:模式修正符位于表达式外部结尾,例如,全局匹配模式 g /^\d[a-Z]*/g
修饰符含义描述
iignore将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
gglobal全局匹配:查找所有的匹配项。
mmulti line多行匹配:使边界字符 ^$ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。g只匹配第一行,添加m之后实现多行。
sdotAll特殊字符圆点.中包含换行符\n。默认情况下的圆点 . 是 匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。

匹配语法 - 非捕获元

  • 使用原因:在使用正则表达式的选择符()时,常常会因为相关的匹配会被缓存,导致出现匹配副作用。此时,使用非捕获元可以消除这种副作用,因为只匹配但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。

  • 类型:非捕获元包括有?: ?= ?! ?<= ?<!

    • (?:pattern) 匹配 pattern 但不获取匹配结果
    • (?=pattern) 零宽正向先行断言(zero-width positive lookahead assertion)
    • (?!pattern) 零宽负向先行断言(zero-width negative lookahead assertion)
    • (?<=pattern) 零宽正向后行断言(zero-width positive lookahead assertion)
    • (?<!pattern) 零宽负向后行断言(zero-width negative lookahead assertion)
  • 零宽断言:也称环视,匹配的是一个位置,如同^代表开头,$代表结尾,\b代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为零宽

(?:pattern) 直接匹配

// 初始正则
var reg1 = /industr(y|ial)/;
// 优化正则
var reg2 = /industr(?:y|ial)/;
console.log(reg1.exec("industry"))  
//捕获了y --> 输出结果:(2) ["industry", "y", index: 0, input: "industry", groups: undefined]
console.log(reg2.exec("industry"))  
//未捕获y --> 输出结果:["industry", index: 0, input: "industry", groups: undefined]

(?=pattern) 正向先行断言

  • 判断true:代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配pattern,但不会缓存。例如a regular expression,要匹配 regular 中的 re,但不匹配 expression 中的 re,可以用 re(?=gular),该表达式限定了 re 右边的位置,这个位置之后是 gular,但并不消耗 gular 这些字符。re(?=gular).(点运算符)可以匹配 reg,这里的(?=gular)不匹配字母,匹配的是字母e与字母g之间的位置。
var reg1 = /re(gular)/g;
var reg2 = /re(?=gular){1}/g;
var reg3 = /re(?=peat){1}/g;
console.log(reg1.exec("regular repeat"))  
//(2) ["regular", "gular", index: 0, input: "regular repeat", groups: undefined]
console.log(reg2.exec("regular repeat"))  
//["re", index: 0, input: "regular repeat", groups: undefined]
console.log(reg3.exec("regular repeat"))  
//["re", index: 8, input: "regular repeat", groups: undefined]

(?!pattern) 负向先行断言

  • 判断false:紧接匹配位置之后的字符串不能匹配pattern,相当于一个匹配过程中的条件判断。例如对 regex represents regular expression,要匹配除 regex 和 regular 之外的 re,可以用 re(?!g),该表达式限定了 re 右边的位置,这个位置后面不匹配字符 g
var reg1 = /re(gular)/g;
var reg2 = /re(?!gular){1}/g;
var reg3 = /re(?!peat){1}/g;
console.log(reg1.exec("regular repeat"))  
//(2) ["regular", "gular", index: 0, input: "regular repeat", groups: undefined]
console.log(reg2.exec("regular repeat"))  
//["re", index: 8, input: "regular repeat", groups: undefined]
console.log(reg3.exec("regular repeat"))  
//["re", index: 0, input: "regular repeat", groups: undefined]

(?<=pattern) 正向后行断言

  • 判断true:紧接该位置之前的字符序列能够匹配,但不会缓存,相当于一个条件判断。例如对 regex represents regular expression,要匹配内部re,但不匹配开头re,可以用 (?<=\w)re,对于内部re而言,在其前面应该是一个单词字符,这里则是使用 \w 去匹配相应的字符,但这里有只需要匹配缓存re这节字符串,那么就需要匹配上前面的字符,又不能将其缓存下来,就可以使用正向后行断言。
var reg1 = /(\w)re/g;
var reg2 = /(?<=\w)re/g;
var reg3 = /(?<=\W)re/g;
var str = "regular repeat represent";
console.log(reg1.exec(str))  
//(2) ["pre", "p", index: 17, input: "regular repeat represent", groups: undefined]
console.log(reg2.exec(str))  
//["re", index: 18, input: "regular repeat represent", groups: undefined]
console.log(reg3.exec(str))  
//["re", index: 8, input: "regular repeat represent", groups: undefined]
console.log(reg3.exec(str))  //第二次调用reg3,因为使用了g模式修正,所以是贪婪捕获
//["re", index: 15, input: "regular repeat represent", groups: undefined]

(?<!pattern) 负向后行断言

  • 判断false:代表字符串的一个位置,在该位置之前的字符串序列不能匹配pattern。例如对 regex represents regular expression,要匹配单词开头的re,可以用 (?<!\w)re,对于单词开头的 re,在本例中,也就是指不在单词内部的 re,即 re 前面不是单词字符。当然这种情况也可以用 \bre 来匹配。
var reg1 = /(\w)re/g;
var reg2 = /(?<!\w)re/g;
var reg3 = /(?<!\W)re/g;
var str = "regular repeat represent";
console.log(reg1.exec(str))  
//(2) ["pre", "p", index: 17, input: "regular repeat represent", groups: undefined]
console.log(reg2.exec(str))  
//["re", index: 0, input: "regular repeat represent", groups: undefined]
console.log(reg2.exec(str))  
//["re", index: 8, input: "regular repeat represent", groups: undefined]
console.log(reg2.exec(str))  
//["re", index: 15, input: "regular repeat represent", groups: undefined]
console.log(reg3.exec(str))  
//["re", index: 0, input: "regular repeat represent", groups: undefined]
console.log(reg3.exec(str))  //第二次调用reg3,因为使用了g模式修正,所以是贪婪捕获  
//["re", index: 18, input: "regular repeat represent", groups: undefined]
console.log(reg3.exec(str))  //第三次调用,已经没有可以捕获的了,所以返回null
//null

对于这 4 个断言的理解,可以从两个方面入手:

  1. 关于先行(lookahead)和后行(lookbehind): 正则表达式引擎在执行字符串和表达式匹配时,会从头到尾(从前到后)连续扫描字符串中的字符,设想有一个扫描指针指向字符边界处并随匹配过程移动。先行断言,是当扫描指针位于某处时,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行。后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。之所以叫后行断言,是因为正则表达式引擎在匹配字符串和表达式时,是从前向后逐个扫描字符串中的字符,并判断是否与表达式符合,当在表达式中遇到该断言时,正则表达式引擎需要往字符串前端检测已扫描过的字符,相对于扫描方向是向后的。
  2. 关于正向(positive)和负向(negative): 正向就表示匹配括号中的表达式,负向表示不匹配。

示例

截取时间

var noticeInfo = "Time is from 2019-10-10 00:00:00 to 2019-12-31 00:00:00";
var regTime = /\d{4}-\d{2}-\d{2}/g;
console.log(`Start: ${regTime.exec(noticeInfo)}\nStop: ${regTime.exec(noticeInfo)}`)
// Start: 2019-10-10
// Stop: 2019-12-31

密码强度

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        .container{
            width: 400px;
            margin: auto;
            text-align: center;
        }
        .pwd-value{
            width: 300px;
            height: 20px;
            display: inline-block;
            margin: 10px auto;
            border-radius: 10px;
            border: 1px solid black;
            outline: none;
            padding: 0 10px;
        }
        .pwd-value:current{
            border: none;
        }
        .pwd-check {
            width: 350px;
            height: 30px;
            margin: auto;
            list-style-type: none;
            display: flex;
            justify-content: space-evenly;
            align-items: center;
        }
        .pwd-check li {
            width: 100%;
            height: 100%;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: auto 2px;
            border-radius: 10px;
            color: white;
            font-weight: bolder;
        }
        .pwd-check-low{
            background-color: red;
        }
        .pwd-check-middle{
            background-color: green;
        }
        .pwd-check-high{
            background-color: blue;
        }
        .see,.hide{
            /*  如果并排的两个元素都是inline-block元素,
                那么需要两个都不取值,或者两者都取值,
                否则会造成两个元素并排却出现一上一下的情况*/
            background-size: cover;
            width: 20px;
            height: 20px;
            display: inline-block;    
            margin: 10px auto;
            cursor: pointer;
        }
        .hide{
            background-image: url(./不可见.png);
        }
        .see{
            background-image: url(./可见.png);
        }
    </style>
</head>
<body>
    <div id="container" class="container">
        <input id="pwd_value" class="pwd-value" type="password" value="" maxlength="6">
        <div id="hide" class="hide"> &nbsp;</div>
        <ul id="pwd_check" class="pwd-check">
            <li ></li>
            <li ></li>
            <li ></li>
        </ul>
    </div>
</body>
<script>
    /**作业9:验证密码强度,最短2位,最长6位
     * 数字或字母(小写、大写) - 低;
     * 数字+字母(小写、大写) - 中;
     * 数字+大小写字母+特殊符号 - 高
     */

    //创建正则对象 
    /**注意事项:如果正则表达式设置了全局标志,
     * test() 的执行会改变正则表达式 lastIndex属性,
     * 连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串,
     * exec() 同样改变正则本身的 lastIndex 属性值
     */ 
    let num = new RegExp(/^\d+$/);              //仅有数字
    let letter = new RegExp(/^[a-zA-Z]+$/);     //仅有字母
    let anyNum = new RegExp(/\d+/);             //至少包含一个数字
    let anyLetter = new RegExp(/[a-zA-Z]+/);    //至少包含大小写字母任意一种
    let special = new RegExp(/[^0-9a-zA-Z]+/);  //至少包含一个特殊字符
    //获取输入的密码,并用正则验证
    let pwd = document.getElementById("pwd_value");
    let checkPwdSimple = document.getElementById("pwd_check").firstElementChild;
    let checkPwdMiddle = document.getElementById("pwd_check").firstElementChild.nextElementSibling;
    let checkPwdStrong = document.getElementById("pwd_check").lastElementChild;
    pwd.oninput = function(){
        let pwdValue = document.getElementById("pwd_value").value;
        let checkNolyNum = num.test(pwdValue);
        let checkNolyLetter = letter.test(pwdValue);
        let checkAnyNum = anyNum.test(pwdValue);
        let checkAnyLetter = anyLetter.test(pwdValue);
        let checkSpecial = special.test(pwdValue);
        if(checkNolyNum || checkNolyLetter || (checkSpecial && pwdValue.length<3)){    //检测低强度:字母或数字仅有其中一个或纯字符长度小于3
            checkPwdSimple.classList.add("pwd-check-low");
            checkPwdMiddle.classList.remove("pwd-check-middle");
            checkPwdStrong.classList.remove("pwd-check-high");
        } else if (checkAnyNum && checkAnyLetter){  //检测中强度:字母+数字
            checkPwdMiddle.classList.add("pwd-check-middle");
            //检测高强度:字母+数字基础上,再加特殊字符
            if (checkSpecial) {
                checkPwdStrong.classList.add("pwd-check-high")
            } else {
                checkPwdStrong.classList.remove("pwd-check-high"); 
            }
        } else if(checkSpecial && pwdValue.length>=3) { //检测高强度:纯字符长度大于等于3
            checkPwdSimple.classList.add("pwd-check-low");
            checkPwdMiddle.classList.add("pwd-check-middle");
            checkPwdStrong.classList.add("pwd-check-high")
        } else {    //删除为空白,不显示强弱
            checkPwdSimple.classList.remove("pwd-check-low");
            checkPwdMiddle.classList.remove("pwd-check-middle");
            checkPwdStrong.classList.remove("pwd-check-high"); 
        }
    }
    hide.onclick = function(){
        if (hide.className == "hide") {
            hide.className = "see";
            pwd.type = "text";
        } else {
            hide.className = "hide";
            pwd.type = "password";
        }
    }
</script>
</html>

正则验证密码强度.gif

Reference

www.codejiaonang.com/#/

ihateregex.io/

regexr-cn.com/

regex101.com/