接触正则表达式是在一年半前,当时做项目要用字符串替换功能,只知道用 replace 方法,不过因为需求也不算复杂,基本能用。最近在搜资料的时候才想起自己应该抽时间系统整理一下正则了。之前学 java 的时候打印过正则表达式相关的语法规则,也做过相关的笔记,这次主要在之前笔记的基础上整理一下日常开发中用到的正则表达式。

巩固基础

最开始了解正则表达式就要弄明白为什么要用正则表达式,我觉得 deerchao 同学的 正则表达式30分钟入门教程 这篇文章里对这一点讲的很好。即,正则表达式是用于描述查找符合某些复杂规则的字符串需要的工具,其中操作对象是”字符串”,目的是符合某些复杂规则,通俗些讲就是 用来筛选查找你想要的字符串。

基础字符

以下的这些字符都是正则表达式中常用的基础,像是英语中的字母(简单的单词)一样,正则表达式是由这些基础字符组装配合起来一起实现筛选查找功能的。

表1. 正则表达式的基础字符

编号 字符 说明 类别 备注
1 . 匹配除换行符以外的任意字符 元字符  
2 \w 匹配字母或数字或下划线或汉字 元字符  
3 \d 匹配数字 元字符  
4 \s 匹配任意的空白符 元字符  
5 \b 匹配单词的开始或结束 元字符  
6 ^ 匹配字符串的开始 元字符  
7 $ 匹配字符串的结束 元字符  
8 \ 对元字符进行转义 转义字符  
9 * 重复零次或很多次 重复字符  
10 + 重复一次或很多次 重复字符  
11 ? 重复零次或一次 重复字符  
12 {n} 重复 n 次 重复字符  
13 {n,} 重复 n 次或很多次 重复字符  
14 {n,m} 重复 n~m 次 重复字符  
15 [] 匹配其中一个 自定义字符  

针对以上基础字符,现给出部分示例,便于理解:

google.com

RegExp_Num

以上为正则表达式的基础字符的使用,其实大多都是元字符的简单逻辑组合,挺容易理解。

正则表达式的检验测试

有了以上的基础字符,其实就可以写简单的正则表达式了,为了能够更清晰地理解正则表达式,很多正则的检验工具就诞生了,这里推荐两款工具如下:

regexp

Regester

同类型的网站还有:

正则表达式的语法详解

分支条件

正则表达式中的分支条件是指,用 “|” 把不同的规则分隔开,只要满足其中任意一种规则都应当匹配。例如,

分组(子表达式)

正则表达式中可以使用小括号”()”来指定子表达式或进行其他操作,具体实现过程如下:

上图中 “(2[0-4]\d|25[0-5]|[01]?\d\d?)” 出现了4次,只是为了精准匹配 0~255之间的数,然后结合 IP 地址的特点进行分组拼接。以后写正则表达式时,应该和检索问题一样,先分析问题,分析待匹配的字符串的特点,再按照需求写表达式

反义

正则表达式中的反义是用于查找不属于某个能简单定义的字符类的字符。用于反义的字符主要有:

编号 字符 说明 类别 备注
1 \W 匹配任意不是字母,数字,下划线,汉字的字符 反义字符  
2 \S 匹配任意不是空白符的字符 反义字符  
3 \D 匹配任意非数字的字符 反义字符  
4 [^x] 匹配除了x以外的任意字符 反义字符  
5 ^abcde 匹配除了abcde以外的任意字符 反义字符  

例如,以下字符都使用到了反义:

后向引用

正则表达式会给所有的子表达式(用小括号括起来的表达式)分组,从左向右,以分组左括号为标志,每个分组会自动拥有一个组号,第一个分组组号为1,后面持续增加。分组0对应整个正则表达式。正则表达式中的后向引用主要是指,用于重复搜索前面某个分组匹配的文本。例如,用 “\1” 代表分组1匹配的文本

例如,匹配重复的单词可以用到后向引用,即:

在写子表达式的时候,也可以自己指定组名,可以用 “(?\w+)" 或者 "(?'Name'\w+)",即可把 "\w+" 的组名指定为 Name,后向引用时可以使用"\k"引用,即上述例子可改写为: "\b(?\w+)\b\s+\k\b"

分组语法如下表所示:

