正则的匹配
字符匹配
简单的精确匹配自不用说,如/Hello world/
, 只能匹配’Hello world’字符串。正则匹配的更大用处在于其模糊匹配。
数量的匹配
用{m, n}
来匹配数值从m到n的多种数量。*+?
也可以达到类似效果。
const re = /\d{2, 5}/g
const string = '123 1234 12345 123456'
console.log(string.match(re)) // ["123", "1234", "12345", "12345"]
贪婪和非贪婪:默认进行贪婪匹配,加?
可变更为非贪婪匹配。
const re = /\d{2,5}/g
const re2 = /\d{2,5}?/g
const string = '123 1234 12345 123456'
console.log(string.match(re)) // ["123", "1234", "12345", "12345"]
console.log(string.match(re2)) // ["12", "12", "34", "12", "34", "12", "34", "56"]
多种情况的匹配
用字符组(character set)来匹配多种情况,其他几种形式都可等价成这种,例如\d\D\s\S\w\W
const re = /a[123]b/g
const string = 'a0b a1b a2b a3b a4b a12b'
console.log(string.match(re)) // ["a1b", "a2b", "a3b"]
如果字符组里面字符很多的话,可用-
表示范围。例如[abcd]
等价于[a-d]
。多种情况也可以是多种分支,用|
来表示逻辑或。
const re = /good|goodbye/g
const string = 'goodbye'
console.log(string.match(re)) // ["good"]
一些常见的正则匹配的例子
- 最多保留两位数字的小数:
/^([1-9]\d*|0)(.\d{1,2})?/
- 电话号码:
/(+86)?1\d{10}/
- 身份证:
/^(\d{15}|\d{17}([xX]|\d))$/
位置匹配
位置是指相邻字符之间的空隙,例如字符串hello
,这个字符串有6个位置(*号表示处),*h*e*l*l*o*
。
常见的位置元字符包括
-
^
和$
分别匹配开头和结尾,例如'hello'.replace(/^|$/g, '#') // '#hello#'
-
\b
和\B
分别匹配单词边界和非单词边界。单词边界是指\w([a-zA-Z0-9_])
和\W
之间的位置,也包括\w
和起始(^$)
字符间的位置。例如'hello word [js]_reg.exp-01'.replace(/\b/g, '#') // #hello# #word# [#js#]#_reg#.#exp#-#01#
-
(?=p)
和(?!p)
匹配p前面的位置和非前面的位置,例如'hello'.replace(/(?=l)/g, '#') // he#l#lo 'hello'.replace(/(?!l)/g, '#') // #h#ell#o#
例子
千分位,将123123123转换为 123,123,123。数字是从后往前数,也就是以一个或者多个3位数字结尾的位置换成 ‘,’ 并且不能包括首位。还需要设置首位为非单词边界,\B
。
String(123123123).replace(/(?=(\B\d{3})+$)/g, ',') // 123,123,123
注意:如果要求一个正则是匹配位置的话,那么所有的条件必须都是位置。
分组和分支结构
在分支结构中,括号用来表示一个整体(p1|p2)
,并对其进行捕获,例如要匹配以下的字符串
I love JavaScript.
I love Regular Expression.
可以用表达式/^I love (JavaScriipt|Regular Expression)$/
而非/^I love JavaScript|Regular Expression/
。表示一个整体还比如/(abc)+/
一个或者多个abc
字符串上面这些使用 () 包起来的地方就叫做分组。
'I love JavaScript'.match(/^I love (JavaScript|Regular Expression)$/)
// ["I love JavaScript", "JavaScript", index: 0, input: "I love JavaScript"]
输出的数组第二个元素,”JavaScript” 就是分组匹配到的内容。
引用分组
提取数据
比如匹配一个日期格式 yyyy-mm-dd,可以写成简单的/\d{4}-\d{2}-\d{2}/
,也可以写成分组的形式/(\d{4})-(\d{2})-(\d{2})/
。这样我们可以分别提取出一个日期的年月日,用 String 的 match 方法或者用正则的 exec 方法都可以。
const re = /(\d{4})-(\d{2})-(\d{2})/g
const string = '2018-01-01'
console.log(string.match(re))
// ["2018-01-01", "2018", "01", "01", index: 0, input: "2018-01-01"]
也可以用正则对象构造函数的全局属性 $1
- $9
来获取
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const string = "2018-01-01";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2018"
console.log(RegExp.$2); // "01"
console.log(RegExp.$3); // "01"
替换数据
将 yyyy-mm-dd 格式的数据转换为 mm/dd/yyyy。 String 的 replace 方法在第二个参数里面可以用 $1 - $9 来指代相应的分组。
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const string = "2018-01-01";
const result = string.replace(regex, "$2/$3/$1");
console.log(result); // "01/01/2018"
等价
const result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); // "01/01/2018"
等价
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const string = "2018-01-01";
const result = string.replace(regex, function(match, year, month, day) {
return month + "/" + day + "/" + year;
});
console.log(result); // "01/01/2018"
反向引用
常见的日期写法有三种
2018-01-01
2018/01/01
2018.01.01
对以上三种写法进行匹配。此时就需要用到反向引用了,反向引用可以在匹配阶段捕获到分组的内容。/(\d{4})([\.-])(\d{2})\2(\d{2})/
若出现括号嵌套的话,以左括号为准。
const re = /^((\d)(\d(\d)))\1\2\3\4$/;
const string = "1231231233";
console.log( re.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
引用了不存在的分组
如果在正则里面引用了前面不存在的分组,这个时候正则会匹配字符本身,比如\1
就匹配\1
非捕获分组
有时候只是想用括号原本的功能而不想捕获他们。这个时候可以用(?:p)
表示一个非捕获分组
例子
- 驼峰变短横
const camel2dash = string => {
return string.replace(/([A-Z])/g, '-$1').toLowerCase()
}
const str = 'regExp'
console.log(camel2dash(str)) // 'reg-exp'
- 获取 URL 的 search 值。
https://www.baidu.com/file?name=foo&age=20
function getParamName(attr) {
let match = RegExp(`[?&]${attr}=([^&]*)`) //分组运算符是为了把结果存到exec函数返回的结果里
.exec(window.location.search)
//["?name=jawil", "jawil", index: 0, input: "?name=jawil&age=23"]
return match && decodeURIComponent(match[1].replace(/\+/g, ' ')) // url中+号表示空格,要替换掉
}
console.log(getParamName('name')) // "jawil"
- 去掉字符串前后的空格
function trim(str) {
return str.replace(/(^\s*)|(\s*$)/g, "")
}
回溯
回溯的思想是,从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”贪婪和非贪婪的匹配都会产生回溯,不同的是贪婪的是先尽量多的匹配,如果不行就吐出一个然后继续匹配,再不行就再吐出一个,非贪婪的是先尽量少的匹配。如果不行就再多匹配一个,再不行就再来一个分支结构也会产生回溯,比如/^(test te)sts$/.test(‘tests’) 前面括号里面的匹配过程是先匹配到 test 然后继续往后匹配匹配到字符 s 的时候还是成功的,匹配到 st 的时候发现不能匹配, 所以会回到前面的分支结构的其他分支继续匹配,如果不行的话再换其他分支。
结构和操作符
结构:字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用
操作符:
- 转义符
\
- 括号和方括号
(...) (?:...) (?=...) (?!...) [...]
- 量词限定符
{m,n} {m,} {m} * + ?
- 位置和序列
^ $ \元字符 一般字符
- 管道符
|
操作符的优先级是从上到下,由高到低的,所以在分析正则的时候可以根据优先级来拆分正则,比如
/ab?(c|de*)+|fg/
- 因为括号是一个整体,所以
/ab?()+|fg/
,括号里面具体是什么可以放到后面再分析 - 根据量词和管道符的优先级,所以
a
,b?
,()+
和管道符后面的f
,g
- 同理分析括号里面的
c|de*
=>c
和d
,e*
- 综上,这个正则描述的是