Skip to content

正则表达式

简介

正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为“元字符”),可以用来描述和匹配字符串的特定模式。

正则表达式是一种用于模式匹配和搜索文件的工具;

正则表达式提供了一种灵活且强大的方式来查找替换验证提取文本数据;

正则表达式可以应用于各种编程语言和文本处理工具中,如 JavaScript、Python、Java、Perl 等。

创建正则

可以使用以下两种方法构建一个正则表达式。

字面量创建👍

使用一个正则表达式字面量,其由包含在斜杠 // 之间的模式组成,如下所示:

js
let regex = /ab+c/;

脚本加载后,正则表达式字面量就会被编译。当正则表达式保持不变时,使用此方法可获得更好的性能。

这种方式不能在其中使用变量,虽然可以使用 eval 转换为 js 语法来实现将变量解析到正则中,如下所示,这种方式比较麻烦,所以有变量时建议使用下面的对象创建方式。

js
let site = "xiaorang.com";
let regex = "a";
console.log(eval(`/${regex}/`).test(site)); // true

对象创建

调用RegExp对象的构造函数,如下所示:

js
let regex = new RegExp("ab+c");

在脚本运行过程中,用构造函数创建的正则表达式会被编译。如果正则表达式将会改变,或者它将会从用户输入等来源中动态产生,就需要使用构造函数来创建正则表达式。

举个栗子(1):根据用户输入高亮显示内容,支持用户输入正则表达式。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="content">docs.xiaorang.fun</div>
  </body>
  <script>
    const search = prompt("请输入要搜索的内容,支持正则表达式");
    const regex = new RegExp(search, "g");
    let content = document.querySelector(".content");
    content.innerHTML = content.innerHTML.replace(
      regex,
      (value) => `<span style="color:red;">${value}</span>`
    );
  </script>
</html>

举个栗子(2):通过对象创建正则提取标签。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>xiaorang.fun</h1>
    <h1>docs.xiaorang.fun</h1>
  </body>
  <script>
    function element(tag) {
      const body = document.body.innerHTML;
      let regex = new RegExp("<(" + tag + ")>.+</\\1>", "g");
      return body.match(regex);
    }
    console.table(element("h1"));
  </script>
</html>

控制输出结果如下所示:
image-20240202174334582

使用正则表达式

正则表达式可以被用于 RegExpexectest 方法以及 Stringmatchreplacesearchsplit 方法。这些方法在 JavaScript 手册中有详细的解释。

方法描述
exec一个在字符串中执行查找匹配的 RegExp 方法,它返回一个数组(未匹配到则返回 null)。
test一个在字符串中测试是否匹配的 RegExp 方法,它返回 true 或 false。
match一个在字符串中执行查找匹配的 String 方法,它返回一个数组,在未匹配到时会返回 null。
matchAll一个在字符串中执行查找所有匹配的 String 方法,它返回一个迭代器(iterator)。
search一个在字符串中测试匹配的 String 方法,它返回匹配到的位置索引,或者在失败时返回 -1。
replace一个在字符串中执行查找匹配的 String 方法,并且使用替换字符串替换掉匹配到的子字符串。
split一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

当你想要知道在一个字符串中的一个匹配是否被找到,你可以使用 test 或 search 方法;想得到更多的信息(但是比较慢)则可以使用 exec 或 match 方法。如果你使用 exec 或 match 方法并且匹配成功了,那么这些方法将返回一个数组并且更新相关的正则表达式对象的属性和预定义的正则表达式对象(详见下)。如果匹配失败,那么 exec 方法返回 null(也就是 false)。

在接下来的例子中,脚本将使用 exec 方法在一个字符串中查找一个匹配。

js
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");

如果你不需要访问正则表达式的属性,这个脚本通过另一个方法来创建 myArray:

js
var myArray = /d(b+)d/g.exec("cdbbdbsbz");
// 和 "cdbbdbsbz".match(/d(b+)d/g); 相似。
// 但是 "cdbbdbsbz".match(/d(b+)d/g) 输出数组 [ "dbbd" ],
// 而 /d(b+)d/g.exec('cdbbdbsbz') 输出数组 [ "dbbd", "bb", index: 1, input: "cdbbdbsbz" ].

如果你想通过一个字符串构建正则表达式,那么这个脚本还有另一种方法:

js
var myRe = new RegExp("d(b+)d", "g");
var myArray = myRe.exec("cdbbdbsbz");

通过这些脚本,匹配成功后将返回一个数组并且更新正则表达式的属性,如下表所示。

对象属性或索引描述在例子中对应的值
myArray匹配到的字符串和所有被记住的子字符串。["dbbd", "bb"]
index在输入的字符串中匹配到的以 0 开始的索引值。1
input初始字符串。"cdbbdbsbz"
[0]最近一个匹配到的字符串。"dbbd"
myRelastIndex开始下一个匹配的起始索引值。(这个属性只有在使用 g 参数时可用在 通过参数进行高级搜索 一节有详细的描述.)5
source模式字面文本。在正则表达式创建时更新,不执行。"d(b+)d"

