很久之前便接触过正则,也用过不少,但是也从来没自己系统性总结下,反而每次都是去搜索别人的博客,突发奇想问了下CTF队伍中同学是否常使用正则表达式,发现还是挺重要的,也想对这块进行一些系统性的总结,涉及的内容可能会比较浅,会尽量通俗易懂,结合例子让读者更能理解。

什么叫做正则表达式?

在进行编程开发时,经常需要进行字符串的操作,如查找、替换等,其中很重要的一点便是匹配符合某种规则的字符串,而正则表达式就是对这些规则的描述,可以使用正则表达式对字符串的格式、规则或含有哪些字符进行描述。正如在linux系统中,如果需要搜索test目录下的python脚本文件,则需要使用的,命令为 "find ./test -name ‘*.py’ " ,其中 “*.py” 中的 ‘*’ 即为匹配任意字符串,这里其实和正则表达式也类似,叫做通配符。而正则表达式则是比这样的规则更为复杂的,能够准确描述一些特殊字符串格式的规则字符串。

入门与简单示例

先介绍一些简单的例子,从而对正则表达式有一个基本的理解。上文已说过,正则表达式其实是一种对字符串格式进行描述的字符串,所以在这样的字符串中,需要规定某些特殊字符、字符组合、符号用以表示一些特殊的含义,比如上述所说的 “*.py” 中的 “*” 就是这样一种,所以,在开始举例前,先介绍一些比较常用的特殊代码(或者叫元字符)

\b	匹配字符(串)的开头和结尾,用以规定需要匹配的字符串和其余字符串的分界处
\w 匹配字母或数字或下划线或数字
\s 匹配任意的空白符(包括空格、制表符Tab、换行符、中文全角空格等)
\d 匹配数字
^ 匹配字符串的开始
$ 匹配字符串的结束
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 匹配制表符,Tab
\r 匹配回车
\v 匹配竖向制表符
\f 匹配换页符
\n 匹配换行符
. 匹配除换行符外的任意字符
* 与前面使用的元字符连接,表示前一个使用的元字符表示的字符匹配任意次(零次或多次)
+ 与*类似,但是+匹配一次或多次
? 与*类似,但是是匹配重复一次或者零次

上述字符中,后三个或许应该称为限定符,下面使用这些基本的元字符构造几个正则表达式的示例:

  • \ba\w*\b
    匹配以字母a开头的单词——先是某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)。
  • \b.*xiaoZ.*\b
    匹配一串字符,字符前是任意数量的除换行符外的任意字符,然后是 “xiaoZ” 最后是任意数量的除换行符外的任意字符。’.*’ 表示匹配任意数量的除换行符外的任意字符。
  • \d+
    匹配一个或更多连续的数字。

这里的例子比较简单,但是可能也有人会想到,如果需要匹配字符为 ‘\b’ 呢?如果直接用在正则中进行匹配的话则会将其解释为匹配字符串开头和结尾,所以这里需要进行转义,转义在编程中用的比较多,一般使用反斜杠来取消这些字符的特殊意义,正则表达式中也是一样,只需要使用 ‘\’ 取消字符中 ‘\’ 的特殊含义,即可实现该效果,即使用 ‘\\b’ 。总结一下就是,如果我们需要匹配的字符中含有作为正则表达式规定的元字符时,需要使用反斜杠符号进行转义操作。说到这个,你觉得我在写这个博客的时候,要出现 ‘\\\\’ 应该写几个反斜杠呢…

正则表达式进阶

重复与分组

可以看到,上述匹配中除了使用单一的匹配完,还使用了 ‘*’ 这样的元字符匹配多个数量的字符,在正则表达式中,使用来表示重复的元字符也叫做限定符,主要有以下几个:

*	与前面使用的元字符连接,表示前一个使用的元字符表示的字符匹配任意次(零次或多次)
+ 与*类似,但是+匹配一次或多次
? 与*类似,但是是匹配重复一次或者零次
{n} 重复n
{n,} 重复n次或更多次
{n,m} 重复n到m次

比如,表示匹配xiaoZ后跟1个或者更多数字使用下列正则表达式:

xiaoZ\d+

上述所使用的重复限定符中,只是对单个字符进行重复,如果需要对于多个字符进行重复呢?

这个时候需要使用的就是分组了,如果在匹配一个字符串时,需要匹配的是满足某串字符重复多次的字符串时该如何匹配?这里使用的就是分组,其实叫做分组可能并不是很准确,倒不如理解为分组重复,即对于某种特定的规则,需要重复匹配多次。

分组中使用的是小括号结合上述所说的限定符,如果需要表示 ‘xiaoZ’ 重复3次则使用:

