如何正确书写正则表达式

如何书写正则表达式

正则常用到的字符很多,列举一些我经常用到的常用字符

常用匹配字符

含义

[0-9]

匹配单个数字0-9

[a-z]

匹配单个小写字母

[A-Z]

匹配单个大写字母

\s

匹配所有空白字符,如空格、换行等

\n

匹配所有换行符

\b

匹配边界如字符之间的空格

特殊字符

含义

用法

^

1. 匹配输入字符串的开始位置 2. 用在[]中时表示 非

1. /^http/ 匹配以http开头的字符串 2. /[^a-zA-Z]/ 匹配非字母

$

匹配输入字符串的结尾位置

/.com$/ 匹配以.com结尾的字符串

|

二选一,表示 或

/a|b/ 匹配a或者b

.

小数点匹配换行符\n之外的任何单个字符

/./ 匹配换行符之外的其他字符

[]

中括号匹配一个字符

/[aeiou]/ 匹配字母 aeiou 中的一个

()

小括号表示一个子表达式分组

匹配的子表达式可以用于以后使用

{}

大括号表示限定一个表达式多少次

{n} 匹配n次; {n,} 匹配最少n次; {n, m} 匹配n-m次

+

匹配前面的子表达式一次或多次

/[0-9]+/ 匹配一个数字或多个数字

*

匹配前面的子表达式零次或多次

/[0-9]*/ 匹配0次数字或多个数字

?

1. 匹配前面的子表达式零次或一次 2. 指明一个非贪婪限定符

1. /[0-9]?/ 2. /<.*?>/ 匹配一个标签如<p>

 

匹配特殊字符本身时需要转义,共有以下几个:

* . ? + $ ^ [ ] ( ) { } | \ /

如何正确书写正则表达式

// 字面量

const reg = /[0-9a-z]/g

// 构造函数

const reg = new RegExp('[0-9a-z]', 'g')

其中字面量中不能包含变量,构造函数中可以使用变量:

const name = '战神白起'

const reg = new RegExp(`我的名字叫${name}`)

经常会用 reg.test(str) 方法来判断字符串中是否匹配到了正则表达式:

const reg = /[0-9]/

const str = '文本中有没有数字1234等'

if (reg.test(str)) {

  ...

}

也经常用str.replace(reg, '') 方法来替换字符串中的内容:

const reg = /[0-9]/g

const str = '文本中的数字1234全部替换成x'

const newStr = str.replace(reg, 'x')

也会用到 str.match(reg) 方法来获取匹配到的内容(也可以用reg.exec(str)):

const reg = /[0-9]+[.][0-9]+[.][0-9]+/g

const str = '这里有个表名字叫做 11.11.11'

str.match(reg) // ['11.11.11']  

  • match 中的正则表达式如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
  • 如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。 在这种情况下,返回的项目将具有如下所述的其他属性。

正则表达式定义:

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。大家可以利用regexper.com这个工具很好地可视化自己写的正则表达式:

 /^(0\d{2,3}-\d{7,8})$|^(1[3-9]\d{9})$/
  message: "座机(028-2580344)或手机号格式不对",

这是我在项目中校验电话格式的一个正则,咱用regexper.com来看看:

如何正确书写正则表达式

这样看起来很一目了然,再也不需要用的时候在网上搜半天还不满足我们的需求

我们学习一个系统化的知识,一定要从其基础构成来了解。正则表达式的基本组成元素可以分为:字符和元字符。字符很好理解,就是基础的计算机字符编码,通常正则表达式里面使用的就是数字、英文字母。而元字符,也被称为特殊字符,是一些用来表示特殊语义的字符。如^表示非,|表示或等。利用这些元字符,才能构造出强大的表达式模式(pattern)。

常用的字符、元字符我已经在文章的开始罗列了就不过多赘述了。

特定次数

在某些情况下,我们需要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。如'a'我想匹配3次,那么我就使用/a{3}/这个正则,或者说'a'我想匹配至少两次就是用/a{2,}/这个正则。

以下是完整的语法:

- {x}: x次
- {min, max}:介于min次到max次之间
- {min, }: 至少min次
- {0, max}:至多max次

由于这些元字符比较抽象,且容易混淆,所以我用了联想记忆的方式编了口诀能保证在用到的时候就能回忆起来。

匹配规则

元字符

联想方式

0次或1次

?

,此事

0次或无数次

