正则表达式

301 阅读5分钟

正则表达式要么匹配字符,要么匹配位置。

1. 字符匹配

正则表达式是用于字符串模糊匹配的强大工具,模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。

  • 横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。实现的方式是使用量词。譬如 {m,n},表示连续出现最少 m 次,最多 n 次。比如正则 /ab{2,5}c/ 表示匹配这样一个字符串:第一个字符是 "a",接下来是 2 到 5 个字符 "b",最后 是字符 "c"。
  • 纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。其实现的方式是使用字符组。譬如 [abc],表示该字符是可以字符 "a"、"b"、"c" 中的任何一个。比如 /a[123]b/ 可以匹配如下三种字符串: "a1b"、"a2b"、"a3b"。

1.1. 字符组

字符组用来匹配一类字符中的一个字符。

  • 基础字符组,[abc],表示匹配一个字符,它可以是 "a"、"b"、"c" 之一。
  • 连字符简写,连续的数字和字母可以用连字符“-”来进行简写,[123456abcdefGHIJKLM],可以写成 [1-6a-fG-M]
  • 转义字符,例如要匹配连词符,可以使用[a-z]
  • 排除字符组, [^abc],表示是一个除 "a"、"b"、"c"之外的任意一个字符。字符组的第一位放 ^(脱字符),表示求反的概念。
  • 系统自带的简写形式

    • 怎么匹配任意字符? [\d\D]、[\w\W]、[\s\S] 和 [^] 中任何的一个。

1.2. 量词

量词表示字符的重复次数。掌握 {m,n} 的准确含义后,只需要记住一些简写形式。

量词默认是贪婪匹配,优先匹配更多的数量

var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["123", "1234", "12345", "12345"]

再量词后面加“?”,就是惰性匹配,优先匹配更少的数量

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']

1.3. 多选分支

多选分支用来实现多种模糊匹配的组合。具体形式如下:(p1|p2|p3),其中 p1、p2 和 p3 是子模式。

var regex = /good|nice/g;
var string = "good idea, nice try.";
console.log( string.match(regex) );
// => ["good", "nice"]

多选分支是惰性的,优先匹配前面的子模式

var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) );
// => ["good"]

而把正则改成 /goodbye|good/,结果是:

var regex = /goodbye|good/g;
var string = "goodbye";
console.log( string.match(regex) );
// => ["goodbye"]

2. 匹配位置

位置就是字符两侧的位置

2.1. 开头与结尾

^(脱字符)匹配开头,在多行匹配中匹配行开头。^p,表示匹配满足p模式的开头

$(美元符号)匹配结尾,在多行匹配中匹配行结尾。p$,表示匹配满足p模式的结尾

var result = "hello".replace(/^|$/g, '#');
console.log(result);
// => "#hello#"
const regExp = /^h/g
console.log('hello'.match(regExp));
// ['h']
console.log('ello'.match(regExp));
// null

2.2. 单词边界

\b 是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置。

var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// => "[#JS#] #Lesson_01#.#mp4#"

\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。

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"

2.3. 匹配模式之前

(?=p),其中 p 是一个子模式,即 p 前面的位置

var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"

而 (?!p) 就是 (?=p) 的反面意思,比如:

var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"

3. 正则中的括号

括号的作用是提供了分组,便于我们引用它。引用某个分组,会有两种情形:在 JavaScript 里引用它,在正则表达式里引用它。

3.1. 分组与分支

括号可以用来分组,强调括号内的正则是一个整体。例如 /a+/ 匹配连续出现的 "a",而要匹配连续出现的 "ab" 时,需要使用 /(ab)+/。

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]

分支如1.3中提到的,括号内表示分支的所有可能

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"

3.2. 分组引用

括号的另一个重要作用是获取括号内的引用,可以进行数据提取等操作

替换操作

把yyyy-mm-dd格式,替换成mm/dd/yyyy

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"

4. 正则表达式编程

上面的操作都是正则的匹配规则,接下来是介绍怎么在JavaScript编程中使用正则表达式

