正则表达式

前言

第一次看到正则表达式这个名次可能会有点儿懵,什么叫正则?其实这个是翻译的问题,正则表达式的英文是Regular Expression,常常被简写为Regex,直译过来就是有规律的表达式。所以说,正则表达式就是指描述某种规则的表达式之意。

其实说白了,正则表达式就是用于字符串匹配的工具。我们可以使用正则表达式描述出来我们想要寻找的字符串符合某种规则。其实正则表达式也没有什么十分复杂的地方,主要就在于匹配符的记忆,以及匹配的经验。下面就直击要点,开始介绍正则表达式的匹配符。

正则表达式语法

普通字符

字母、数字、汉字、下划线、以及没有特殊定义的标点符 号,都是“普通字符”。表达式中的普通字符,在匹配一 个字符串的时候,匹配与之相同的一个字符。

这个玩意是最简单的。比如说hello就是匹配helloworld就是匹配world。没什么要说的地方。

常用的转义字符

匹配符 含义
\n 换行符
\t 制表符
\\ \本身
^ ,\(, \)等一系列 用来匹配\后面的字符

学过任何一门的编程语言的应该都可以轻松理解上面的意思。因为^ $ ( ) { } [ ] .等这些符号在正则表达式语法具有某些特殊的含义,所以我们需要使用\转义之后才可以匹配这些字符本身。

标准字符集合

匹配符 含义
\d 任意一个数字, 0-9中的任意一个
\w 任意一个字母、数字、下划线中任意一个
\s 空格、制表符、换行符等空白字符中的任意一个
. 任意一个字符除了换行符(\n)

所谓的标准字符集合的意思就是可以和多个字符匹配的表达式。

上面的这些匹配符都是区分大小写的,而且大写是相反的意思。比如\D表示的意思是匹配除了数字以外的任意一个字符\W表示匹配除了字母、数字和下划线以外的任意一个字符\S表达匹配除了空格、制表符、换行符这些空白字符以外的任意一个字符。需要注意的是上面的.不要匹配换行符,如果需要匹配任意一个字符的话,推荐使用[\s\S]。细心的其实一下子就可以才出来[]是取并集的意思。

自定义字符集合

也就是上面说的那样,[]表示匹配其中的任意一个字符。

匹配符 含义
[abc@f] 匹配a、b、c、@、f中的任意一个字符
[^abc] 匹配除了a、b、c之外的任意一个字符
[c-g2-9] 匹配c到g之间的任意一个字母或2-9之间的任一个数字(包含两端)
[^A-Z] 匹配除了A-Z之间字母以外的任意一个字符

从上面可以看到^-这两个字符在[]中是有特殊含义的。

^需要放在[]的最前面表示取反的(除以下的字符以外)的意思。

-需要放在字母之间,数字之间,表示其中间的所有字符。注意不能混用,不可以出现a-Z这种写法。

  • 除了^-之外其他的所有的特殊字符到了[]中便失去了其原本的含义。比如.不再代表万能匹配符,而就是匹配.
  • 如果想要在[]匹配字符^或者-,需要使用转义字符的方式。如[\^\-]可以匹配^或者-之间的任意一个字符。
  • 标准字符集合除了.[]中依旧保持其原有的含义。比如[\d\s]匹配任意一个数字或者任意一个空白字符。上面说到的[\s\S]就是匹配任意一个字符。

量词

量词,是用来修饰匹配次数的特殊符号

量词 含义
{n} 表达重复n次
{n, m} 表示最少n次,最多m次
{n, } 表示最少n次
? 表示出现0次或者1次
+ 表示出现至少一次,相当于{1,}
* 表示不出现或者出现任意次,相当于{0,}

比如有如下的字符串

shr sher sheer sheeer shrr

使用表达式sh\w{3}r可以匹配到sheeer

使用表达式sh[a-z]{1,3}r可以匹配到sher,sheer

使用表达式sh\w{2,}r可以匹配到sheer,sheeer

使用表达式sh.?r可以匹配到shr,sher,shrr

使用表达式sh.+r可以匹配到shr sher sheer sheeer shrr

使用表达式sh.*r可以匹配到shr sher sheer sheeer shrr