如这个例子中的第二种形式所示,你可以使用对象初始器创建一个正则表达式实例,但不分配给变量。如果你这样做,那么,每一次使用时都会创建一个新的正则表达式实例。因此,如果你不把正则表达式实例分配给一个变量,你以后将不能访问这个正则表达式实例的属性。例如,假如你有如下脚本:

js
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + myRe.lastIndex);

这个脚本输出如下:

markdown
The value of lastIndex is 5

然而,如果你有如下脚本:

js
var myArray = /d(b+)d/g.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + /d(b+)d/g.lastIndex);

它显示为:

markdown
The value of lastIndex is 0

当发生/d(b+)d/g 使用两个不同状态的正则表达式对象,lastIndex 属性会得到不同的值。如果你需要访问一个正则表达式的属性,则需要创建一个对象初始化生成器,你应该首先把它赋值给一个变量。


举个栗子:改变输入字符串的顺序

以下例子解释了正则表达式的构成和 string.split() 以及 string.replace()的用途。它会整理一个只有粗略格式的含有全名(名字首先出现)的输入字符串,这个字符串被空格、换行符和一个分号分隔。最终,它会颠倒名字顺序(姓氏首先出现)和 list 的类型。

js
// 下面这个姓名字符串包含了多个空格和制表符,
// 且在姓和名之间可能有多个空格和制表符。
var names = "Orange Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand ";

var output = ["---------- Original String\n", names + "\n"];

// 准备两个模式的正则表达式放进数组里。
// 分割该字符串放进数组里。

// 匹配模式:匹配一个分号及紧接其前后所有可能出现的连续的不可见符号。
var pattern = /\s*;\s*/;

// 把通过上述匹配模式分割的字符串放进一个叫做 nameList 的数组里面。
var nameList = names.split(pattern);

// 新建一个匹配模式:匹配一个或多个连续的不可见字符及其前后紧接着由
// 一个或多个连续的基本拉丁字母表中的字母、数字和下划线组成的字符串
// 用一对圆括号来捕获该模式中的一部分匹配结果。
// 捕获的结果稍后会用到。
pattern = /(\w+)\s+(\w+)/;

// 新建一个数组 bySurnameList 用来临时存放正在处理的名字。
var bySurnameList = [];

// 输出 nameList 的元素并且把 nameList 里的名字
// 用逗号接空格的模式把姓和名分割开来然后存放进数组 bySurnameList 中。
//
// 下面的这个替换方法把 nameList 里的元素用 $2, $1 的模式
//(第二个捕获的匹配结果紧接着一个逗号一个空格然后紧接着第一个捕获的匹配结果)替换了
// 变量 $1 和变量 $2 是上面所捕获的匹配结果。

output.push("---------- After Split by Regular Expression");

var i, len;
for (i = 0, len = nameList.length; i < len; i++) {
  output.push(nameList[i]);
  bySurnameList[i] = nameList[i].replace(pattern, "$2, $1");
}

// 输出新的数组
output.push("---------- Names Reversed");
for (i = 0, len = bySurnameList.length; i < len; i++) {
  output.push(bySurnameList[i]);
}

// 根据姓来排序,然后输出排序后的数组。
bySurnameList.sort();
output.push("---------- Sorted");
for (i = 0, len = bySurnameList.length; i < len; i++) {
  output.push(bySurnameList[i]);
}

output.push("---------- End");

console.log(output.join("\n"));

运行结果如下所示:

markdown
---------- Original String

Orange Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand 

---------- After Split by Regular Expression
Orange Trump
Fred Barney
Helen Rigby
Bill Abel
Chris Hand 
---------- Names Reversed
Trump, Orange
Barney, Fred
Rigby, Helen
Abel, Bill
Hand, Chris 
---------- Sorted
Abel, Bill
Barney, Fred
Hand, Chris 
Rigby, Helen
Trump, Orange
---------- End

元字符

