正则表达式再学习

在处理字符串的时候,经常会有查找某些符合规则的字符串的需要,正则表达式就是用于描述这些规则的工具。
在 windows/Dos 下有用于文件查找的 通配符,* 和 ? 。例如:

  • *.doc 表示查找目录下所有的 word 文档

和通配符类似,正则表达式也是用来进行文本匹配的工具,只不过比起通配符它能更精确地描述你的需求。

通过实例入门

1
2
3
4
var reg = /\bhi\b/ig;
var regStr = 'lf him---history---ihslsaaahI lsssaaa HI xxxxx Hi';
regStr.match(reg);
// ['HI', 'Hi']

通过该实例最终的打印结果可以看见,这里仅仅将最后两个值 HI 和 Hi 输出了,那么这里的正则表达式究竟做了一些什么呢?
首先,看一下正则表达式 /\bhi\b/ 中的 \b\b 是正则表达式规定的一个特殊代码,也叫元字符(metacharacter),代表着单词的开头或结尾,也就是单词的分界处,虽然英文单词是由空格、标点符号、换行来进行分隔,但 \b 并不匹配这些单词分隔字符中的任何一个,它只匹配一个位置
那么上面这个例子中就是将 hi 这个单词匹配出来,并且不区分大小写,虽然有 history 和 ihslsaaahI 这两个任意单词,但是它们并不满足正则表达式中查找出一个单独的单词 hi(即当前单词 hi 的前后都有分隔符进行分隔的单个单词) 的条件;这里 \b 学了个明白。
再看一个实例:

1
2
3
4
var reg = /\bhi\b.*\bLucy\b/ig;
var regStr = 'aaa hi slslflslfsl啊啊bb Lucy lucylll';
regStr.match(reg);
// ['hi slslflslfsl啊啊bb Lucy']

最终得到的结果如上实例打印值,这里为什么会是出现的这么长一串字符串值呢?
通过第一个实例可以知道 \b 的作用,那么这里出现这种结果的条件肯定是正则表达式中间的 .* 这个操作,这里究竟是在干什么?
首先,这里的 . 也是一个元字符,它的含义是匹配除了换行符以外的任意字符* 同样是元字符,不过它代表的不是字符,也不是位置,而是数量,它的作用是指定 * 前面的内容可以连续重复使用任意次以使整个表达式得到匹配。
因此,这就明白了,.* 连在一起就意味着任意数量的不包含换行符的字符;所以得到了示例中的一串字符。

元字符

在上面的实例中了解到了 \b.* 这几个元字符,而正则表达式中的元字符并不仅仅只有这几个,比如还有 \s\w\d 等。

最后,常用的元字符如下:

  • . 匹配除换行符以外的任意字符
  • \s 匹配任意的空白符,包括空格、制表符(Tab)、换行符、中文全角空格等
  • \w 匹配字母、数字、下划线、汉字
  • \d 匹配数字
  • \b 匹配单词的开始或结束位置
  • ^ 匹配字符串的开始
  • $ 匹配字符串的结束

这里总结了常用的元字符,如果要查找的内容是元字符本身的话该怎么办呢?
例如要查找的是 . 号或者 * 号,这如果直接在正则表达式中的话肯定回出问题,它们会被解释成元字符该有的意思。这个时候就得使用 \ 来取消这些字符的特殊意义。因此,这里使用 \.\* 来查找元字符本身,当然,如果要查找 \ 本身的话也得用 \\ 来表示。

这里对常用的元字符进行了统计,那么,有时候需要查找的字符不属于某个能简单定义的字符类,比如想查找出了数字以外,其它任意字符都行的情况,这是是不是需要一个反义?那么常用的反义代码有哪些:

常用的反义代码

  • \S 匹配任意不是空白符的字符
  • \W 匹配任意不是字母、数字、下划线、汉字的字符
  • \D 匹配任意非数字的字符
  • \B 匹配不是单词开头或结束的位置
  • [^x] 匹配除了x以外的任意字符
  • [^aeiou] 匹配除了 aeiou 这几个字母以外的任意字符
1
2
3
4
5
var regStr = '/\S+/';
// 匹配不包含空白字符的字符串

var regStr1 = '/<a[^>]+>/';
// 匹配用尖括号括起来的以 a 开头的字符串

重复

在查找一个符号或者字符并不总是只会有一个,那么匹配重复的方式改怎么做?
常用的限定符如下:

  • * 重复零次或更多次(星号)
  • + 重复一次或更多次(加号)
  • ? 重复零次或一次
  • {n} 重复 n 次
  • {n,} 重复 n 次或更多次
  • {n,m} 重复 n 次到 m 次

字符类

如果想要查找数字、字母、数字或者空白很简单,前面的元字符就可以对它们进行操作,但是如果想匹配没有预定义元字符的字符集合(如:a、e、i、o、u等)该怎么办?
很简单,直接把它们放在方括号里面就行,如 [aeiou] 就匹配任何一个英文元音字母,[.?!] 匹配标点符号 .?!号。
我们还可以指定一个字符范围,像 [0-9] 代表的是与 \d 一致的意思,只是 \d 只匹配一位数字。
看一个复杂的表达式:/\(?0\d{2}[) -]?\d{8}/
这个表达式可以匹配几种格式的电话号码,如:(023)88889999、023-88889999、02388889999。