编号 字符 说明 类别 备注
1 (exp) 匹配exp,并捕获文本到自动命名的组里 捕获字符  
2 (?exp) 匹配exp,捕获文本到名为name的组,也可写作(?’name’exp) 捕获字符  
3 (?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号 捕获字符 ES支持
4 (?=exp) 匹配exp前的位置 零宽断言  
5 (?<=exp) 匹配exp后的位置 零宽断言 ES不支持
6 (?!exp) 匹配后面不是跟exp的位置 零宽断言  
7 (?<!exp) 匹配前面不是exp的位置 零宽断言 ES不支持
8 (?#comment) 不对正则表达式产生影响,仅供注释时用 注释 ES不支持

上表中的注释可以列举 IP 地址单项的例子:

零宽断言

正则表达式中的零宽断言是 用于查找某些内容之前或者之后东西,但并不包括这些内容,用于指定一个位置,像”\b”,”^”,”$”表示位置,这些位置应满足一定条件(断言),例如上表中的几个零宽断言可以详细解释为:

负向零宽断言

正则表达式中的负向零宽断言是用于确保某个字符没有出现,但并不想去匹配它,此时可以使用负向零宽断言。如果用反义字符完成 匹配后面不是字母u的字母q的单词,可能会这样写 “\b\w*q[^u]\w*\b”,但是这样可能会有 bug ,比如它也会匹配空格([^u]),于是就有了可能匹配 “Iraq FAQ”这样的字符串。如果使用 负向零宽断言,则有:

将后向引用和零宽断言结合起来的实例:
“(?<=<(\w+)>).*(?=<\/\1>)”
可用于匹配HTML 简单标签标签中间的内容(不包含标签的前缀和后缀),只能识别无自定义class 或者 id 的标签内容。

贪婪与懒惰

正则表达式中,包含能接受重复的限定符时,贪婪匹配策略会匹配尽可能的的字符,例如,表达式”a.*b”,它将会匹配最长的以 a 开始,以 b 结束的字符串,若搜索 “aabab” 字符串,它会匹配整个字符串。

与贪婪匹配对应的是 懒惰匹配,即懒惰匹配策略会匹配经可能少的字符,只需要将限定符后面加一个 “?” 即可将贪婪匹配模式转换成懒惰匹配模式,”.*?” 即可表达匹配任意数量的重复,但在能使整个匹配成功的前提下使用最少的重复。如下图所示:

贪婪策略与懒惰策略

懒惰策略限定符如下表所示:

编号 字符 说明 类别 备注
1 *? 重复任意次,但尽可能少重复 懒惰限定符  
2 +? 重复1次或更多次,但尽可能少重复 懒惰限定符  
3 ?? 重复0次或1次,但尽可能少重复 懒惰限定符  
4 {n,m}? 重复n~m次,但尽可能少重复 懒惰限定符  
5 {n,}? 重复n次以上,但尽可能少重复 懒惰限定符  

平衡组与递归匹配

将分组存入堆栈的操作,和取出的操作,JavaScript里面好像不支持,就先不写了。后面有需要再补充。

JavaScript中的正则表达式

JavaScript 中有 RegExp 类表示正则表达式, JavaScript 中的正则表达式语法是 Perl5 的正则表达式语法的大型子集。

JavaScript 中的正则表达式的表示法

正则表达式的直接量定义,可以使用一对斜杠(/)来包裹,斜杠之间的字符即是要使用的表达式,例如:

var pattern = /s$/;

也可使用 RegExp() 构造函数创建,即:

var pattern = new RegExp("s$");

两种形式都是创建一个新的 RegExp 对象,用来匹配以 “s” 结尾的字符串。

一个正则表达式直接量在执行到它时转换为一个RegExp对象,ECMAScript 5 中,同一段代码所表示的正则表达式直接量的每次运算都返回新对象。ECMAScript 3,却返回的是同一对象,这个需要注意。

JavaScript 正则表达式基础使用

JavaScript 正则表达式中的直接量字符如下表所示:

编号 字符 说明 类别 备注
1 字母和数字字符 代表其自身含义 直接量字符  
2 \o NUL字符(\u0000) 直接量字符  
3 \t 制表符(\u0009) 直接量字符  
4 \n 换行符(\u000A) 直接量字符  
5 \v 垂直制表符(\u000B) 直接量字符  
6 \f 换页符(\u000C) 直接量字符  
7 \r 回车符(\u000D) 直接量字符  
8 \xnn 由十六进制数nn指定的拉丁字符 直接量字符  

在元字符中有很多符号如果表达它自己都需要用反斜线转义,如果不记得哪些标点符号需要反斜线转义,可以在每个标点符号前都加上反斜线。JavaScript 正则表达式的字符类与前面提到的正则表达的基础字符相差不大。

“[…]”,”[^…]”,”.”,”\w”,”\W”,”\s”,”\S”,”\d”,”\D”,”[\b]”。 其中”[\b]”表示匹配一个退格量。

JavaScript 正则表达式的重复字符主要有: “{n,m}”,”{n,}”,”{n}”,”?”,”+”,”*” 来匹配重复的字符,和前面基本一样。

JavaScript 里也有非贪婪的重复(懒惰模式),使用的字符和前面差不多 “??”,”+?”,”*?”来实现尽可能少的重复匹配

JavaScript 正则表达式里也包含 选择,分组和引用,使用符号 “|“,”(…)”,”(?:…)”,”\num” num是分组编号。

JavaScript 正则表达式中指定匹配位置的元素为正则表达式的锚。,常使用的符号为 “^”,”$”,”\b”,”\B”,”(?=p)”,”(?!p)”,其中后两个也是零宽正向先行断言,和零宽负向先行断言,这些都是匹配符合条件的位置。

JavaScript 正则表达式中有一个特殊的存在:正则表达式的修饰符。放在两个”/”符号之外的符号,不是在两个斜线之间,一般放在第二个斜线之后,一般修饰符有:

可以使用 “/a+/gi” 来联合使用不区分大小写和全局匹配。

JavaScript 正则表达式用于模式匹配的String方法

JavaScript 中的 String 支持 4 种使用正则表达式的方法:

search()的参数是一个正则表达式,返回第一个与之匹配的子串的起始位置,找不到匹配的子串就返回 “-1”,可以有以下使用方式:

"JavaScript".search(/script/i);

若 search() 方法传入的不是正则,则通过构造函数 RegExp 先转换成正则。search() 方法不支持全局搜索

replace() 方法用以执行检索和替换操作。第一参数是正则表达式,第二个参数是要进行替换的字符串。若正则中带有修饰符”g”,则源字符串中所有与模式匹配的子串都将替换成第二个参数指定的字符串,不带修饰符”g”,则只匹配可以检索到的第一个。若 replace() 方法第一个参数不是正则,则当作字符串直接搜索。replace() 方法可用作如下操作:

text.replace(/javascript/gi, "JavaScript");
// 可以将所有不区分大小写的 javascript 都替换成大小写正确的 JavaScript

replace() 方法还可结合后向引用进行较为复杂的操作,使用$num(num表示编号)的方式取得匹配时的分组内容,例如将英文引号替换成中文半角引号,可以写作:

// 起始于引号,结束于引号,中间不能有引号
var quote = /"([^"]*)"/g;
text.replace(quote,  '”$1“');
// 用中文半角引号替换英文引号,保持引号之间的内容(存储在 $1 中)没有被修改