4.1. 验证

验证是正则表达式最直接的应用,比如表单验证,判断输入的字符串是否满足某个规则。

RegExp.prototype.test

var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) );
// => true

String.proptotype.match

var regex = /\d/;
var string = "abc123";
console.log( !!string.match(regex) );
// => true

没有修饰符/g,只匹配第一个,匹配成功返回对象,匹配失败返回null

let text = "cat, bat, sat, fat";
let pattern = /.at/; 
let matches = text.match(pattern);  
// 与patten.exec(text)结果一致console.log(matches); 
// 输入结果如下:{    
//    0: "cat",    
//    index: 0,    
//    input: "cat, bat, sat, fat",   
//     groups: undefined,    length: 1
// }

有修饰符/g,匹配成功返回匹配的数组,匹配失败返回null

let text = "cat, bat, sat, fat";
let pattern = /.at/g; 
let matches = text.match(pattern);
console.log(matches); // 输出结果如下:["cat", "bat", "sat", "fat"]

4.2. 拆分

根据正则来拆分字符串

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"]

4.3. 提取

使用括号与RegExpt.prototype.test完成

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
string.search(regex);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

4.4. 替换

var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2");
console.log(result);
// => "5=2+3"

replace的第二个参数还可以是一个函数,这样会极大的增强了replace的功能

const str = "1234 2345 3456";
const str2 = str.replace(
  /(\d)\d{2}(\d)/g,
  function (match, $1, $2, index, input) {
    console.log([match, $1, $2, index, input]);
    return `${$1}${$2}`;
  }
);
console.log(str2); // 14 25 36
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]

除了替换之外,也可以在第二个函数中实现其他功能,例如提取:

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
var date = [];
string.replace(regex, function (match, year, month, day) {
  date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]

5. 案例

5.1. 匹配十六进制颜色值

#ffbbad
#Fc01DF
#FFF
#ffE

正则如下:

/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g

5.2. 匹配时间

22:59

正则如下:

/^([01][0-9]|[2][0-4]):[0-6][0-9]$/

5.3. 不匹配任何字符

// 因为此正则要求只有一个字符,但该字符后面是开头,而这样的字符串是不存在的。
/.^/

5.4. 千分位分割

比如把 "12345678",变成 "12,345,678"。

(/(?=(\d{3})+$)/g,匹配3的倍数个数字前的位置

var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')
console.log(result);
// => ",123,456,789"

如果数字位数是3的倍数,会导致最开头也会有逗号,因为需要匹配非开头且3的倍数个数字前的位置

var regex = /(?!^)(?=(\d{3})+$)/g;
var result = "12345678".replace(regex, ',')
console.log(result);
// => "12,345,678"
result = "123456789".replace(regex, ',');
console.log(result);
// => "123,456,789"

如果要把 "12345678 123456789" 替换成 "12,345,678 123,456,789"。

var string = "12345678 123456789",
regex = /(?!\b)(?=(\d{3})+\b)/g;
var result = string.replace(regex, ',')
console.log(result);
// => "12,345,678 123,456,789"

千分位分割的应用之一就是货币格式化:

function format (num, decimal = 2) {
  return num.toFixed(decimal).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$ ");
};
console.log( format(1888) );
// => "$ 1,888.00"

5.5. 字符串trim操作

匹配到开头和结尾的空白符,然后替换成空字符

function trim(str) {
  return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim(" foobar ") );
// => "foobar"

5.6. 驼峰化

中划线转驼峰

function kebabToCamelCase(str) {
    return str
        .replace(/-./g, match => match.charAt(1).toUpperCase());
}

// 示例
const input = "hello-world-this-is-a-test";
console.log(kebabToCamelCase(input)); // 输出: "helloWorldThisIsATest"

5.7. 匹配手机号

const chinaPhoneRegex = /^1[3-9]\d{9}$/;

const phoneNumber = '13812345678';
console.log(chinaPhoneRegex.test(phoneNumber)); // 输出: true

参考

JS正则迷你书