任意字符(.

. 字符默认匹配除换行符之外的任意单个字符。如果单行标志s被设置为 true,那么它也会匹配换行符。

举个栗子:正则表达式: /.ar/g,表示匹配一个任意字符后紧跟着a和r的字符串。输入的字符串:“The car parked in the garage” => “The car parked in the garage”

转义字符(\

如果要把特殊字符作为常规字符来使用,则需要对其进行转义,只需在它前面加个反斜杠 \ 即可。

常见的需要转义的字符:[ ] ( ) { } . * + ? ^ $ \

假如有这样的场景,如果想通过正则表达式查找斜杠符号 /,虽然它并不是一个特殊字符,但是在字面量正则表达式中有特殊的含义。如果写成 /// 的话则会造成解析错误,所以要使用转义语法 /\// 来匹配。

js
const url = "https://docs.xiaorang.fun";
console.log(/https?:\/\//.test(url)); // true

使用 RegExp 对象创建正则时在转义上会有些许区别,下面为对象与字面量创建正则时的区别:

js
let price = 12.23;
// .字符在正则表达式中表示除换行符之外的任何字符,如果想表示普通的字符.,则需要使用\.才能转义成普通的字符.
console.log(/\d+\.\d+/.test(price));
// 在字符串中 \d 与 d 的含义是一样的,所以在使用RegExp时\d会被视为d,因此需要在前面再额外多加一个\ => \\d
console.log("\d" === "d");
let regex = new RegExp("\\d+\\.\\d+");
console.log(regex.test(price));

Tip

在使用 RegExp 对象创建正则时,如果自己不确定的话,可以先使用 console.log 把表达式打印出来,如果结果和字面量定义的一样则表示对了!

举个栗子(1):网址检测中转义字符的使用。

js
let url = "https://docs.xiaorang.fun";
console.log(/https?:\/\/\w+\.\w+\.\w+/.test(url)); // true

举个栗子(2):匹配所有以 .js 或者 .jsx 结尾的文件名。

js
const fileNames = ["abc.js", "cba.java", "nba.html", "mba.js", "aaa.jsx"];
const newNames = fileNames.filter(item => /\.jsx?$/.test(item));
console.log(newNames); // ['abc.js', 'mba.js', 'aaa.jsx']

量词(*+?{n,m}

Important

默认情况下,像 *+?{n,m} 这样的量词是贪婪的,这意味着它们试图尽可能地通过提供的文本扩展匹配,通俗点说,就是匹配尽可能多的字符串

如果在量词的后面加上 ? 字符的话,则会使被修饰的量词变成非贪婪的,意味着它一旦找到匹配的就会停止

举个栗子:

  • /(.*at)/ => “The fat cat sat on the mat.”
  • /(.*?at)/ => “The fat cat sat on the mat.”
字符描述例子
*匹配>=0个重复在*号之前的字符,等价于{0,}.字符与*字符搭配,即.*=>可以匹配所有的字符。例如,正则表达式 :/a*/g,表示匹配以a字符开头后面紧跟着0个或无数个字符的字符串。正则表达式:/[a-z]*/g 表示匹配所有以小写字母组成的字符串 => “The car parked in the garage.”
+匹配>=1个重复在+号之前的字符,等价于{1,}例如,正则表达式:/c.+t/g,表示匹配以首字母c开头以字母t结尾,中间至少夹着一个字符的字符串 => “The fat cat sat on the mat.”
?标记?号之前的字符为可选,即可出现1次或0次,等价于{0,1}例如,正则表达式:/[T]?he/g,表示匹配字符串The或者he => “The car is parked in the garage.”
{n,m}匹配num(其中 n <= num <= m)个大括号之前的字符或字符集,即至少n次,至多m次。例如,正则表达式:/[0-9]{2,3}/g,表示匹配2位数或3位数的数字 => “The number was 9.9997 but we rounded it off to 10.0.”
{n,}匹配num(其中 n <= num)个大括号之前的字符或字符集,即至少n次。例如,正则表达式:/[0-9]{2,}/g,表示匹配至少2位数的数字 => “The number was 9.9997 but we rounded it off to 10.0.”
{n}匹配n个大括号之前的字符或字符集,即固定的n次。例如,正则表达式:/[0-9]{3}/g,表示匹配3位数的数字 => “The number was 9.9997 but we rounded it off to 10.0.”

字符集/原子表([]

[] 称之为字符集,匹配方括号中的任意一个字符,包括转义字符 \

Note

在方括号中的字符不关心顺序。

在方括号中可以使用连字符来指定一个字符范围,比如 [0-9a-zA-Z] 可以匹配任意一个字母或者数字。

对于 .* 这样的特殊符号在一个字符集中没有特殊的意义,它们不必进行转义,不过转义的话也是起作用的。

举个栗子(1):正则表达式:/ar[.]/g,输入的字符串:“A garage is a good place to park a car.” => “A garage is a good place to park a car.

举个栗子(2):正则表达式:/[a-z.]+/g/[\w.]+/,输入的字符串:“test.i.ng” => “test.i.ng

否定字符集([^]

[^] 称之为否定字符集,它用于匹配任何一个没有包含在当前方括号中的字符

举个栗子(1):正则表达式:/[^c]ar/g,用于匹配任意一个除字符c之外后面跟着ar的字符串,输入的字符串:“The car parked in the garage.” => “The car parked in the garage.”

举个栗子(2):正则表达式:/[^abc]/g/[^a-c]/g,输入的字符串:“chop” => “chop

预定义字符集

正则表达式提供一些常用的预定义字符集。如下所示:

字符描述例子
\d匹配任意一个数字字符,等价于 [0-9]例如,正则表达式:/\d/g=> “B2 is the suite number.”
例如,正则表达式 /\d{2,3}/g 用于匹配 2~3 位的数字字符 => “The number was 9.9997 but we rounded it off to 10.0.”
\D\d 的意思刚好相反,匹配任意一个非数字字符,等价于 [^0-9][^\d]例如,正则表达式:/\D+/g => “one: 1, two: 2”
\w匹配任意一个字母、数字或下划线字符,等价于 [A-Za-z0-9_]例如,正则表达式:/\w+/g => “any word character
\W\w 的意思刚好相反,匹配任意一个除字母、数字和下划线之外的字符,等价于 [^A-Za-z0-9_][^\w]例如,正则表达式:/\W+/g => “not.a@word%character”
\s匹配任意一个空白字符,包括空格、制表符、换页符和换行符,等价于 [ \t\f\n\r\v]例如,正则表达式:/\s+/g,输入字符串:”any whitespace character.“ => “any_whitespace_character.”(此处的下划线仅仅是为了占位,不然背景色显示不出来)
\S匹配任意一个非空白字符,等价于 [^ \t\f\n\r\v][^\s]例如,正则表达式:/\S+/g => “any non-whitespace.

锚点(^$

字符描述例子
^用来检查匹配的字符串是否在所输入字符串的开头。如果多行标志m被设置为 true,即设置了 RegExp 对象的 Multiline 属性的话,那么也会匹配换行符后紧跟的位置。例如,/^(T|t)he/g 用于匹配以 The 或 the 开头的字符串。
/^(T|t)he/g => The car is parked in the garage.
此处,为什么没有匹配小写的 the 呢?那是因为它并没有出现在字符串开头的位置。
$用来检查匹配的字符串是否在所输入字符串的结尾。如果多行标志m被设置为 true,即设置了 RegExp 对象的 Multiline 属性的话,那么也会匹配换行符之前的位置。例如,/(at\.)$/g 用于匹配以 at. 结尾的字符串。
/(at\.)$/g => The fat cat. sat. on the mat.
此处,为什么没有匹配 cat. 和 sat. 中的 at. 呢?那是因为这两项并没有出现在字符串结尾的位置。

举个栗子:检测用户名长度为 3~6 位,且只能为字母。如果不同时使用 ^$ 的话则无法得到正确答案,输入的字符串必须是 3~6 位且全部都为字母!

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" name="username" />
    <script>
      document.body
        .querySelector(`[name="username"]`)
        .addEventListener("keyup", function () {
          console.log(/^[a-zA-Z]{3,6}$/.test(this.value));
        });
    </script>
  </body>
</html>

边界(\b\B

\b 表示单词边界

\B 表示非单词边界,应理解为(非单词)边界,而不是非(单词边界),它仍然匹配的是边界。

Important

将正则中的位置分为 字符的占位字符的间隙

字符的占位是显式的位置。以 I'm iron man 为例,肉眼可见的字母符号空格都是可以占位的字符,也就是可以用下标获取到字符的位置。

字符的间隙是隐式的位置,即显式位置之间的位置。比如说 I' 之间的位置,字符串开头和 I 之间的位置等等。

其中边界指的就是占位的字符左右的间隙位置

  • 单词边界匹配的是这样的间隙位置:左右两边占位的字符至少有一个不是\w。如下所示:
    • 正则表达式:/\b/g,输入的字符串:0az;用 “.” 替换的话 => .0az.,只有首位位置匹配;
    • 正则表达式:/\b/g,输入的字符串:a+a;用 “.” 替换的话 => .a.+.a.,因为 + 号不属于 \w,所以 + 号的左右间隙都可以被匹配;
    • 正则表达式:/\b/g,输入的字符串:a a;用 “.” 替换的话 => .a. .a.,与上一个案例的情形类似,因为空格也不属于 \w,所以空格的左右间隙也都可以被匹配;
  • 非单词边界与单词边界相反,匹配的是这样的间隙位置:左右两边占位的字符必须都是\w
    • 正则表达式:/\B/g,输入的字符串:0aZ_;用 “.” 替换的话 => 0.a.Z._
    • 正则表达式:/\B/g,输入的字符串:a+a;用 “.” 替换的话 => a+a,在这个例子找不到这样的间隙位置,原样输出;
    • 正则表达式:/\B/g,输入的字符串:a a;用 “.” 替换的话 => a a,同上一个案例一样,也找不到这样的间隙位置,原样输出;

Note

/\w\b\w/ 将不能匹配任何字符串,因为 \b 要求两边占位的字符至少有一个不是 \w,而现在两边占位的字符都是 \w,所以该正则表达式将不能匹配任何字符串!

举个栗子(1):正则表达式:/d\b/g,输入的字符串:“word boundaries are odd” => “word boundaries are odd

举个栗子(2):正则表达式:/r\B/g,输入的字符串:“regex is really cool” => “regex is really cool” => “regex is really cool”

选择分支(|

| 这个符号代表选择修饰符,也就说左右两侧(需要各自看成是一个整体)只要有一个匹配即可,类似于“或”的逻辑。

举个栗子:检测电话是否是上海或北京的坐机。

js
let tel = "010-12345678";
// 错误结果:匹配输出的结果为010,因为它将正则表达式中|右侧看成了一个整体,导致只有|左侧的010满足条件,所以输出010,但是这样的结果并不满足咱们的要求
console.log(tel.match(/010|020-\d{7,8}/)); 
// 正确结果:|需要放在原子组中进行使用,这样|右侧的内容只有)以内的020
console.log(tel.match(/(010|020)-\d{7,8}/))

标志/模式修饰符

全局搜索(g)

修饰符 g 常用于执行一个全局搜索匹配,即不仅仅返回第一个匹配的,而是返回全部匹配结果

例如,正则表达式:/.(at)/g,表示搜索任意字符(除了换行)+ at,并返回全部结果。

举个栗子:

/.(at)/ => “The fat cat sat on the mat.”

/.(at)/g => “The fat cat sat on the mat.”

忽略大小写(i)

修饰符 i 用于忽略大小写搜索。

例如,正则表达式:/The/gi,表示全局搜索大写的 The 和小写的 the

举个栗子:

/The/ => “The fat cat sat on the mat.”

/The/gi => “The fat cat sat on the mat.”

多行匹配(m)

修饰符 m 常用于执行一个多行匹配。像之前介绍的 ^$ 用于检查格式是否是在待检测字符串的开头或结尾。我们如果想要它在每行的开头和结尾生效的话,则需要用到多行修饰符 m

例如,正则表达式:/.at(.)?$/,表示除换行符之外的任意字符+小写字符at+末尾可选除换行符外的任意字符。根据 m 修饰符,现在表达式匹配每行的结尾。

举个栗子(1):

/.at(.)?/gm => “The fat
cat sat
on the mat.

举个栗子(2):分离出字符串中的课程和与之对应的价格,以 {name, price} 的对象格式进行输出。

js
let str = `
  #1 js,200元 #
  #2 php,300元 #
  #9 xiaorang.com # xiaorang
  #3 node.js,180元 #
`;
let lessons = str.match(/^\s*#\d+\s+.+\s+#$/gm).map(item => {
  item = item.replace(/\s*#\d+\s*/, "").replace(/\s+#/, "");
  let [name, price] = item.split(",");
  return {name, price};
});
console.log(JSON.stringify(lessons, null, 2));

运行结果如下所示:

json
[
  {
    "name": "js",
    "price": "200元"
  },
  {
    "name": "php",
    "price": "300元"
  },
  {
    "name": "node.js",
    "price": "180元"
  }
]

单行匹配(s)

单行匹配,此模式下.号能匹配任意字符,包括换行符。

举个栗子:

/xiaorang./gms => “google
xiaorang
taobao”

捕获组/原子组🔥

捕获组就是把正则表达式中子表达式匹配的内容保存到内存中以数字编号或显式命名的组里方便后面引用

捕获组有两种形式,一种是普通捕获组,另一种是命名捕获组,通常所说的捕获组指的是普通捕获组。语法如下:用小括号 (...) 的形式,达到分组捕获的目的。

  • 普通捕获组:(Expression)
  • 命名捕获组:(?<name>Expression)

捕获组编号规则

编号规则指的是以数字为捕获组进行编号的规则编号为_0_的捕获组,指的是正则表达式整体,这一规则在支持捕获组的语言中,基本上都是适用的。下面对其它编号规则逐一展开讨论。

捕获组的编号都是按照 “(” 出现的顺序,从左到右,从1开始进行编号的

Caution

网上有的人会说当普通捕获组与命名捕获组混合使用时,会先忽略命名捕获组,对普通捕获组进行编号,当普通捕获组完成编号后,再对命名捕获组进行编号。这个结论是错误的!!!

A named-capturing group is still numbered as described in Group number.

咱们一起来看下下面这三个例子,就会对捕获组的编号规则有个准确的认识。

普通捕获组

如果没有显式为捕获组命名,即没有使用命名捕获组,那么需要按数字顺序来访问所有捕获组。
例如,正则表达式/(\d{4})-(\d{2}-(\d\d))/g可以用来匹配格式为 yyyy-MM-dd 的日期格式。当用以上正则表达式匹配字符串:9999-12-31,匹配结果如下所示:regex101: 普通捕获组

Match 10-109999-12-31
Group 10-49999
Group 25-1012-31
Group 38-1031

命名捕获组

命名捕获组通过显式命名,可以通过组名方便的访问到指定的组,比普通捕获组多了一种访问手段,而不需要去一个个的数编号,同时避免了在正则表达式扩展过程中,捕获组的增加或减少对引用结果导致的不可控。
例如,正则表达式/(?<year>\d{4})-(?<date>\d{2}-(?<day>\d\d))/g可以用来匹配格式为 yyyy-MM-dd 的日期格式。当用以上正则表达式匹配字符串:9999-12-31,匹配结果如下所示:regex101: 命名捕获组

Match 10-109999-12-31
Group year0-49999
Group date5-1012-31
Group day8-1031

普通捕获组与命名捕获组混合使用

例如,正则表达式/(\d{4})-(?<date>\d{2}-(\d\d))/g可以用来匹配格式为 yyyy-MM-dd 的日期格式。当用以上正则表达式匹配字符串:9999-12-31,匹配结果如下所示:regex101: 普通捕获组与命名捕获组混合

Match 10-109999-12-31
Group 10-49999
Group date5-1012-31
Group 38-1031

非捕获组

与捕获组相反,非捕获组可以仅进行分组而不会捕获内容和分配编号。语法如下:用小括号 (?:...) 的形式。

还是拿上面捕获组中的正则表达式/(?:\d{4})-(?<date>\d{2}-(\d\d))/g来举例,该正则表达式可以用来匹配格式为 yyyy-MM-dd 的日期格式。当用以上正则表达式匹配字符串:9999-12-31,匹配结果如下所示:regex101: 非捕获组

Match 10-109999-12-31
Group date5-1012-31
Group 28-1031

反向引用🔥

定义:捕获组捕获到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用

反向引用的作用通常是用来查找或限定重复限定指定标识配对出现等等。对于普通捕获组和命名捕获组的引用,语法如下:

  • 普通捕获组反向引用:\k<number>,通常简写为 \number ,其中 number 是捕获组的编号,这种方式使用的比较多;
  • 命名捕获组反向引用:\k<name>或者\k'name',其中 name 为捕获组的组名。

为什么需要反向引用?

以一个案例来进行说明:HTML 程序员使用标题标签(<h1><h6>,以及配对的结束标签)来定义和排版 Web 页面里的标题文字。假设你现在需要把某个 Web 页面的所有标题文字全部查找出来,不管是几级标题。如下所示:

html
<body>
  <h1>Welcome to my Homepage</h1>
  Content is divided into two sections:<br/>
  <h2>SQL</h2>
  Information about SQL.
  <h2>RegEx</h2>
  Information about Regular Expressions.
</body>

正则表达式/<[hH][1-6]>.*?<\/[hH][1-6]>/g,匹配结果如下所示:regex101: 为什么使用反向引用的案例

Match 19-40<h1>Welcome to my Homepage</h1>
Match 288-100<h2>SQL</h2>
Match 3128-142<h2>RegEx</h2>

看起来没有问题,可是真的是这样的吗?未必哟,看看下面这个例子。

html
<body>
  <h1>Welcome to my Homepage</h1>
  Content is divided into two sections:<br/>
  <h2>SQL</h2>
  Information about SQL.
  <h2>RegEx</h3>
  Information about Regular Expressions.
</body>

还是相同的正则表达式/<[hH][1-6]>.*?<\/[hH][1-6]>/g,匹配结果如下所示:

Match 19-40<h1>Welcome to my Homepage</h1>
Match 288-100<h2>SQL</h2>
Match 3128-142<h2>RegEx</h3>

咱们发现有一处标题的标签是以<h2>开头,以</h3>结束,这显然是个无效的标题,但是也能和我们使用的模式匹配上,问题在于匹配的第二部分对匹配的第一部分一无所知。此时就需要反向引用闪亮登场了✨✨✨✨

反向引用

解决匹配 HTML 标题的问题前,咱们先看一个简单例子,如果不使用反向引用,根本无法解决。
假设你有一段文本,你想把这段文本里所有连续重复出现两次的单词打印出来,显然再搜索某个单词的第二次出现时,这个单词必须是已知的。反向引用允许正则表达式模式引用之前匹配的结果。如下所示:

markdown
This is a block of of text.
several words here are are
repeated, and and they
should not be.

正则表达式/\b(\w+)\b\s\1/g,其中 \1 表示的意思是引用编号为1的捕获组所捕获的内容。匹配结果如下所示:regex101: 反向引用案例之搜索一段文本中连续重复出现两次的单词

Match 116-21of of
Group 116-18of
Match 247-54are are
Group 147-50are
Match 365-72and and
Group 165-68and

了解反向引用的用法之后,再回到 HTML 标题的例子↩️,利用反向引用可以构造一个模式去匹配任何标题的开始标签和相应的结束标签。正则表达式/<([hH][1-6])>.*?<\/\1>/g,匹配结果如下所示:regex101: 反向引用的案例

Match 19-40<h1>Welcome to my Homepage</h1>
Group 110-12h1
Match 288-100<h2>SQL</h2>
Group 189-91h2

零宽度断言(前后预查)🔥

【断言】就是说正则可以断定在指定内容的前面或后面会出现满足指定规则的内容。

【零宽】 断言部分只确定位置不匹配任何内容,只是一种模式。内容宽度为零。

先行断言和后发断言(合称 lookaround)都属于非捕获组用于匹配模式,结果不包括在匹配列表中)。当我们需要一个模式的前面或后面有另一个特定的模式时,就可以使用它们。

例如,如果希望从下面的输入字符串 "$4.44" 和 "$10.88" 中获得所有以 $ 字符开头的数字,可以使用如下正则表达式 (?<=\$)[0-9\.]*。意思是:获取所有包含 . 并且前面是 $ 的数字。

零宽度断言如下:

符号描述
?=正先行断言-存在
?!负先行断言-排除
?<=正后发断言-存在
?<!负后发断言-排除

正先行断言

?=...正先行断言,表示第一部分子表达式所匹配的内容的后面挨着?=...断言中定义的表达式所匹配的内容。返回的结果中只包含第一部分子表达式所匹配的内容。 定义一个正先行断言要使用 (),在括号内部使用一个问号和等号:(?=...),正先行断言的内容写在括号中的等号后面。

例如,/(T|t)he(?=\sfat)/g中的第一部分表达式匹配 The 和 the,在后面的小括号中定义了正先行断言(?=\sfat),表达的意思为 The 或 the 的后面紧跟着 (空格)fat。

正则表达式:/(T|t)he(?=\sfat)/g => The fat cat sat on the mat.

正则表达式:/foo(?=bar)/g => foobar foobaz.

负先行断言

?!...负先行断言,表示第一部分子表达式所匹配的内容的后面不挨着?!...定义的表达式所匹配的内容。

例如,/(T|t)he(?!\sfat)/g中的第一部分表达式匹配 The 和 the,在后面的小括号中定义了负先行断言(?!\sfat),表达的意思为 The 或 the 的后面不跟着 (空格)fat。

正则表达式:/(T|t)he(?!\sfat)/g => The fat cat sat on the mat.

正则表达式:/foo(?=bar)/g => foobar foobaz.

正后发断言

?<=...正后发断言,表示第二部分子表达式所匹配的内容的前面挨着?<=...断言中定义的表达式所匹配的内容。

例如,/(?<=(T|t)he\s)(fat|mat)/g中的第二部分表达式匹配 fat 和 mat,在前面的小括号中定义了正后发断言(?<=(T|t)he\s),表达的意思为 fat 或 mat 的前面是 The(空格) 或 the(空格)。

正则表达式:/(?<=(T|t)he\s)(fat|mat)/g => The fat cat sat on the mat.

正则表达式:/(?<=foo)bar/g => foobar fuubar.

负后发断言

?<!...负后发断言,表示第二部分子表达式所匹配的内容的前面不挨着?<=...断言中定义的表达式所匹配的内容。

例如,/(?<!(T|t)he\s)(cat)/g中的第二部分表达式匹配 cat,在前面的小括号中定义了负后发断言(?<!(T|t)he\s),表达的意思为 cat 的前面不是 The(空格) 或 the(空格)。

正则表达式:/(?<!(T|t)he\s)(cat)/g => The cat sat on cat.

正则表达式:/(?<!not )foo/g => not foo but foo.

常用正则表达式🗂️

数字校验

描述正则表达式备注
数字^[0-9]*$
n位数字^\d{n}$
至少n位数字^\d{n,}$
m~n位数字^\d{m,n}$
整数^(-?[1-9]\d*)$非0开头,包括正整数和负整数
正整数^[1-9]\d*$
负整数^-[1-9]\d*$
非负整数^(([1-9]\d*)|0)$
非正整数^((-[1-9]\d*)|0)$
浮点数^-?(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0\.0+|0)$包括正浮点数和负浮点数
正浮点数^(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
负浮点数^-(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
非正浮点数^(?:-(?:[1-9]\d*\.\d+|0\.\d*[1-9]\d*)|0\.0+|0)$包含0
非负浮点数^(?:[1-9]\d*\.\d+|0\.\d+|0\.0+|0)$包含0
仅一位小数^-?(?:0|[1-9][0-9]*)\.[0-9]{1}$
最少一位小数^-?(?:0|[1-9][0-9]*)\.[0-9]{1,}$
最多两位小数^-?(?:0|[1-9][0-9]*)\.[0-9]{1,2}$
连续重复的数字^(\d)\1+$例如:111,222

字符校验

描述正则表达式备注
中文^[\u4E00-\u9FA5]+$
全角字符^[\uFF00-\uFFFF]+$
半角字符^[\u0000-\u00FF]+$
英文字符串(大写)^[A-Z]+$
英文字符串(小写)^[a-z]+$
英文字符串(不区分大小写)^[A-Za-z]+$
中文和数字^(?:[\u4E00-\u9FA5]{0,}|\d)+$
英文和数字^[A-Za-z0-9]+$
数字、英文字母或者下划线组成的字符串^\w+$
中文、英文、数字包括下划线^[\u4E00-\u9FA5\w]+$
不含字母的字符串^[^A-Za-z]*$
连续重复的字符串^(.)\1+$例如:aa,bb
长度为n的字符串^.{n}$
ASCII^[ -~]$

日期和时间校验

描述正则表达式备注
日期^\d{1,4}-(?:1[0-2]|0?[1-9])-(?:0?[1-9]|[1-2]\d|30|31)$弱校验,例如:2022-06-12
日期^(?:(?!0000)[0-9]{4}-(?:(?\:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$严格校验,考虑平闰年
时间^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$12小时制,例如:11:21:31
时间^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$24小时制,例如:23:21:31
日期+时间^(\d{1,4}-(?:1[0-2]|0?[1-9])-(?:0?[1-9]|[1-2]\d|30|31)) ((?:[01]\d\2[0-3]):[0-5]\d:[0-5]\d)$例如:2000-11-11 23:20:21

日常生活相关

描述正则表达式备注
中文名^[\u4E00-\u9FA5·]{2,16}$
英文名^[a-zA-Z][a-zA-Z\s]{0,20}[a-zA-Z]$
车牌号^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$不含新能源
车牌号^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:(?:[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])|(?:(?:\d{5}[A-HJK])|(?:[A-HJK][A-HJ-NP-Z0-9][0-9]{4})))$包含新能源
火车车次^[GCDZTSPKXLY1-9]\d{1,4}$例如:G1234
手机号^(?:(?:\+|00)86)?1[3-9]\d{9}$弱匹配
手机号^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$严格匹配
固话号码^(?:(?:\d{3}-)?\d{8}|^(?:\d{4}-)?\d{7,8})(?:-\d+)?$
手机IMEI码^\d{15,17}$一般是15位
邮编^(?:0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$例如:211100
统一社会信用代码^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$
身份证号码(1代)^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$15位数字
身份证号码(2代)^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[0-9Xx]$18位数字
QQ号^[1-9][0-9]{4,}$一般是5到10位
微信号^[a-zA-Z][-_a-zA-Z0-9]{5,19}$一般6~20位,字母开头,可包含字母、数字、-、_,不含特殊字符
股票代码^(s[hz]|S[HZ])(000[\d]{3}|002[\d]{3}|300[\d]{3}|600[\d]{3}|60[\d]{4})$A股,例如:600519
银行卡卡号^[1-9]{1}(?:\d{15}|\d{18})$一般为19位

互联网相关

描述正则表达式备注
域名^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$例如:r2coding.com
网址^(?:https?:\/\/)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$例如:https://docs.xiaorang.fun/
带端口号的网址(或IP)^(?:https?:\/\/)?[\w-]+(?:\.[\w-]+)+:\d{1,5}\/?$例如:http://127.0.0.1:9527/
URL^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)$例如:https://docs.xiaorang.fun/#/README?id=1
邮箱email^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$支持中文,例如:15019474951@163.com
用户名^[a-zA-Z0-9_-]{4,20}$4到20位
弱密码^[\w]{6,16}$6~16位,包含大小写字母和数字的组合
强密码^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@\.#$%^&*? ]).*$至少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
端口号^(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$例如:65535
IPv4地址^(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d\1\d\d|2[0-4]\d|25[0-5])$例如:192.168.31.1
IPv4地址+端口^(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$例如:192.168.31.1:8080
IPv6地址^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$例如:CDCD:910A:2222:5498:8475:1111:3900:2020
IPv6地址+端口^\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\](?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$例如:[CDCD:910A:2222:5498:8475:1111:3900:2020]:9800
子网掩码^(?:254|252|248|240|224|192|128)\.0\.0\.0|255\.(?:254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(?:254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(?:255|254|252|248|240|224|192|128|0)$例如:255.255.255.0
MAC地址^(?:(?:[a-f0-9A-F]{2}:){5}|(?:[a-f0-9A-F]{2}-){5})[a-f0-9A-F]{2}$
Version版本号^\d+(?:\.\d+){2}$例如:12.1.1
图片后缀\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif)+可按需增删扩展名集合
视频后缀\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4)+可按需增删扩展名集合
图片链接(?:https?:\/\/)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+.+\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif)可按需增删扩展名集合
视频链接(?:https?:\/\/)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+.+\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4)可按需增删扩展名集合
迅雷链接thunderx?:\/\/[a-zA-Z\d]+=
ed2k链接ed2k:\/\/\|file\|.+\|\/
磁力链接magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*

其他

描述正则表达式备注
MD5格式^(?:[a-f\d]{32}|[A-F\d]{32})$32位MD5,例如:7552E7071B118CBFFEC8C930455B4297
BASE64格式^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)\s*$例如:data:image/jpeg;base64,xxxx==
UUID^[a-f\d]{4}(?:[a-f\d]{4}-){4}[a-f\d]{12}$例如:94f9d45a-71b0-4b3c-b69d-20c4bc9c8fdd
16进制^[A-Fa-f0-9]+$例如:FFFFFF
16进制颜色^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$例如:#FFFFFF
SQL语句^(?:select|drop|delete|create|update|insert).*$
Java包名^(?:[a-zA-Z_]\w*)+(?:[.][a-zA-Z_]\w*)+$例如:fun.xiaorang.controller
文件扩展名\.(?:doc|pdf|txt)可按需增删扩展名集合
Windows文件路径^[a-zA-Z]:(?:\\[\w\u4E00-\u9FA5\s]+)+[.\w\u4E00-\u9FA5\s]+$例如:C:\Users\Administrator\Desktop\a.txt
Windows文件夹路径^[a-zA-Z]:(?:\\[\w\u4E00-\u9FA5\s]+)+$例如:C:\Users\Administrator\Desktop
Linux文件路径^\/(?:[^\/]+\/)*[^\/]+$例如:/root/library/a.txt
Linux文件夹路径^\/(?:[^\/]+\/)*$例如:/root/library/

在线工具🔨

参考资料🎁