分枝条件

在字符类学习内容里面有一个复杂的正则表达式:/\(?0\d{2}[) -]?\d{8}/;如下:

1
2
3
4
5
6
7
8
var numStr = '023)88889999';
var numStr1 = '(02388889999';
var numRegStr = /\(?0\d{2}[) -]?\d{8}/ig;

numStr.match(numRegStr);
// ['023)88889999']
numStr1.match(numRegStr);
// ['(02388889999']

这里将这种不规范的号码格式也能匹配成功,那么要解决这个问题,我们需要用到分枝条件
正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配成功,具体的方法就是用 | 把不同的规则分开;如下:

1
2
3
4
5
6
7
var regStr = '/0\d{2}-\d{8}|0\d{3}-\d{7}/';
// 此匹配能够将三位区号和四位区号的电话号码匹配出来
// 023888899999 0234-8888999

var regStr1 = '/\d{5}-\d{4}|\d{5}/';
// 此表达式用于匹配美国的邮政编码,它们的邮编规则是5位数字,或者是用连字号间隔的9位数字
// 02002-8888 99988

注意:使用分枝条件时,其各个条件的顺序很重要,顺序不同,匹配的条件结果也不同,因为在匹配分枝条件时,将会从左到右的测试每个条件,如果满足了条件就不会去管其他条件了。

分组

前面学习过程中已经知道了怎么对单个重复的字符进行匹配(直接在字符后面加上限定符就行),但是如果想要重复多个字符怎么办呢?答案就是使用小括号来指定子表达式(分组);然后就可以指定子表达式的重复次数了。

1
2
3
4
5
6
7
8
var regStr = '/(\d{1,3}\.){3}\d{1,3}/';
// 这个正则表达式的目的是想做一个匹配 ip 地址的功能表达式
// 但是这里请注意 (\d{1,3}\.){3} 它带一个英文句号连续重复三次将 ip 的前三位匹配出来了,最后 \d{1,3} 将 ip 的最后一位做了匹配,能得到部分正确结果
// 虽然上面的例子能够在真正的 ip 地址上匹配成功,但是,它也能将这种 888.999.299.566 不是正确 ip 地址的字符也成功匹配。。再想想办法

var regStr = '/((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/';
// 这里的关键就是理解正则表达式中的 (2[0-4]\d|25[0-5]|[01]?\d\d?);它将 ip 地址的规律进行了解释,只有满足 0 -255 的 ip 地址值才能被成功匹配。
// tips: IP 地址中每个值都是小于等于 255 的,比如 01.02.09.08 这种带 0 的数字在 IP 地址里也是正确的,因为 IP 地址里的数字可以包含有前导0.

后向引用

使用小括号指定一个子表达式后,匹配这个子表示的文本(也就是此分组捕获的内容)可以在表达式或其他程序中作进一步处理;默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。
后向引用用于重复搜索前面某个分组匹配的文本;例如匹配重复的单词

1
2
var reg = /\b(\w+)\b\s+\1\b/;
// 这个正则表达式的功能是用来匹配重复的单词,比如 hello hello 这种。

上面的表达式能够匹配重复的单词,那么表达式中的 \1 是怎么工作的呢?它代表的是什么?答案很明显,\1 代表分组1匹配的文本,及前面的那个小括号括起来的内容的分组号是1。

除此之外,还可以自己指定子表达式的组名,要指定一个子表达式的组名,可以使用语法:(?<word>\w+)或者把尖括号换成 ' 也行,如:(?'word'\w+) ,这样就把 \w+ 的组名指定为 word 了;如果要反向引用这个分组捕获的内容,可以使用 \k<word> ,所以前面的例子也可以写成如下:

1
2
3
4
var reg = /\b(?<word>\w+)\b\s+\k<word>\b/;
// 这里使用了自己指定的子表达式组名 <word>
// ?<word> 定义的组名
// 反向引用组名 \k<word>