*

宇宙洪荒,辰宿列张:宇宙伊始,从无到有,最后星宿布满星空

1次或无数次

+

一加, +1

特定次数

{x}, {min, max}

可以想象成一个数轴,从一个点,到一个射线再到线段。min和max分别表示了左闭右闭区间的左界和右界

 

位置边界

在长文本字符串查找过程中,我们常常需要限制查询的位置

边界和标志

正则表达式

记忆方式

单词边界

\b

boundary

非单词边界

\B

not boundary

字符串开头

^

头尖尖那么大个

字符串结尾

$

终结者,美国科幻电影,美元符$

多行模式

m标志

multiple of lines

忽略大小写

i标志

ignore case, case-insensitive

全局模式

g标志

global

 

子表达式

通过嵌套递归和自身引用可以让正则发挥更强大的功能,从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

(1)分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

(2)回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?

Hello what what is the first thing, and I am am scq000.

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以js代码作演示:

var str = 'abc abc 123';str.replace(/(ab)c/g,'$1g');// 得到结果 'abg abg 123'

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

var str = 'scq000'.str.replace(/(scq00)(?:0)/, '$1,$2')// 返回scq00,$2  // 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2 

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

I.前向查找

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如happy happily这两个单词,我想获得以happ开头的副词,那么就可以使用happ(?=ily)来匹配。如果我想过滤所有以happ开头的副词,那么也可以采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

  1. (?=): 即查找符合限定条件 (?=) 的前面的匹配项(输出内容不包括 (?=) 中的匹配项)

                const str = 'a.png b.jpg c.gif d.svg'    // 查找所有 边界开头的、 .svg 前面的 小写字母。

                const reg = /\b[a-z](?=.svg)/g

                str.match(reg) // ['d']

       2.(?!): 即查找 不符合 限定条件 (?!) 的前面的匹配项(输出内容不包括 (?!) 中的匹配项)

                const str = 'a.png b.jpg c.gif d.svg'      // 查找所有边界开头的、 非.svg 前面的、 `.[a-z]{3}` 前面的 小写字母。

                const reg = /\b[a-z](?!.svg)(?=\.[a-z]{3})/g

                str.match(reg) // ['a', 'b', 'c']

II.后向查找

后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: applepeople都包含ple这个后缀,那么如果我只想找到appleple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。

/(?<=app)ple/

其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向查找。另外一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找appleple也可以这么写成/(?<!peo)ple

  1. 查找符合限定条件 (?<=) 的后面的匹配项(输出内容不包括 (?<=) 中的匹配项)

                   const str = '1. 1111; 2. 2222; 3. 3333; 4. 4444。'    //  查找所有 序号 后面的项。

                   const reg = /(?<=\b[0-9]+\.\s).+?[;。]/g

                   str.match(reg) // ["1111;", "2222;", "3333;", "4444。"]

    2.查找 不符合 限定条件 (?<!) 的后面的匹配项(输出内容不包括 (?<!) 中的匹配项)

                    const str = 'a.png b.jpg c.gif d.svg'    // 查找前缀不为 a b c 的后面的项

                   const reg = /\b(?<![abc]\.)[a-z]{3}/g

                   str.match(reg) // ['svg']

前向查找和后向查找齐用的例子:

假设要获取 <img crossorigin src="https://abcdefg.com" data-img-url="https://test.com"> 中的 data-img-url 属性中的链接。可以确定的是链接左边一定是 data-img-url=" ,右边一定是紧贴着 " (非贪婪)。

                   const str = '<img crossorigin src="https://abcdefg.com" data-img-url="https://test.com">'

                   const dataImgUrl = 'data-img-url'

                   const reg = new RegExp(`(?<=${dataImgUrl}=").+?(?=")`, 'g')

                   str.match(reg) // ['https://test.com']

回溯查找

正则

记忆方式

引用

\0,\1,\2 和 $0, $1, $2

转义+数字

非捕获组

(?:)

引用表达式(()), 本身不被消费(?),引用(:)

前向查找

(?=)

引用子表达式(()),本身不被消费(?), 正向的查找(=)

前向负查找

(?!)

引用子表达式(()),本身不被消费(?), 负向的查找(!)

后向查找

(?<=)

引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=)

后向负查找

(?<!)

引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!)

 

(3)逻辑处理

在正则里面,默认的正则规则都是的关系所以这里不讨论。而关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^这个元字符。在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