replace() 方法的第二个参数可以是函数,可动态地计算替换字符串。

match() 方法只有一个参数,即输入的正则表达式,或者通过构造函数RegExp转换成的正则表达式,返回的是一个由匹配结果组成的数组,若正则表达式包含修饰符”g”,则数组包含字符串中所有的匹配结果。例如:

"1 plus a equals 3".match(/\d+/g); // 返回 ["1","2","3"]

若 match() 方法的正则表达式没有包含修饰符 “g”,则 数组 a[0]存放完整的匹配,余下元素存放的是子表达式匹配的结果,和上述的$n 对应的是 a[n]。
例如,解析 URL 的正则操作可以有:

var url = /(\w+):\/\/([\w.]+)\/(\S*)/;
var text = "Visit his blog at http://www.example.com/~david";
var result = text.match(url);
if (result != null ){
    var fullurl = result[0]; // 匹配 "http://www.example.com/~david"
    var protocol = result[1]; // 匹配 "http”
    var host = result[2]; // 匹配 "www.example.com"
    var path = result[3]; // 匹配 "~david"
}

split() 方法可以调用它的字符串拆分为一个子串组成的数组,使用的分隔符是传入的参数,传入的参数也可以是一个正则表达式。例如以下的两条语句:

"123,456,789".split(","); // 返回 ["123", "456", "789"]
"1,  2, 3, 4,  5".split(/\s*,\s*/); // 返回 ["1","2","3","4","5"]
// 第二条用于拆分逗号允许两边留有任意多的空格

