一看就会,一做就懵的正则表达式,你会了吗?

137 阅读5分钟

正则表达式是搜索和替换字符串的一种强大方式。

在 JavaScript 中,正则表达式通过内建的“RegExp”类的对象来实现,并与字符串集成。

请注意,在各编程语言之间,正则表达式是有所不同的。在本教程中,我们只专注于 JavaScript。

本文旨在汇总整理,非原创。

正则表达式

语法一:

regexp = new RegExp("pattern", "flags");

语法二:

regexp = /pattern/; // 没有修饰符
regexp = /pattern/gmi; // 伴随修饰符 g、m 和 i

区别:

/***/语法简练,但不接受传参,一般在确定匹配规则的情况下使用,new RegExp 允许从字符串中动态地构造模式

修饰符

在 JavaScript 中,有 6 个修饰符:

i

使用此修饰符后,搜索时不区分大小写: A 和 a 没有区别。

g

使用此修饰符后,搜索时会查找所有的匹配项,而不只是第一个。

m

多行模式。

s

启用 “dotall” 模式,允许点 . 匹配换行符 \n。

u

开启完整的 unicode 支持。该修饰符能够修正对于代理对的处理。

y

粘滞模式

字符类

字符类(Character classes) 是一个特殊的符号,匹配特定集中的任何符号。

\d(“d” 来自 “digit”)

数字:从 0 到 9 的字符。

\s(“s” 来自 “space”)

空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v\f\r

\w(“w” 来自 “word”)

“单字”字符:拉丁字母或数字或下划线 _。非拉丁字母(如西里尔字母或印地文)不属于 \w

反向类

\D

非数字:除 \d 以外的任何字符,例如字母。

\S

非空格符号:除 \s 以外的任何字符,例如字母。

\W

非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。

小结

\d —— 数字。

\D —— 非数字。

\s —— 空格符号,制表符,换行符。

\S —— 除了 \s

\w —— 拉丁字母,数字,下划线 '_'。

\W —— 除了 \w

. —— 任何带有 's' 标志的字符,否则为除换行符 \n之外的任何字符。

锚点

插入符号 ^ 匹配文本开头,而美元符号 $ - 则匹配文本末尾。

什么字符串可以匹配模式 ^$?

空字符串 ""

"m" — 多行模式

通过 flag /.../m 可以开启多行模式。

这仅仅会影响 ^$ 锚符的行为。

在多行模式下,它们不仅仅匹配文本的开始与结束,还匹配每一行的开始与结束。

let str = `1st aaa
2nd bbb
33rd ccc`;

alert( str.match(/^\d+/gm) ); // 1, 2, 33

词边界:\b

当正则表达式引擎(实现搜索正则表达式的程序模块)遇到 \b 时,它会检查字符串中的位置是否是词边界。

alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null

集合

比如说,[eao] 意味着查找在 3 个字符 'a'、'e' 或者 ‘o’ 中的任意一个。

范围

方括号也可以包含字符范围。

比如说,[a-z] 会匹配从 a 到 z 范围内的字母,[0-9] 表示从 0 到 9 的数字。

\d —— 和 [0-9] 相同,

\w —— 和 [a-zA-Z0-9_] 相同,

\s —— 和 [\t\n\v\f\r ] 外加少量罕见的 unicode 空格字符相同。

排除范围

除了普通的范围匹配,还有类似 [^…] 的“排除”范围匹配。

[^aeyo] —— 匹配任何除了 'a'、'e'、'y' 或者 'o' 之外的字符。

[^0-9] —— 匹配任何除了数字之外的字符,也可以使用 \D 来表示。

[^\s] —— 匹配任何非空字符,也可以使用 \S 来表示。

量词 +,*,?{n}

数量 {n} 明确的n次

某个范围的位数:{n,m} n-m次匹配区间

+ 代表“一个或多个”,相当于 {1,}

? 代表“零个或一个”,相当于 {0,1}

* 代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现。

贪婪模式

在贪婪模式下(默认情况下),量词都会尽可能地重复多次。

懒惰模式

懒惰模式中的量词与贪婪模式中的是相反的。它想要“重复最少次数”。

我们能够通过在量词之后添加一个问号 '?' 来启用它,所以匹配模式变为 *?+?,甚至将 '?' 变为 ??