上面就是这些量词的用法,看起来时蛮简单的,不过你可能主要到了一个问题,就是最后的两个的匹配。可能我们原来的意思是给我匹配shr,sher。。。这样的多个字符,而他却只给我匹配了一个大字符,这是为什么呢?

  • 这是因为匹配模式默认开启贪婪模式(匹配的字符串越多越好),所以说只要还下一个字符还是可以匹配的就会继续加上去,使得匹配的字符串越唱越好。
  • 我们可以在量词的后面加上开启非贪婪模式(匹配的字符串越少越好)

使用表达式sh.+?r可以匹配到shr sher,sheer,sheeer,shrr

使用表达式sh.*?r可以匹配到shr,sher,sheer,sheeer,shr

上面就是正则表达式中的量词的基本的使用方式。

字符边界

字符边界并不是用来匹配某一类字符用的,而是用来描述某种调节的位置。

匹配符 含义
^ 与字符串开始的地方相匹配
$ 与字符串结束的地方相匹配
\b 匹配一个单词边界

所谓的\b所描述的单词边界的意思就是这样的一个位置——位置的两边不都是\w

有如下的字符串待匹配

sher shera asher asherb sherb
sher sher

使用表达式^sher可以匹配到位于字符串最开始的第一个sher

使用表达式sher$可以匹配到第二行中的最后的一个sher

使用表达式sher\b可以匹配到四个sher,除了上面的上个sher外,asher的尾部sher被匹配了。

上面就是字符边界的一般使用方法。

不过如果我们需要匹配每一行的开始的sher,或者最后的sher,需要怎么写表达式呢?

window中的换行符是\r\n,我们可以是用sher\r\n匹配到除了最后一行的末尾,所以使用sher\r\n|sher$的表达式来匹配每一行末尾。使用\r\nsher|^sher来匹配每一行的开始。不过有没有简单的方式呢?

那么我们就需要调节正则表达式的模式了。正则表达式有以下的三种模式可以选择。

  • IGNORECASE 忽略大小写模式

    • 匹配时忽略大小写。

    • 默认情况下,正则表达式是要区分大小写的。

  • SINGLELINE 单行模式

    • 整个文本看作一个字符串,只有一个开头,一个结尾。
    • 使小数点 “.” 可以匹配包含换行符(\n)在内的任意字符。
  • MULTILINE 多行模式

    • 每行都是一个字符串,都有开头和结尾。
    • 在指定了 MULTILINE 之后,如果需要仅匹配字符串开始和结束位置,可以使 用 \A 和 \Z

如果要实现上面我说的匹配每一行开头和结尾。可以开启多行模式。这时^就是指每一行的开始$就是指每一行的结束。此时如果我们还是想要匹配字符串的开始和结束位置可以使用\A\Z

选择符和分组

表达式 作用
Expression1|Expression2 分支结构。表示或,匹配左面或者右面
(Expression) 捕获组。作用下面叙述
(?:Expression) 非捕获组。作用下面叙述

上面的第一个分支结构,上面我也说过了。在非多行模式下表达式:^sher|\r\nsher其中左面是第一行的开始,右面是其他行的开始。取或就是所以行的开始位置。

那么那个所谓的捕获组又是什么意思呢?其实这个名字听起来玄乎玄乎的,其实理解起来也是很容易的。

  1. 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
  2. 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
  3. 每一对括号会分配一个编号,使用 () 的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式 匹配的文本

第一点其实上面忘了说了,表达式she{3}r中的{3}只可以描述前面的一个字符e,只可以匹配sheeer。如果要描述前面的多个字符需要使用(),比如说(she){3}r就可以匹配sheshesher

