正则表达式教程

正则表达式教程

本文是搬运,来源是我五年以前在贴吧发的帖(

最近度娘不吃了,就整回来吧。以此缅怀忘却的初中时代。

不过现在再仔细看,发现已经都忘了

看着五年以前自己写的帖子学习.jpg

正则表达式是字符串学习重点,也是难点。lz我苦学一个国庆,对其略有收获,谨以此专题让大家对正则表达式有一些认识。

INDEX

笔者学习资料为 C语言中文网

本帖原载 百度贴吧链接

一、正则表达式的“卵用”

在Bilibili弹幕网的度娘词条下,有这么一句话:“……ID封禁一般是由触犯特定的正则表达式引起的……”我国庆没补成番怎么想也是正则表达式的错!由此可见,正则表达式的一大用途在于匹配。

而常用功能查找替换实质也是正则表达式。由此足矣见得正则表达式运用范围之广。

那么问题来了,什么是正则表达式?顾名思义,正则表达式就是正确法则表达式,是对于字符串的匹配操作时所用的表达式。

二、最简单的正则表达式

1.png

2.png

Java的正则表达式算法与.net相同,有专用的测试工具。lz我呢,没电脑,所以就随便安了个正则表达式测试工具。

在java中使用.maches方法,语法格式如下:

str.matches("String regex");

返回值为boolean型。例如:

1
2
String str1=new String("Hi");
System.out.println(str1.matches"Hi");

结果为True.

三、转义字符与元字符

在正则表达式中转义字符是很令人心烦的。在转义字符前我们必须引出一个概念,元字符。元字符是一系列正则表达式独有的表示特殊意思的符号。其典型代表即是\b

\b是正则表达式的一个特殊字符。为什么这么说呢?它表示的是一个单词的开头结尾。假使要查找hi,万一碰见hime,history,his,him,hire,hippo,.etc就不妙了。因而来个开头结尾的标志是很有用的。因此,查找hi时我们可以用:\bhi\b

下面是一些常用的元字符:

  • . 匹配换行符外的任意字符
  • \w 匹配字母或数字或下划线或汉字
  • \s 匹配任意空白符
  • \d 匹配数字
  • \b 匹配开始和结束
  • ^ 匹配开始
  • & 匹配结束

我们发现,^$等价于\b

.表示任意一个字符。

而另一对元字符{}表示重复次数。例如:

\ba.{3}\b表示以a打头的4字母单词。不幸的是,这连ae33都能表示。因而我们可以改成:\ba[a-z]{3}\b,我们一会讲到。

{}有三种形式,{a},{a,},{a,b},分别表示a次,大于等于a次,和大于等于a小于等于b次。
表示重复的还有?(表示0-1次)*(表示大于等于0次)+(表示大于等于1次)

()表示优先级。这个我们先不讨论。

如果元字符为[,那么正表达式字符为何?答案是\[。换句话讲,\具有取消转义的作用。同样的,\.\|\$\?等等的意思便不言自明了。

\n\t很特别。它们既属于char型转义序列,又属于正则转义序列。因此,\t等价于\\t,\n等价于\\n.

但更复杂还在于后面。我们主要说的是java,而java又有所不同。在java中,正则表达式被””扩起,实质还是字符串。因而需要再一次转义:

原始字符 { \b \

正则表达式转义 \{ \\b \\

Java中maches方法调用表示 \\{ \\\\b \\\\

熟悉了java中转义调用后,我们之后就不再涉及有关问题了。

还要注意一点,成对出现的不可拆散。

例一:有理数

\d不支持大于等于10的数、负数、小数与虚数(你说的什么废话),换句话说它就是1-9。怎么确定是不是有限小数,我们来编辑一个正则表达式:

?-\d+?(\.\d+)

表示任意有理数。但虚数怎么办?大家下去思考一下。

四、分支、反义、与或非

假如想要查找一个英文单词是不是相对开音节(即aeiou+辅+e)怎么办?别着急,马上揭晓。

事实上,我们上一节埋了一个伏笔:[]|。后者我们很熟悉,是或,而前者也是或。[12345_ah]表示1,2,3,4,5,_,a,h中任意一个。同时还可以[1-7],表示1-7中7个数中的一个。很简单w!

当然,[]内可以加个,分割

注意,[]内使用-最好进行转义。

因此\w=[a-zA-Z_1-9]

但是只有在[]内才用转义喔,千万要小心……

开头的问题便可以解决了:

\w+[aeiou]\we

另一个常见的是反义。下表列出了一些常见的反义字符:

  • \W 不是字母、数字、下划线、汉字
  • \S 非空白字符
  • \D 非数字
  • \B 非开始或结束
  • [^x] 非x
  • [^aeiou] 非aeiou

换句话讲,\W=[^\w]=[^a-zA-Z_1-9],对吧?

例二 天朝电话号码

天朝电话号码有两种:3-8与4-7,分别又有许多种写法:010-85427975;01075426886;(062)75424675;(033)-75422466……

因而这是相当复杂的,特别是组合到一起的时候……

先来看3-7:

这个正则表达式是这么写的:

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

但是残念的是,它还能匹配(010-86429765这种不正确的电话番号。怎么破?答案很简单:用|隔开。

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

怎么样,看懂了吗?试着匹配一下4-7吧!

例三 ip地址

这个可谓是相当坑爹的了,其表达式如下:

\d+\.\d+\.\d+\.\d+

然而这是不太对的,至少,1554.86454.86534.8753277合法吗?当然不合法!那怎么办?有人想出了这个:

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

然后进一步简化:

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

然而还有问题:ip地址每个值不能超过255.但正则表达式没有定义任意一个数学运算,因而最后我们得到了这么一个奇异的表达式:

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

怎么样,试着分析分析吧!

然后我们来看一看补集、交集、并集:

[a-z[def]],等价于[a-zdef],指的是并集;

[a-z&&[def]],等价于[def],指的是交集;

[a-e&&[^db]],等价于[ace],指的是补集;

这就是正则表达式的与或非。

五、Unicode与正则表达式

如果你用的是Phython语言的话,这确实很难处理:涉及到GBK编码与Unicode一向很复杂。幸亏java默认Unicode,这节会比较简单。

3.png

是的——这是万恶之源——大家都能看懂,我就不说什么了

实际上还有几个:

  • \p{XDigit}表示16进制数字;
  • \p{Space}表示空白字符;
  • \p{InGreek}Greek块中的字符
  • \p{Lu}大写字母
  • \p{Sc}货币字符
  • \P{InGreek}非Greek块中的字符

当然,p可以随意反义表示非,如\P表示非货币字符

注意根据Unicode编码,\u4e00-\u9fa5是中文字符,因而[\u4e00-\u9fa5]可用来匹配中文字符。

Unicode定义了三种形式,Unicode Property、Unicode Block与Unicode Script。JAVA支持前两种,第一种是按功能分类,即上述;第二种是按国家地区分类,这里举一例:
\p{InCJK_Compatibility_Ideographs}表示中日韩文字符。

六、分组,注释

(exp表示某一表达式)

分组是方便表示的方法。这个名字是我为了理解起的,真实名字应该是捕获,或者后向引用……

小括号扩住的可以称作一组。组号随括号叠加而递增。引用时可以用\组号来定。

注意嵌套,前后为准。

例四 重复单词

本例想来探讨探讨连续单词的正则语法,而我们用以前的知识可以写出来:

\b\w+\b\s\b\w+\b

但是这样匹配出来并不是重复的。

可以用这种方法:

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

注意,引用组的时候是需要用”+”连接的。这里是\1实质上是对第一个组的引用,组的标号从1开始。其效用是,将第一个捕获的结果存储起来,和数组一样放到一个组里。下次直接调用看是否相等。

除去数字默认的以外,组名也可以是自己起的,例如:

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

或者将<name>改为'name'

然而java似乎不支持……

注释则是和//一样的东西,格式为:

(?#exp)

随便写,反正不管

例如:
(?#LittleBusters最高!)(\b\w+\b)\s+\1(?#lzsb)

七、零宽断言

XD…终于跋涉到了零宽断言…你会感到坑爹的…

什么叫断言呢?断言是某种条件,而零宽指的是某种位置。下面四种用法是零宽断言最常见的四种用法。

第一个是?=。这个零宽断言叫做零宽度正预测先行断言(啥?),卵用就在于,断言此位置之后能够匹配表达式。

什么?看不懂?哦。你若看得懂就是一个天才了。

那么来解释解释:

看一句话:

I’m singing while you’re dancing.

假设我们要查找一个单词中ing之外的东西怎么破?

我们来提供一个正则表达式:

\b\w+(?=ing\b)

那么在一个正则表达式工具测试截图如下:

4.png

换句话说,?:用于预测一个原文中是否有表达式之前的部分

第二个是?<=。这个零宽断言叫做零宽度正回顾后发断言(啥?),卵用就在于,断言此位置前面能匹配表达式。

同样看一句话:

This is a simple sample of the book.

我们来写这么一个正则表达式:

(?<=\bs)\w+\b

然后进行查找的话,结果是

imple ample

换句话说,?<=用于预测一个原文中是否有表达式之后的部分

假如还是看不懂,我们来这么解释一下:

两个正向零宽断言都是用于提供一个查找的索引,之后匹配索引之前(?<=)或之后(?=)的一些字符。

那么下面的两个零宽断言是负向零宽断言。

负向零宽断言的来源是,假设我们想要匹配一个单词,不是相对开音节,亦即不是元加辅加e,这个时候你可能会写出这么一个表达式:

\b\w*[^aeiou]\w*\b|\b\w*[aeiou]\w[^e]\b|\b\w*[aeiou]\w\b

很长对吧?而且对非单词没法处理,比如this?

那么怎么破?也许你会这么做:

[^e]改为[a-df-z]

好吧你狠,然而你的隔壁却用了一个简单的负向零宽断言,然后解决了这个问题。他是这样写的:

(?!e)

?!,即零宽度负预测先行断言,断言此位置后面无法匹配表达式。例如,abc(?!def)表示abc后面不能匹配def.

相对应的,?<!,零宽度正回顾后发断言,断言此位置前面不能匹配表达式。例如,(?<![a-z])\d{7}匹配前面不是字母的七位数字。

下面一个例子是零宽断言的最重要运用之一。

(?<=<(\w+)>).*(?=<\/\1>)匹配不包含属性的简单HTML标签内里的内容。(<?(\w+)>)指定了这样的前缀:被尖括号括起来的单词(比如可能是<b>),然后是.*(任意的字符串),最后是一个后缀(?=<\/\1>)。注意后缀里的\/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是<b>的话,后缀就是</b>了。整个表达式匹配的是<b></b>之间的内容(再次提醒,不包括前缀和后缀本身)。

恭喜,零宽断言结束。

八、贪婪、懒惰、优先级

正则表达式中有一项原则,前大于后。这个无须赘述。

我们主要要讨论的优先级是,贪婪/懒惰。

”当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。考虑这个表达式:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。

常用如下:

  • *? 重复任意次,尽可能少重复
  • +? 重复至少一次,尽可能少重复
  • ?? 重复0-1次,尽可能少重复
  • {n, m}? 重复n到m次,尽可能少重复

九、平衡组和递归匹配

这部分内容当时候没学,现在补上吧

其核心思想其实可以看成一个递归。当匹配到什么什么东西的时候就压入栈,然后规定什么时候出栈,最后用一个断言来讨论其正确性。

这一形式最常见的地方就在于多重嵌套的括号匹配问题。比如((()()))()如果直接匹配\(.*\),得到的是整个字符串;而如果用懒惰匹配\(.*?\),得到是((()。哪种都不符合人意。

(?'qwq')将捕获内容命名为qwq,并压入堆栈。如果有(?'-qwq'),就将栈顶出栈。(?(qwq)Branch1|Branch2)表示栈里是否有qwq,如果有执行branch1,没有执行branch2

因此,我们可以写出下面的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
\(
(
(?<qwq>\()
|
(?<!qwq>\()
|
[^()]+
)*
(?(qwq)
(?!)

)
\)

解释一下。首先匹配左右的两个括号,然后我们分组。在组内,我们讨论三种情况:

  • \(,压入堆栈
  • \),弹出栈顶
  • 都不是,不操作

这一过程可以重复n次,直到我们见到下一个断言:

如果qwq还有,我们直接执行零宽断言(?!)。由于后继表达式为空,所以直接匹配失败。反之我们记匹配成功。

最后写起来,就是

\(((?<qwq>\()|(?<!qwq>\()|[^()]+)*(?(qwq)(?!))\)

附录一 元字符

5.png

6.png

7.png

8.png

附录二 常用正则表达式

中国电话号码验证

匹配形式如:0511-4405222 或者021-87888822 或者 021-44055520-555 或者 (0511)4405222

正则表达式 (\(\d{3,4})|\d{3,4}-)?\d{7,8}(-\d{3})*

中国邮政编码验证

匹配形式如:215421

正则表达式 \d{6}

电子邮件验证

匹配形式如:justali@justdn.com

正则表达式 \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]w+)*

身份证验证

匹配形式如:15位或者18位身份证

正则表达式 \d{18}|\d{15}

非法字符验证

匹配非法字符如:< > & / ‘ |

正则表达式 [^<>&/|'\\]+

日期验证

匹配形式如:20030718,030718

范围:1900–2099

正则表达式((((19){1}|(20){1})\d{2})|\d{2})[01]{1}\d{1}[0-3]{1}\d{1}

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×