捕获组

模式的一部分可以用括号括起来 (...)。这称为“捕获组(capturing group)”。

这有两个影响:

  1. 它允许将匹配的一部分作为结果数组中的单独项。
  2. 如果我们将量词放在括号后,则它将括号视为一个整体。
alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo"

命名组

用数字记录组很困难。对于简单模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号起个名字。

这是通过在开始括号之后立即放置 ?<name> 来完成的。

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";

let groups = str.match(dateRegexp).groups;

alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
替换捕获组

方法 str.replace(regexp, replacement) 用 replacement 替换 str 中匹配 regexp 的所有捕获组。这使用 $n 来完成,其中 n 是组号。

let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020
非捕获组 ?:
let str = "Gogogo John!";
// ?: 从捕获组中排除 'go'
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);

alert( result[0] ); // Gogogo John(完全匹配)
alert( result[1] ); // John
alert( result.length ); // 2(数组中没有更多项)

模式中的反向引用:\N\k<name>

let str = `He said: "She's the one!".`;
let regexp = /(['"])(.*?)\1/g;

alert( str.match(regexp) ); // "She's the one!"

不要搞混了: 在模式中用 \1,在替换项中用:$1

在替换字符串中我们使用美元符号:$1,而在模式中 – 使用反斜杠 \1

let str = `He said: "She's the one!".`;
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;

alert( str.match(regexp) ); // "She's the one!"

选择(OR)|

let reg = /html|php|css|java(script)?/gi;
let str = "First HTML appeared, then CSS, then JavaScript";

alert( str.match(reg) ); // 'HTML', 'CSS', 'JavaScript'

gr(a|e)y 严格等同 gr[ae]y。

gra|ey 匹配 “gra” or “ey”。

前瞻断言与后瞻断言

前瞻断言

语法为:x(?=y),它表示“仅在后面是 y 的情况匹配 x”。

那么对于一个后面跟着 € 的整数金额,它的正则表达式应该为:\d+(?=€)

前瞻否定断言

语法为:x(?!y),意思是 “查找 x, 但是仅在不被 y 跟随的情况下匹配成功”。

后瞻肯定断言

(?<=y)x, 匹配 x, 仅在前面是 y 的情况。

后瞻否定断言

(?<!y)x, 匹配 x, 仅在前面不是 y 的情况。

let str = "1 turkey costs 30€";
let reg = /\d+(?=(€|kr))/; // €|kr 两边有额外的括号

alert( str.match(reg) ); // 30, €
let str = "1 turkey costs $30";
let reg = /(?<=(\$|£))\d+/;

alert( str.match(reg) ); // 30, $

正则表达式(RegExp)和字符串(String)的方法

str.match(regexp)

如果 regexp 不带有 g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 index(匹配项的位置)、input(输入字符串,等于 str):

let str = "I love JavaScript";

let result = str.match(/Java(Script)/);

alert( result[0] );     // JavaScript(完全匹配)
alert( result[1] );     // Script(第一个分组)
alert( result.length ); // 2

// 其他信息:
alert( result.index );  // 7(匹配位置)
alert( result.input );  // I love JavaScript(源字符串)

如果 regexp 带有 g 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息。

let str = "I love JavaScript";

let result = str.match(/Java(Script)/g);

alert( result[0] ); // JavaScript
alert( result.length ); // 1

如果没有匹配项,则无论是否带有标记 g ,都将返回 null。

这是一个重要的细微差别。如果没有匹配项,我们得到的不是一个空数组,而是 null。忘记这一点很容易出错,例如:

let str = "I love JavaScript";

let result = str.match(/HTML/);

alert(result); // null
alert(result.length); // Error: Cannot read property 'length' of null

如果我们希望结果是一个数组,我们可以这样写:

let result = str.match(regexp) || [];

str.matchAll(regexp)

let str = '<h1>Hello, world!</h1>';
let regexp = /<(.*?)>/g;

let matchAll = str.matchAll(regexp);

alert(matchAll); // [object RegExp String Iterator],不是数组,而是一个可迭代对象

matchAll = Array.from(matchAll); // 现在返回的是数组

let firstMatch = matchAll[0];
alert( firstMatch[0] );  // <h1>
alert( firstMatch[1] );  // h1
alert( firstMatch.index );  // 0
alert( firstMatch.input );  // <h1>Hello, world!</h1>

str.split(regexp|substr, limit)

alert('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56']

str.search(regexp)

let str = "A drop of ink may make a million think";

alert( str.search( /ink/i ) ); // 10(第一个匹配位置)

str.replace(str|regexp, str|func)

// 用冒号替换连字符
alert('12-34-56'.replace("-", ":")) // 12:34-56
// 将连字符替换为冒号
alert( '12-34-56'.replace( /-/g, ":" ) )  // 12:34:56

第二个参数是一个替代字符串。我们可以在其中使用特殊字符:

符号替换字符串中的操作
$&插入整个匹配项
$`在匹配项之前插入字符串的一部分
$'在匹配项之后插入字符串的一部分
$n如果 n 是一个 1 到 2 位的数字,则插入第 n 个分组的内容,详见 捕获组
$<name>插入带有给定 name 的括号内的内容,详见 捕获组
$$插入字符 $
let str = "John Smith";

// 交换名字和姓氏
alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

对于需要“智能”替换的场景,第二个参数可以是一个函数。

每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。

该函数 func(match, p1, p2, ..., pn, offset, input, groups) 带参数调用:

match - 匹配项,

p1, p2, ..., pn - 分组的内容(如有),

offset - 匹配项的位置,

input - 源字符串,

groups - 所指定分组的对象。

如果正则表达式中没有括号,则只有 3 个参数:func(str, offset, input)。

let str = "html and css";

let result = str.replace(/html|css/gi, str => str.toUpperCase());

alert(result); // HTML and CSS

如果有许多组,用 rest 参数(…)可以很方便的访问:

let str = "John Smith";

let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`);

alert(result); // Smith, John

或者,如果我们使用的是命名组,则带有它们的 groups 对象始终是最后一个对象,因此我们可以这样获得它:

let str = "John Smith";

let result = str.replace(/(?<name>\w+) (?<surname>\w+)/, (...match) => {
  let groups = match.pop();

  return `${groups.surname}, ${groups.name}`;
});

alert(result); // Smith, John

regexp.exec(str)

regexp.exec(str) 方法返回字符串 str 中的 regexp 匹配项。与以前的方法不同,它是在正则表达式而不是字符串上调用的。

根据正则表达式是否带有标志 g,它的行为有所不同。

如果没有 g,那么 regexp.exec(str) 返回的第一个匹配与 str.match(regexp) 完全相同。这没什么新的变化。

但是,如果有标记 g,那么:

调用 regexp.exec(str) 会返回第一个匹配项,并将紧随其后的位置保存在属性 regexp.lastIndex 中。 下一次同样的调用会从位置 regexp.lastIndex 开始搜索,返回下一个匹配项,并将其后的位置保存在 regexp.lastIndex 中。 …以此类推。 如果没有匹配项,则 regexp.exec 返回 null,并将 regexp.lastIndex 重置为 0。 因此,重复调用会挨个返回所有的匹配项,属性 regexp.lastIndex 用来跟踪当前的搜索位置。

过去,在将 str.matchAll 方法添加到 JavaScript 之前,在循环中是通过调用 regexp.exec 来获取分组的所有匹配项:

let str = 'More about JavaScript at https://javascript.info';
let regexp = /javascript/ig;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Found JavaScript at position 11,然后
  // Found javascript at position 33
}
let str = 'Hello, world!';

let regexp = /\w+/g; // 不带标记 "g",lastIndex 属性会被忽略
regexp.lastIndex = 5; // 从第 5 个位置搜索(从逗号开始)

alert( regexp.exec(str) ); // world

regexp.test(str)

let str = "I love JavaScript";

// 这两个测试相同
alert( /love/i.test(str) ); // true
alert( str.search(/love/i) != -1 ); // true
let str = "Bla-bla-bla";

alert( /love/i.test(str) ); // false
alert( str.search(/love/i) != -1 ); // false
let regexp = /love/gi;

let str = "I love JavaScript";

// 从位置 10 开始:
regexp.lastIndex = 10;
alert( regexp.test(str) ); // false(无匹配)

参考:zh.javascript.info/regular-exp…