JavaScript 正则表达式RegExp对象及使用方法

RegExp() 构造函数可输入两个参数,第一个参数是字符串表示的正则表达式,第二个参数是三种修饰符或者其组合。在第一个参数中要经常用 “"字符作为转义字符的前缀,即必须将 正则中的”" 替换成 “\“,例如可构造如下正则:

var zipcode = new RegExp("\\d{5}","g");
// 注意这里使用的是 "\\" 而不是 "\"

每个 RegExp 对象都包含 5 个属性

RegExp的实例方法

RegExp最主要的执行模式匹配的方法是exec() ,其参数是一个字符串,即对一个指定的字符串执行一个正则表达式。
在字符串中执行匹配检索,若没有检索到,返回 null ,若找到匹配项,返回一个数组,这里与 match() 方法返回的类似。
在非全局模式下,返回的是子串的检索结果,index包含发生匹配的字符位置,属性input引用的是正在检索的字符串。不过与 match() 方法不同,无论是否全局,exec() 方法返回的数组是一样的。

var testText = "mom and dad and baby";
var pattern = /mom( and dad( and baby)?)?/gi;
var matches = pattern.exec(testText);
console.log(matches.index); // 0
console.log(matches.input); // "mom and dad and baby"
console.log(matches[0]) // "mom and dad and baby"
console.log(matches[1]) // " and dad and baby"
console.log(matches[2]) // " and baby"
console.log(matches.lastIndex); // 20

对于 exec() 方法而言,若设置了全局标志(g),调用该方法每次只会返回一个匹配项。
不设置全局标志情况下,对同一字符串多次调用 exec() 方法将始终返回第一匹配项的信息;若设置全局标志情况下,每次调用 exec() 则都会在字符串中继续查找新匹配项。

RegExp 另一个方法是 test(),用来对某个字符串检测,如果包含匹配结果,返回 true。
即当exec() 返回结果不是 null 时,test() 返回均为true。

var testText = "000-00-0000";
var pattern = /\d{3}-\d{2}-\d{4}/;
if (pattern.test(testText)){
    console.log("The pattern was matched.");
}

JavaScript 正则表达式模式的局限性

ECMAScript 中的正则表达式功能虽然很完备,但仍然缺少一些 Perl 所支持的高级正则表达式特性。JavaScript 正则表达式不支持的特性有:

应用实例

国内手机号码正则匹配

Github 上有一个国内手机号码的正则表达式项目 ChinaMobilePhoneNumberRegex,有1000+的 Star,主要功能就是用于匹配中国联通移动电信和网络号段的手机号,主要正则表达式如下:

”^(?:+?86)?1(?:3\d{3}|5[^4\D]\d{2}|8\d{3}|7[^29\D](?(?<=4)(?:0\d|1[0-2]|9\d)|\d{2})|9[189]\d{2}|6[567]\d{2}|4(?:[14]0\d{3}|[68]\d{4}|[579]\d{2}))\d{6}$”

但是上述表达式却在几个在线测试平台都报错了,在 “(?(?<=4)”处对第一个 “?”报错,删除了可以匹配。

字符串全局替换

日常使用中,经常会遇到需要替换指定字符串的操作,这里就会用到正则表达式。
主要使用的是字符串的 replace() 方法,通常可以传入两个参数,第一个是 RegExp 对象或者字符串,第二个是要替换的字符串。如果要全局替换,第一个参数就要是RegExp 对象,例如:

var text = "cat, bat, sat, fat";
var result = text.replace(/at/g, "ond");
console.log(result); // "cond, bond, sond, fond"

第二个参数也可以引用 第一个参数中的捕获组参数,例如:

var text = "cat, bat, sat, fat";
var result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // "word (cat), word (bat), word (sat), word (fat)"

所以日常使用字符串处理时,可以考虑利用好正则表达式。

参考资源

关于正则表达式的学习资料很多,除了单纯讲解正则表达式的通用操作外,本次整理主要参考了 JavaScript 中正则表达式的使用,详细参考资料如下: