ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
>[info] 部分内容摘自老姚的《JavaScript 正则表达式迷你书》,[链接](https://github.com/qdlaoyao/js-regex-mini-book)。 [TOC] # 贪婪匹配与惰性匹配 什么是正则表达式的贪婪与惰性匹配?来看下面这段代码: ```js let str = "abcaxc" p1 = /ab.*c/ p2 = /ab.*?c/ console.log(p1.exec(str)) // [ 'abcaxc', index: 0, input: 'abcaxc', groups: undefined ] console.log(p2.exec(str)) // [ 'abc', index: 0, input: 'abcaxc', groups: undefined ] ``` 贪婪匹配:正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。如上面使用模式 p1 匹配字符串 str,结果就是匹配到:`abcaxc` 惰性匹配:就是匹配到结果就好,尽可能少地匹配。如上面使用模式 p2 (p1 的惰性模式)匹配字符串 str,结果就是匹配到:`abc` 通过在量词后面加个问号就能实现惰性匹配,比如下面这样: | 贪婪量词 | 惰性量词 | | --- | --- | | {m, n} | {m, n}? | | {m, } | {m, }? | | ? | ?? | | + | +? | ```js var regex = /\d{2,5}/g var string = "123 1234 12345 123456" console.log( string.match(regex) ) // => ["123", "1234", "12345", "12345"] ``` ``` js var regex = /\d{2,5}?/g var string = "123 1234 12345 123456" console.log( string.match(regex) ) // => ["12", "12", "34", "12", "34", "12", "34", "56"] ``` # 位置匹配 位置(锚)是相邻字符之间的位置。比如,下图中箭头所指的地方 ![](https://box.kancloud.cn/feca1993f09361ede170e2019aaeb6b2_701x159.png =400x) 如何匹配位置?在 ES5 中,共有 6 个锚: `^、$、\b、\B、(?=p)、(?!p)` 相应的可视化形式是: ![](https://box.kancloud.cn/54318cbe637d2a72630d094f9562d3c4_937x139.png ) ## ^ 和 $ ^(脱字符)匹配开头,在多行匹配中匹配行开头。 $(美元符号)匹配结尾,在多行匹配中匹配行结尾。 比如我们把字符串的开头和结尾用 "#" 替换: ```js var result = "hello".replace(/^|$/g, '#'); console.log(result); // => "#hello#" ``` 多行匹配模式(即有修饰符 m)时,二者是行的概念,这一点需要我们注意 ```js var result = "I\nlove\njavascript".replace(/^|$/gm, '#'); console.log(result); /* #I# #love# #javascript# */ ``` ## \\b 和 \\B `\b`是单词边界,具体就是`\w`与`\W`之间的位置,也包括`\w`与`^`之间的位置,和`\w`与`$`之间的位置。 比如考察文件名`"[JS] Lesson\01.mp4"`中的`\b`,如下: ```js var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#'); console.log(result); // => "[#JS#] #Lesson_01#.#mp4#" ``` `\B`就是`\b`的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉`\b`,剩下的都是`\B`的 ```js var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#'); console.log(result); // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4" ``` ## (?=p) 和 (?!p) `(?=p)`,其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p。 比如`(?=l)`,表示 "l" 字符前面的位置,例如: ```js var result = "hello".replace(/(?=l)/g, '#'); console.log(result); // => "he#l#lo" ``` 而`(?!p)`就是`(?=p)`的反面意思,即该位置的后面不匹配 p: ```js var result = "hello".replace(/(?!l)/g, '#'); console.log(result); // => "#h#ell#o#" ``` ## 例题 千分符表示法一个常见的应用就是货币格式化。 比如把下面的字符串: `1888`格式化为`$ 1,888.00` ```js function format (num) { return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$$ "); // replace 函数里两个 $$ 才能表示美元符号,因为第二个参数 $ 有特殊含义 }; console.log(format(1888)); // => "$ 1,888.00" ``` # 正则表达式括号的作用 ## 分组 我们知道 `/a+/` 匹配连续出现的 `"a"`,而要匹配连续出现的 `"ab"` 时,需要使用 `/(ab)+/`。 其中括号是提供分组功能,使量词 `+` 作用于 `"ab"` 这个整体,测试如下: ```js var regex = /(ab)+/g; var string = "ababa abbb ababab"; console.log( string.match(regex) ); // => ["abab", "ab", "ababab"] ``` ## 分支结构 在多选分支结构`(p1|p2)`中,此处括号的作用也是不言而喻的,提供了分支表达式的所有可能。 比如,要匹配如下的字符串: ```js I love JavaScript I love Regular Expression ``` 可以使用正则: ```js var regex = /^I love (JavaScript|Regular Expression)$/; console.log( regex.test("I love JavaScript") ); console.log( regex.test("I love Regular Expression") ); // => true // => true ``` 如果去掉正则中的括号,即`/^I love JavaScript|Regular Expression$/` 匹配字符串是 "I love JavaScript" 和 "Regular Expression",当然这不是我们想要的。 ## 替换 比如,想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做? ```js var regex = /(\d{4})-(\d{2})-(\d{2})/; var string = "2017-06-12"; var result = string.replace(regex, "$2/$3/$1"); console.log(result); // => "06/12/2017" ``` 其中 replace 中的,第二个参数里用 $1、$2、$3 指代相应的分组 ## 反向引用 在正则本身里引用之前的分组,即反向引用,如 \\1 表示第一个分组 比如要写一个正则支持匹配如下三种格式 ```js 2016-06-12 2016/06/12 2016.06.12 ``` ```js var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/; var string1 = "2017-06-12"; var string2 = "2017/06/12"; var string3 = "2017.06.12"; var string4 = "2016-06/12"; console.log( regex.test(string1) ); // true console.log( regex.test(string2) ); // true console.log( regex.test(string3) ); // true console.log( regex.test(string4) ); // false ``` # JavaScript 中使用正则 ## RegExp 创建正则表达式,两种写法,一种是直接`/正则表达式/`,另一种是使用 RegExp 的构造方法 `new RegExp(pattern, attributes)`传入的参数都为字符串/字符,第二个参数是修饰符 'i'、'g'、'm' >[danger]应该优先使用字面量形式,因为用构造函数会写很多 \ ```js var re1 = /ABC\-001/ var re2 = new RegExp('ABC\\-001') re1 // /ABC\-001/ re2 // /ABC\-001/ ``` 一个 RegExp 对象有 exec 和 test 方法,比如上面的 re1 和 re2 可以这么使用`re1.test(str)` - test 方法检索字符串是否满足正则匹配。返回 true 或 false。 - exec 方法执行对字符串的正则匹配。返回一个数组包含相关信息(如果不满足则返回 null)。 ```js let string = "2017.06.27" let reg = /\b(\d+)\b/g // \b 匹配单词边界 let result while (result = reg.exec(string)) { console.log(result, reg.lastIndex) } // => ["2017", "2017", index: 0, input: "2017.06.27"] 4 // => ["06", "06", index: 5, input: "2017.06.27"] 7 // => ["27", "27", index: 8, input: "2017.06.27"] 10 ``` exec 方法返回的数组的第 0 个元素是与正则表达式相匹配的文本,第 1 个元素是与 RegExpObject 的第 1 个分组相匹配的文本(如果有的话),第 2 个元素是与 RegExpObject 的第 2 个分组相匹配的文本(如果有的话),依次类推。 <br/> 除了数组元素和 length 属性之外,exec() 方法还返回两个属性(可以通过res.input 和 res.index 来访问)。index 属性声明的是匹配文本的第一个字符的位置。input 属性则存放的是被检索的字符串 string。我们可以看得出,在调用非全局的 RegExp 对象的 exec() 方法时,返回的数组与调用方法 String.match() 返回的数组是相同的。 <br/> 但是,当 RegExpObject 是一个**全局正则表达式**时,exec() 的行为就稍微复杂一些。它会在 RegExpObject 的 lastIndex 属性指定的字符处开始检索字符串 string。当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把 RegExpObject 的 lastIndex 属性设置为匹配文本的**最后一个字符**的下一个位置。这就是说,您可以通过反复调用 exec() 方法来遍历字符串中的所有匹配文本。当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。 # 字符串中可以使用正则表达式的方法 ## match match() 方法只接受一个参数,要么是一个正则表达式,要么是一个 RegExp 对象 match() 方法将检索字符串 stringObject,以找到一个或多个与 regexp 匹配的文本。这个方法的行为在很大程度上有赖于 regexp 是否具有标志 g。 <br/> 如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配。如果没有找到任何匹配的文本, match() 将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index 属性声明的是匹配文本的起始字符在 stringObject 中的位置,input 属性声明的是对 stringObject 的引用。 <br/> 如果 regexp 具有标志 g,则 match() 方法将执行全局检索,找到 stringObject 中的所有匹配子字符串。若没有找到任何匹配的子串,则返回 null。如果找到了一个或多个匹配子串,则返回一个数组。不过全局匹配返回的数组的内容与前者大不相同,它的数组元素中存放的是 stringObject 中所有的匹配子串,而且也没有 index 属性或 input 属性。 可以看到,在全局检索模式下,match() 既不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果需要这些全局检索的信息,可以使用 RegExp.exec()。 ```js var text = "cat, bat, sat, fat" var pattern = /.at/ // 与 pattern.exec(text) 相同 var matches = text.match(pattern) console.log(matches.index) // 0 console.log(matches[0]) // "cat" console.log(pattern.lastIndex) // 0 console.log(matches) // ['cat', index:0, input:'cat,bat,sat,fat' ] ``` ```js var text = "cat, bat, sat, fat" var pattern = /.at/g var matches = text.match(pattern) console.log(matches.index) // undefined console.log(matches[0]) // "cat" console.log(pattern.lastIndex) // 0 console.log(matches) // ['cat','bat','sat','fat'] ``` ## search 这个方法的唯一参数与 match() 方法的参数相同:由字符串或 RegExp 对象指定的一个正则表达式。search() 方法返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回 -1。而且,search() 方法始终是从字符串开头向后查找模式。 >[warning]似乎 RegExp 对象的 exec 和 test 方法可以完美地取代这两个字符串方法? ## replace 这个方法接受两个参数:第一个参数可以是一个 RegExp 对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以是一个字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。**要想替换所有子字符串,唯一的办法就是提供一个正则表达式,而且要指定全局(g)标志**,如下所示 ```js var text = "cat, bat, sat, fat" var result = text.replace("at", "ond") console.log(result) // "cond, bat, sat, fat" result = text.replace(/at/g, "ond") console.log(result) // "cond, bond, sond, fond" ``` 第二个参数,可以是字符串,也可以是函数。 当第二个参数是字符串时,如下的字符有特殊的含义: | 属性 |描述| | --- | --- | |$1,$2,…,$99 |匹配第 1-99 个分组里捕获的文本| |$&| 匹配到的子串文本| |$`| 匹配到的子串的左边文本| |$' |匹配到的子串的右边文本| |$$| 美元符号| 例如,把 "2,3,5",变成 "5=2+3": ```js var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2") console.log(result) // => "5=2+3" ``` 当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么: ```js "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) { console.log([match, $1, $2, index, input]) }) // => ["1234", "1", "4", 0, "1234 2345 3456"] // => ["2345", "2", "5", 5, "1234 2345 3456"] // => ["3456", "3", "6", 10, "1234 2345 3456"] ``` 在正则表达式中定义了多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,最后两个参数分别是模式的匹配项在字符串中的位置(这个匹配项的第一个字符在字符串中的位置)和原始字符串。**这个函数应该返回一个字符串,表示应该被替换的匹配项。** ```js function htmlEscape (text) { return text.replace(/[<>"&]/g, function (match, pos, originalText) { switch (match) { case '<': return '&lt;' case '>': return '&gt;' case '&': return '&amp;' case '\"': return '&quot;' } }) } console.log(htmlEscape(`<p class="greeting">Hello world!</p>`)) // &lt;p class=&quot;greeting&quot;&gt;Hello world!&lt;/p&gt; ``` ## split split 也可以使用正则 ```js var regex = /\D/; console.log( "2017/06/26".split(regex) ); console.log( "2017.06.26".split(regex) ); console.log( "2017-06-26".split(regex) ); // => ["2017", "06", "26"] // => ["2017", "06", "26"] // => ["2017", "06", "26"] ``` # 例题 ## 1.使用正则表达式去除字符串中重复的字符 ``` js var str = "aaabbb___cccddd" str = str.replace(/(.)\1*/g, '$1') console.log(str) // ab_cd ``` `\1`用于正则表达式内取值,取的是第一个分组匹配到的值。 `$1`用于正则表达式外取值, 取的是第一个分组匹配到的值。 ## 2.验证手机号 ``` js var reg = /^1[3578]\d{9}/ var str = '15616460659' console.log(reg.test(str)) // true ``` ## 3.写函数实现任意标签转换成 json 形式 ```javaScript /* <div> <span> <a></a> </span> <span> <a></a> <a></a> </span> </div> */ function DOM2JSON(str) { let reg = /<(.+)>(.*?)<\/\1>/g // 注意 .\*? 是惰性匹配,如果使用 .\* 这样的情况会出问题: <span><a></a></span><span></span> 不会最短地闭合 let result = null let nodes = [] while((result = reg.exec(str)) !== null) { // 当 exec() 再也找不到匹配项后它将返回 null,并把 lastIndex 属性重置为 0 nodes.push({ tag: result[1], children: DOM2JSON(result[2])}) // exec 返回的数组,[0]匹配的字符串 然后依次是捕获的分组 然后有 index 和 input 属性 } return nodes.length > 1 ? nodes : nodes[0] } console.log(JSON.stringify(DOM2JSON('<div><span><a></a></span><span><a></a><a></a></span></div>'))) // {"tag":"div","children":[{"tag":"span","children":{"tag":"a"}},{"tag":"span","children":[{"tag":"a"},{"tag":"a"}]}]} ``` 这里主要利用了 exec 函数会在上一次匹配的结果之后继续匹配,且如果未匹配成功会返回 null,然后注意下 exec 和正则表达式分组的使用即可。 ## 4.匹配 16 进制颜色 要求匹配: ```css #ffbbad #Fc01DF #FFF #ffE ``` 分析: 表示一个 16 进制字符,可以用字符组 [0-9a-fA-F]。 其中字符可以出现 3 或 6 次,需要是用量词和分支结构。 使用分支结构时,需要注意顺序。 ```js var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; var string = "#ffbbad #Fc01DF #FFF #ffE"; console.log( string.match(regex) ); // => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"] ``` ## 5.windows 操作系统文件路径 要求匹配: ```shell F:\study\javascript\regex\regular expression.pdf F:\study\javascript\regex\ F:\study\javascript F:\ ``` 分析: 整体模式是: ```shell 盘符:\文件夹\文件夹\文件夹\ ``` 其中匹配 `"F:\"`,需要使用 `[a-zA-Z]:\\`,其中盘符不区分大小写,注意 \ 字符需要转义。 文件名或者文件夹名,不能包含一些特殊字符,此时我们需要排除字符组 `[^\\:*<>|"?\r\n/]`来表示合法字符。 另外它们的名字不能为空名,至少有一个字符,也就是要使用量词 +。因此匹配 `文件夹\`,可用 `[^\\:*<>|"?\r\n/]+\\` 另外 `文件夹\`,可以出现任意次。也就是 `([^\\:*<>|"?\r\n/]+\\)*`。其中括号表示其内部正则是一个整体。 路径的最后一部分可以是 文件夹,没有 \,因此需要添加 `([^\\:*<>|"?\r\n/]+)?`。 ```js var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/; console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") ); // 在JavaScript 中字符串要表示字符 \\ 时,也需要转义 console.log( regex.test("F:\\study\\javascript\\regex\\") ); console.log( regex.test("F:\\study\\javascript") ); console.log( regex.test("F:\\") ); // => true // => true // => true // => true ``` # 速查表 ## 字符组 | 模式 | 说明 | | --- | --- | | [abc] | 匹配 "a"、"b"、"c" 其中任意一个字符 | | [a-d1-4] | 匹配 "a"、 "b"、 "c"、 "d"、 "1"、 "2"、 "3"、 "4" 其中任意一个字符 | | [^abc] | 匹配除了 "a"、"b"、"c" 之外的任意一个字符 | | [^a-d1-4]] | 匹配除了 "a"、 "b"、 "c"、 "d"、"1"、 "2"、 "3"、"4" 之外的任意一个字符 | | . | 通配符,匹配除了少数字符 (\\n) 之外的任意字符 | | \d | 匹配数字,等价于 [0-9] | | \D | 匹配非数字,等价于 [^0-9] | | \w | 匹配单词字符,等价于 [a-zA-Z0-9_] | | \W | 匹配非单词字符,等价于 [^a-zA-Z0-9_] | | \s | 匹配空白符,等价于 [\\t \\v \\n \\r \\f] | | \S | 匹配非空白符,等价于 [^\\t \\v \\n \\r \\f] | ## 量词 | 模式 | 说明 | | --- | --- | | {n, m} | 连续出现 n 到 m 次 | | {n, } | 至少连续出现 n 次 | | {n} | 连续出现 n 次 | | ? | 等价于 {0,1},0 次或 1 次 | | + | 等价于 {1, } 1 次及以上 | | * | 等价于 {0, } 0 次及以上 | ## 修饰符 | 符号 | 说明 | | --- | --- | | g | 全局匹配,找到所有满足匹配的子串 | | i | 匹配过程中,忽略英文字母大小写 | | m | 多行匹配,把 ^ 和 $ 变成行开头和结尾 | ## 元字符转义 所谓元字符,就是正则中有特殊含义的字符。 所有结构里,用到的元字符总结如下: `^`、`$`、`.`、`*`、`+`、`?`、`|`、`\`、`/`、`(`、`)`、`[`、`]`、`{`、`}`、`=`、`!`、`:`、`-` 当匹配上面的字符本身时,可以一律转义: ```js var string = "^$.*+?|\\/[]{}=!:-,"; var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/; console.log( regex.test(string) ); // => true ```