或关系,通常给子表达式进行归类使用。比如,我同时匹配a,b两种情况就可以使用(a|b)这样的子表达式。

逻辑关系

正则元字符

[^regex]和!

|

 

括号的作用

                                          如何正确书写正则表达式

1. 分组和分支结构

这二者是括号最直觉的作用,也是最原始的功能。

1.1 分组

我们知道/a+/匹配连续出现的“a”,而要匹配连续出现的“ab”时,需要使用/(ab)+/。

其中括号是提供分组功能,使量词“+”作用于“ab”这个整体,测试如下:

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

1.2 分支结构

而在多选分支结构(p1|p2)中,此处括号的作用也是不言而喻的,提供了子表达式的所有可能。

比如,要匹配如下的字符串:

I love JavaScript

I love Regular Expression

可以使用正则:

var regex=/^I love (JavaScript|Regular Expression)$/;
console.log(regex.test("I love JavaScript"));// true
console.log(regex.test("I love Regular Expression"));// true

如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。

2. 引用分组

这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。

而要使用它带来的好处,必须配合使用实现环境的API。

以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:

var regex=/\d{4}-\d{2}-\d{2}/;

然后再修改成括号版的:

var regex=/(\d{4})-(\d{2})-(\d{2})/;

2.1 提取数据

比如提取出年、月、日,可以这么做:

var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
console.log(string.match(regex));// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符g,match返回的数组格式是不一样的)。

另外也可以使用正则对象的exec方法:

var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
console.log(regex.exec(string));// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

同时,也可以使用构造函数的全局属性$1至$9来获取:

var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
regex.test(string);// 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1);// "2017"
console.log(RegExp.$2);// "06"
console.log(RegExp.$3);// "12"

2.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"

其中replace中的,第二个参数里用$1、$2、$3指代相应的分组。等价于如下的形式:

var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
var result=string.replace(regex,function(){       
return RegExp.$2+"/"+RegExp.$3+"/"+RegExp.$1;});
console.log(result);// "06/12/2017"

也等价于:

var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
var result=string.replace(regex,function(match,year,month,day){
return month+"/"+day+"/"+year;
});
console.log(result);// "06/12/2017"

3. 反向引用

除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

还是以日期为例。

比如要写一个正则支持匹配如下三种格式:

2016-06-12

2016/06/12

2016.06.12

最先可能想到的正则是:

var regex=/\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1="2017-06-12";
var string2="2017/06/12";
var string3="2017.06.12";
var string4="2016-06/12";
console.log(regex.test(string1));// true
console.log(regex.test(string2));// true
console.log(regex.test(string3));// true
console.log(regex.test(string4));// true

其中/和.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。

假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:

var regex=/\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1="2017-06-12";
var string2="2017/06/12";
var string3="2017.06.12";
var string4="2016-06/12";
console.log(regex.test(string1));// true
console.log(regex.test(string2));// true
console.log(regex.test(string3));// true
console.log(regex.test(string4));// false

注意里面的\1,表示的引用之前的那个分组(-|\/|\.)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。

我们知道了\1的含义后,那么\2和\3的概念也就理解了,即分别指代第二个和第三个分组。

看到这里,此时,恐怕你会有三个问题。

4. 非捕获分组

之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。

如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p),例如本文第一个例子可以修改为:

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

5. 匹配成对标签

要求匹配:

<title>regular expression</title>

<p>laoyao bye bye</p>

不匹配:

<title>wrong!</p>

匹配一个开标签,可以使用正则<[^>]+>,

匹配一个闭标签,可以使用<\/[^>]+>,

但是要求匹配成对标签,那就需要使用反向引用,如:

var regex=/<([^>]+)>[\d\D]*<\/\1>/;
var string1="<title>regular expression</title>";
var string2="<p>laoyao bye bye</p>";
var string3="<title>wrong!</p>";
console.log(regex.test(string1));// true
console.log(regex.test(string2));// true
console.log(regex.test(string3));// false

其中开标签<[^>]+>改成<([^>]+)>,使用括号的目的是为了后面使用反向引用,而提供分组。闭标签使用了反向引用,<\/\1>。

另外[\d\D]的意思是,这个字符是数字或者不是数字,因此,也就是匹配任意字符的意思。

 

上一篇:正则表达式


下一篇:一日一技:使用二分法排查正则表达式的异常