常见的分组语法统计

  • 捕获
    • (exp) 匹配 exp,并捕获文本到自动命名的组里
    • (?exp) 匹配 exp,并捕获文本到名称为 name 的组里,也可以写成 (?’name’exp)
    • (?:exp) 匹配 exp,不捕获匹配的文本,也不给此分组分配组号
  • 零宽断言
    • (?=exp) 匹配 exp 前面的位置
    • (?<=exp) 匹配 exp 后面的位置
    • (?!exp) 匹配后面跟的不是 exp 的位置
    • (?<!exp) 匹配前面不是 exp 的位置
  • 注释
    • (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释

这里学习了捕获类里面的前两种语法,第三个 (?:exp) 不会改变正则表达式的处理方式,只是这样的组匹配的内容不会像前两种那样被捕获到某个组里,也不会拥有组号。

零宽断言

在查找某些内容(但并不包含这些内容)之前或之后的东西,也就是说它们像 \b ^ $ 那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们被称为零宽断言。

(?=exp) 也叫 零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式 exp,例如:

1
2
3
4
5
6
var reg = /\b\w+(?=ing)\b/ig;
// 匹配 ing 结尾的单词的前面部分(除了 ing 以外的部分)
var str = "I'm singing while you're dancing";

str.match(reg);
// [sing, danc]

这里匹配出了 exp 整个单词前面的部分内容。

(?<=exp) 也叫 零宽正回顾后发断言,它断言自身出现的位置的前面能匹配表达式 exp,例如:

1
2
3
4
5
6
var reg = /(?<=sin)\w+\b/ig;
// 匹配 ing 结尾的单词的前面部分(除了 ing 以外的部分)
var str = "I'm singing while you're sincing";

str.match(reg);
// [ging, cing]

这里最终匹配出了 exp 整个单词后面的部分内容。

负向零宽断言

前面提到过怎么查找不是某个字符或不在某个字符里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?
例如:我们想查找一个单词它里面出现了字母q,但是q后面跟的不是字母u,我们可以怎么做?

1
2
3
4
5
6
7
var reg = /\b\w*q[^u]\w*\b/ig;
// 这里的 [^u] 就是匹配除了 u 以外的所有字符

var str1 = 'google search aquery parmas is qq and qddslslsl!';

str1.match(reg);
// ["qq and", "qddslslsl"]

这里得到了最终的结果,但是 aquery 是被排除没有匹配的。但是这里有一个特殊的地方 qq and 被一起匹配出来了,这是因为 [^u] 总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的 [^u] 将会匹配q后面的单词分隔符或者其他的字符,而后面的 \w*\b 会匹配下一个单词,于是就出现了这个 qq and 被一起匹配出来了;那怎么样能解决这个问题呢?

负向零宽断言了解一下,因为它只匹配一个位置,不消费任何字符,如下:

1
2
3
4
5
6
var reg1 = /\b\w*q(?!u)\w*\b/ig;
// 将前面例子中的 [^u] 换成了 (?!u)
var str1 = 'google search aquery parmas is qq and qddslslsl!';

str1.match(reg1)
// ["qq", "qddslslsl"]

零宽度负预测先行断言 (?!exp),断言此位置的后面不能匹配表达式 exp;例如 \d{3}(?!\d) 匹配三位数字,而且这三位数字的后面不能是数字。
同理,零宽度负回顾后发断言 (?<!exp),断言此位置的前面不能匹配表达式 exp;例如 (?<![a-z])\d{7} 匹配前面不是小写字母的7位数字。

注释

小括号的另一种用途是通过语法 (?#comment) 来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199);其中对各个匹配数据做了注释信息。

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是匹配尽可能多的字符。以 a.*b 为例,它将会匹配最长的以 a 开始,以 b 结束的字符串。如果用来匹配 aabab 的haul,它会匹配整个字符串 aabab,这称为贪婪匹配。
有时候,我们更需要懒惰匹配,也就是匹配尽可能少的字符。将前面的限定符转换成懒惰匹配模式,只要在它后面加上一个问号 ? 即可。即 .*? 就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复;例如 a.*?b 匹配最短的以 a 开始,以 b 结束的字符串 aabab,它会匹配 aab 和 ab。

懒惰限定符

  • *? 重复任意次,但尽可能少重复
  • +? 重复1次或更多次,但尽可能少重复
  • ?? 重复0次或1次,但尽可能少重复
  • {n,m}? 重复n到m次,但尽可能少重复
  • {n,}? 重复n次以上,但尽可能少重复

其它语法

  • \a 报警字符
  • \b 单词分界位置,如果在字符类里使用代表退格
  • \t 制表符 Tab
  • \r 回车
  • \v 竖向制表符
  • \f 换页符
  • \n 换行符
  • \e Escape
  • \0nn ASCII代码中八进制代码为nn的字符
  • \xnn ASCII代码中十六进制代码为nn的字符
  • \unnnn Unicode代码中十六进制代码为nnnn的字符
  • \cN ASCII控制字符,比如 \cC 代表 Ctrl+C
  • \A 字符串开头(类似^,但不受处理多行选项的影响)
  • \Z 字符串结尾或行尾(不受处理多行选项的影响)
  • \z 字符串结尾(类似$,但不受处理多行选项的影响)
  • \G 当前搜索开头
  • \p{name} Unicode中命名为name的字符类,例如\p{isGreek}
  • (?>exp) 贪婪子表达式
  • (?-exp) 平衡组
  • (?im-nsx:exp) 在子表达式exp中改变处理选项
  • (?im-nsx) 为表达式后面的部分改变处理选项
  • (?(exp)yes|no) 把exp当做零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式,否则使用no
  • (?(exp)yes) 同上,只是使用空表达式作为no
  • (?(name)yes|no) 如果命名为name的组捕获到了内容,使用yes作为表达式,否则使用no
  • (?(name)yes) 同上,只是使用空表达式作为no

总结

好吧。。内容比较多,自己都已经整晕了。。