(xiaoZ){3}

如果需要匹配IP地址,则使用下列正则表达式:

(\d{1,3}\.){3}\d{1,3}

很显然,上述正则表达式只是简单的匹配,而对于IP地址来说,对于每一位的最大值都是有固定规定的,所以需要进行一些比较的操作,可以使用下列正则表达式(其中可能使用了后面才讲到的字符,可以先跳过,随后返回进行分析):

((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

后向引用

那么这里又存在另外一个问题了,如果我现在在一个字符串中,某串字符可能重复多次,但是在不同的地方又该如何表示?此时简单的使用重复与分组已经用处不大,所以需要使用另外一种表示方式,即后向应用。

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

举个例子进行说明

\b(\w+)\b\s+\1\b

可以用来匹配重复的单词,像go go, 或者kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)

当然,对于组名的指定,也可以自己进行指定,比如上述匹配单词,则指定一个子表达式的组名为Word,需要使用这样的语法:(?\w+)(或者把尖括号换成’也行:(?‘Word’\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容,也可以使用\k,所以上一个例子也可以写成这样:

\b(?<Word>\w+)\b\s+\k<Word>\b

另外,使用小括号的时候,还有很多特定用途的语法。如下:

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

预定义字符组

上述正则表达式中,所用到的匹配都是对于某一类字符进行匹配,那么如果需要编写一个程序,查找一串字符串中包含有 ‘abc’ 三个字母的个数有多少个需要如何表示呢?在正则表达式中,可以通过将字符预定义为类来匹配指定的字符,格式为 ‘[需要匹配的字符集]’ ,如上述例子则使用 ‘[abc]’ 匹配三个字母中任意一个,而这样的字符集结合限定符进行使用,则能够更好的匹配一些复杂的字符串。

除了直接指定字符外,也可以指定字符的范围,如使用 ‘[0-9]’ 匹配0到9的数字,’[a-z0-9A-Z]’ 则与 ‘\w’ 具有同样含义。(注意这里使用的不同范围直接写在一起)。

分枝条件

先看一个例子:

\(?0\d{2}[) -]?\d{8}

这个表达式可以匹配几种格式的电话号码,
比如(010)88886666,或022-22334455,或02912345678等。

首先是一个转义字符\(,它能出现0次或1次(?)
然后是一个0,后面跟着2个数字(\d{2})
然后是)或-或空格中的一个,它出现1次或不出现(?),最后是8个数字(\d{8})

上述对于电话号码进行匹配的正则表达式,看似挺不错,但是观察可以看到,这里因为 ‘(’ 和 ‘)’ 中使用的是 ‘?’ 匹配每次使用0次或1次,则如果现在给出的是 ‘010)12345678’ ,虽然给出的电话号码是错误的,但是依旧是符合这个正则表达式的。如何解决这个问题呢?

对上述问题进行分析,出现这样问题的原因在于对括号或一些可有可无的字符的匹配存在问题,类似于一个排列问题,符合上述正则表达式的排列有8种(’(?’ 为2,’[) -]?'为4,相乘为8),但是其实我们需要匹配的电话号码只是这8种中的一部分,所以可以我们需要的那几种排列列出,满足这几种排列任意一种的正则表达式的字符串即可匹配。而在将这几种进行排列时,使用的就是分枝条件,这里使用的符号为 ‘|’ ,如下列正则表达式:

0\d{2}-\d{8}|0\d{3}-\d{7}

这一正则表达式则是将两种排列分别列出,使用’|’ 符号进行连接,用以匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)。

再比如:

\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}

这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。

这里需要注意的是,在使用分枝条件时,需要使用各个条件(排列)的使用顺序,在使用含有多个条件的正则表达式进行匹配时,只会对第一个满足了的条件进行匹配。如上述使用的匹配电话号码的分枝条条件,满足前一条件时,则不会再去匹配其余条件。

反义

上述对各种不同的元字符进行了介绍,那么正如数学中所学的补集一般,有时需要使用正则表达式来匹配除了某种或某个字符外的所有字符,则需要使用反义字符来表示,常用的反义字符如下:

\W	匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^abc] 匹配除了abc这几个字母以外的任意字符

比如: ‘\S+’ 匹配不包含空白符的字符串

结语

对正则表达式的简单介绍也就到这里了,也就仅限于简单介绍了,认真看完的话,应该已经能够看懂一些正则表达式,也能自己写一些了,但是毕竟写的不是很深入,也可能存在一些问题,请包涵,另外的话,如果有想自己深入去看一些关于正则表达式的东西的话,推荐看看关于正则表达式语言元素的MSDN在线文档