正则的匹配

regexp

Posted by Ericteen on March 1, 2018

正则的匹配

字符匹配

简单的精确匹配自不用说,如/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)表示一个非捕获分组

例子

  1. 驼峰变短横
const camel2dash = string => {
  return string.replace(/([A-Z])/g, '-$1').toLowerCase()
}

const str = 'regExp'

console.log(camel2dash(str)) // 'reg-exp'
  1. 获取 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"
  1. 去掉字符串前后的空格
function trim(str) {
 return str.replace(/(^\s*)|(\s*$)/g, "")
}

回溯

回溯的思想是,从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”贪婪和非贪婪的匹配都会产生回溯,不同的是贪婪的是先尽量多的匹配,如果不行就吐出一个然后继续匹配,再不行就再吐出一个,非贪婪的是先尽量少的匹配。如果不行就再多匹配一个,再不行就再来一个分支结构也会产生回溯,比如/^(test te)sts$/.test(‘tests’) 前面括号里面的匹配过程是先匹配到 test 然后继续往后匹配匹配到字符 s 的时候还是成功的,匹配到 st 的时候发现不能匹配, 所以会回到前面的分支结构的其他分支继续匹配,如果不行的话再换其他分支。

结构和操作符

结构:字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用

操作符:

  1. 转义符 \
  2. 括号和方括号 (...) (?:...) (?=...) (?!...) [...]
  3. 量词限定符 {m,n} {m,} {m} * + ?
  4. 位置和序列 ^ $ \元字符 一般字符
  5. 管道符 |

操作符的优先级是从上到下,由高到低的,所以在分析正则的时候可以根据优先级来拆分正则,比如 /ab?(c|de*)+|fg/

  1. 因为括号是一个整体,所以/ab?()+|fg/,括号里面具体是什么可以放到后面再分析
  2. 根据量词和管道符的优先级,所以a, b?, ()+和管道符后面的f, g
  3. 同理分析括号里面的c|de* => cd, e*
  4. 综上,这个正则描述的是

正则描述