第二点应该和第三点合起来,讲的大概意思就是正则表达式会将()进行分组,其中0表示的就是匹配到的整个字符串。比如表达式((sher)|hony)|(sherhony)。就被分为了四个组。0就是这个表达式本身,剩下来的从(开始从1开始分组。第一组是(sher)|hony。第二组是sher,第三组是sherhony。那么获取了这写分组有什么作用呢?这就要谈到下面的新语法了。

反向引用

通过反向引用可以对分组中已经捕获的字符串进行引用。

语法: \nnn。nnn表示的分组的组号。

举例:假如我们要在字符串中匹配这样的一个单词,他的前面两个字符和后面的两个字符是相等的。

字符串为 goingo sher todotodo hony

正确表达式为:\b([a-z]{2})[a-z]*\1\b

上面的表达式中最后的\1就是一个反向引用。引用的是前面匹配到的[a-z]{2}。用这种方式就可以确认单词的首位的两个字符是相同的。而两个\b确保了这是一个单词。

非捕获组

不过这个捕获组也有一定的问题。我们之所以可以引用,是因为他将之前括号中的匹配到的字符串放入了内存中供我们后面使用。加入我们不想要引用这部分的类容呢?我们仅仅只是使用括号用于作用一,而不想要浪费捕获组的这部分内容时。我们就可以使用非捕获组(?:Expression)

比如上面的(she){3}r就可以改成(?:she){3}r。毕竟这个表达式中,使用捕获组是毫无意义的,使用非捕获组的时候,并不会影响字符串的匹配。(不使用反向引用)使用非捕获组还使用反向引用这就是非常没有意义的行为了!!

零宽断言

所谓的零宽就是指没有宽度的意思。很明显这些表达式都是不匹配字符的,只是匹配位置的。比如之前学过的^$\b都是零宽的表达式。下面又来介绍一下零宽的另一些表达式。

表达式 含义
(?=Exp) 断言自身位置的后面可以匹配Exp
(?<=Exp) 断言自身位置的前面可以匹配Exp
(?!Exp) 断言自身位置的后面不可以匹配Exp
(?<!Exp) 断言自身位置的前面不可以匹配Exp

字符串:1sher 1shersher sher1 sher34

表达式:sher(?=\d)可以匹配到后面的两个sher不过不会匹配到他们后面的数字。所以这个表达式的意思是匹配后面的一个字符是数字的那个sher。如果使用的是sher(?=\d\d)就只能匹配到最后的那个sher。

表达式: (?<=\d)sher可以匹配到前面的两个sher。(第二个的前面)

表达式: sher(?!\d)可以匹配到前面的两个sher。(第二个的后面)

表达式: (?<!\d)sher可以匹配到后面的两个sher

上面的所有的基本上就是正则表达式的语法了。不过想要掌握正则表达式还是需要不断的练习的。

正则表达式练习

匹配电话号码

电话号码分为座机和手机。其中座机可以表示为0\d{2,3}-\d{7,8}手机可以表示为1[34578]\d{9}。(手机号的第二位现在只有34578)

综合上面我们可以得到电话号码的匹配表达式\b0\d{2,3}-\d{7,8}\b|\b1[358]\d{9}\b。其中使用的\b是确保这个不是其他数字的一部分。(不过有时候也不要这个\b,玩意电话号码就是和字母连一起了呢?)。

匹配电子邮件

电子邮件的格式是比较简单的。这里我使用的是。

\b\w+@[\da-z]+(\.[a-z]{2,3})+\b

我看了一下网上给出的匹配字符串的正则表达式是

\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

这个我倒是感觉没啥必要,一般的用户名都是只可以\w的。至于后面的域名匹配我倒是感觉他的不够严肃了,而且看起来很沙雕。

匹配HTML标签

这个应该是用处最大的东西。HTML标签分为两种标签,一种是成对存在的,另一种是单独存在的。这里我写的表达式是:

<\s*([a-z]+)[^>]*>.*?</\1>|<.*?/>

其中第一个\s是防止某些不规范的写法,比如< a>。后面是一个捕获组([a-z]+)用来捕获标签的名字。再后面的[^>]*用来捕获标签中的其他的一些属性值。[\s\S]*?用来捕获标签中的内容。注意使用\s\S而不是.,是因为很多标签的前后都会有很多的内容,也就是说会换行,而.无法匹配换行。所以要使用\s\S。后面的</\1>中的\1是一个反向引用,用来引用前面的标签中的内容,前后标签的内容是要相等的。后面的<.*?/>是用来匹配单标签的。

不过上面的正则表达式还是有问题的。比如下面的这个HTML中。

<div>
    <div></div>
</div>

会出现匹配失误。第一个<div>和另一个</div>匹配了。不过关于这个问题,我也没有想到什么解决方式。看来使用正则表达式来匹配嵌套的标签还是有一定的难度的。

Java中的正则表达式

public class Demo1 {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("sh.*?r",Pattern.CANON_EQ);
        Matcher matcher = pattern.matcher("sher sheer sheeer shrr");
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

使用的形式主要如上。其实也没什么好说的东西。使用Pattern编译正则表达式,后面可以选择相应的模式。然后匹配一个字符串,返回一个Matcher,然后使用findgroup方法就好了。

总结

上面只是简单的介绍一个正则表达式的语法,其实正则表达式还有其他很多的高级语法,可以等遇到了再学习吧。下面我准备写一个使用正则表达式来爬虫(Java)的一个小代码。蛤蛤蛤~


一枚小菜鸡