1000行代码徒手写正则表达式引擎【1】--JAVA中正则表达式的使用

简介

本文是系列博客的第一篇,主要讲解和分析正则表达式规则以及JAVA中原生正则表达式引擎的使用。在后续的文章中会涉及基于NFA的正则表达式引擎内部的工作原理,并在此基础上用1000行左右的JAVA代码,实现一个支持常用功能的正则表达式引擎。它支持贪婪匹配和懒惰匹配;支持零宽度字符(如“\b”, “\B”);支持常用字符集(如“\d”, “\s”等);支持自定义字符集(“[a-f]”,“[^b-k] ”等);支持所有重复操作(“*”,“+”,“?”,“{n,m}”等);支持通配符(“. ”);支持运算符本身的转义字符(“\*”,“\.”等);支持命名捕获组。本系列博客的目的是理解正则表达式的内部工作原理,所以没有考虑正则表达式引擎的工作效率以及正负断言和非命名捕获组等不常用的功能,后续的工作将会对其优化并完备其功能。由于正则表达式引擎的实现需要运用数据结构中的相关内容,阅读本系列博客前请熟悉栈,队列,图以及JAVA中容器等相关知识。

欢迎探讨,如有错误敬请指正

如需转载,请注明出处 http://www.cnblogs.com/nullzx/


1. 正则表达式的作用

正则表达式的功能就是在文本串中搜索特定模式的字符串。我们以下面方框中豆瓣电影网页中给出的信息为例,我们想在这些文本中找出所有的日期信息,我们发现日期信息的字符格式在以下文本串中具有特定的格式,都是xxxx-xx-xx的模式(比如2017-01-27),这里的x表示一个具体的数字。所以我们搜索的字符串的格式就是“\d{4}-\d{2}-\d{2}”,在正则表达式中\d表示数字,{n}表示重复n次。

猜火车2

猜火车2 / 迷幻列车2(港) / T2:Trainspotting

2017-01-27(英国) / 伊万·麦克格雷格 / 约翰尼·李·米勒 / 罗伯特·卡莱尔 / 艾文·布莱纳 / 雪莉·亨德森 / 安杰拉·奈迪亚科娃 / 史蒂文·罗伯特森 / 戈登·肯尼迪 / 西蒙·韦尔 / 詹姆斯·卡沙莫 / 梁佩诗 / 阿塔·雅谷伯 / 埃文·威尔什 /...

...

...

...

7.8 (5392人评价)

宝贝老板

宝贝老板 / 娃娃老板 / 波士BB(港)

2017-03-12(迈阿密电影节) / 2017-03-31(美国) / 亚历克·鲍德温 / 迈尔斯·克里斯托弗·巴克什 / 吉米·坎摩尔 / 丽莎·库卓 / 史蒂夫·布西密 / 托比·马奎尔 / 詹姆斯·麦格拉思 / 康拉德·弗农 / 薇薇安·叶 / 小埃里克·贝尔 / 大卫·索伦 / 伊迪·米尔曼...

8.3 (184408人评价)

逃出绝命镇

逃出绝命镇 / 访吓(港)

2017-01-23(圣丹斯电影节) / 2017-02-24(美国) / 丹尼尔·卡卢亚 / 艾莉森·威廉姆斯 / 凯瑟琳·基纳 / 布莱德利·惠特福德 / 卡赖伯·兰德里·琼斯 / 马库斯·亨德森 / 贝蒂·加布里埃尔 / 勒凯斯·斯坦菲尔德 / 斯蒂芬·鲁特 / 李雷尔·哈瓦瑞...

7.5 (51576人评价)

由于排版的需要,以上文本框中的内容比下图实际处理数据中的内容为基础进行了删减

我们通过正则表达式测试工具进行文本串中特定模式串\d{4}-\d{2}-\d{2}匹配,结果如下图所示 123

通过得到的处理结果,我们搜索到了文本串中所有的日期信息。从这个例子我们可以看出正则表达式引擎的主要功能就是在给定的文本串中搜索符合正则表达规则的特定模式的字符串,而这个特定的模式是我们通过分析文本串中感兴趣的信息总结得到的一般规律。比如要得到文本中电影的评分,字符串的格式就是“\d.\d”。

除了上述例子外,正则表达式还有很多应用。例如,在注册用户时,验证用户输入的邮箱是否合法;在网络爬虫技术中,爬取我们感兴趣的相关内容;编译器设计中,我们还可以将正则表达式作为词法分析器,等等。使用正则表达式能够使我们更方便,更加高效的解决字符串模式匹配的相关问题,而不必为每一个问题单独写一个程序。这里我们所说的效率高,是指编写程序的效率更高,而非程序的运行效率。

我们的目的是写一个正则表达式引擎,所以我们接下来就非常有必要了解一下正则表达式的一般规则。

2. JAVA正则表达式的规则

2.1 自定义字符集

[abc]

a或b或c

[^abc]

除了a,b,c的其它字符

[a-zA-Z]

满足a-z范围的字符或A-Z范围的字符

例子:下面的正则表达式会匹配两个字符,第一个为小写字母,第二个为数字,文本串中已捕获的内容用红色表示。

 

正则表达式:“[a-z][0-9]”

文本串内容:“absef809sefdk1dfes12389”

 

2.2 已定义字符集

.

可以匹配非换行符以外的任何字符,能否匹配换行符是可配置的

\d

数字,等价于[0-9]

\D

非数字,等价于[^0-9]

\s

空白符,等价于[ \t\n\x0B\f\r]

\S

非空白符,等价于[^\s]

\w

字母、数字或下划线,等价于[a-zA-Z_0-9]

\W

非字母和数字,等价于[^\w]

例子:下面的正则表达式会匹配以非空白字符开头和非空白字符结尾,中间是“abc”的字符串,总共需要捕获5个字符,文本串中已捕获的内容用红色表示。

 

正则表达式:“\Sabc\S”

文本串内容:“abcd abc defabcyjkabc”

 

2.3 转义字符(不解释)

\t

The tab character ('\u0009')

\n

The newline (line feed) character ('\u000A')

\r

The carriage-return character ('\u000D')

\f

The form-feed character ('\u000C')

\a

The alert (bell) character ('\u0007')

\e

The escape character ('\u001B')

\cx

The control character corresponding to x

2.4 零宽度边界字符

零宽度边界字符,只会匹配一个位置而不会占有字符

^

行开始

$

行结束

\b

单词的开始边界或结束边界

\B

非单词的边界

例子:下面的正则表达式会匹配字符串“abc”,并且要求第一个字符‘a’的前面不是字母字符和数字字符,最后一个字符‘c’的后面不是字母字符和数字字符。正则表达式总共需要捕获3个字符,文本串中已捕获的内容用红色表示。

 

正则表达式:“\babc\b”

文本串内容:“abc dabcd abc abcd -abc

 

2.5 贪婪重复模式(尽量多重复)

X表示一个合法的正则表达式

X?

X重复一次或0次

X*

X,重复0次或多次

X+

X重复至少1次

X{n}

X重复刚好n次

X{n,}

X重复至少n次

X{n,m}

X重复至少n次,最多m次

例子:下面的正则表达式会匹配以a开头和a界结尾的,中间有尽可能多的其它字符,且其它字符要求至少有一次,文本串中已捕获的内容用红色表示。

 

正则表达式:“a.+a”

文本串内容:“zxyabcdefasseaa09876”

 

2.6 懒惰重复模式(尽量少重复)

X??

X重复一次或0次

X*?

X,重复0次或多次

X+?

X重复至少1次

X{n}?

X重复刚好n次

X{n,}?

X重复至少n次

X{n,m}?

X重复至少n次,最多m次

例子:下面的正则表达式会匹配以a开头和a界结尾的,中间有尽可能少的其它任意字符,且其它任意字符要求至少有一次。文本串中已捕获的内容用红色表示。

 

正则表达式:“a.+?a”

文本串内容:“zxyabcdefasseaa09876”

 

2.7 逻辑运算符

X和Y分别表示两个正则表达式

XY先满足正则表达式X,然后满足正则表达式Y的正则表达式

X|Y 满足正则表达式X或满足正则表达式Y的正则表达式

注意优先级,X|YZ 等价于 X|(YZ),而(X|Y)Z表示XZ|YZ

 

正则表达式:“a(b|c)d”

文本串内容:“zxyabcdefacdeaabd876”

 

2.8 括号

在正则表达式中的作用有两个,一个和四则运算中的括号相同,用来改变优先级,另一个用于分组捕获。分组捕获又分为两种,一种是自定义命名的分组,还有一种是未命名的分组(或者称为自动编号分组)。

命名分组的格式为:(?<name>X),其中X表示一个正则表达式

例子:下面的正则表达式表示已数字开头,中间是字母,以数字结尾的字符串。名为letter的捕获组捕获符合该正则表达式中间为字母的部分。文本串中捕获的内容用红色表示。

 

正则表达式:“\d+(?<letter>[a-zA-Z]+)\d+”

文本串内容:“0123ab456def gisd4ZDG6zz”

名为letter的捕获组中的内容为:“0123ab456def gisd4ZDG6zz”

 

对于未命名分组,每一对括号实际上都是一对分组,正则表达式引擎会在编译该表达式的时候会从左到右扫描正则表达式,对未命名分组进行编号。遇到的第1个左括号(和相应匹配的右括号)是第1组,遇到的第2个左括号(和相应匹配的右括号)是第2组,……。第0组的内容匹配的是整个正则表达式。实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给自定义命名组分配,也就是说自定义命名分组也是有编号的,且所有自定义命名组的组号都大于未命名的组号。

2.9 特殊字符的匹配

对于一些在正则表达式中具有特殊含义的特殊字符,比如“{”,“*”“\”等等,如果我们想在文本串中捕获它们,就只能通过转义的方式。比如我们想匹配文本串中以“{”花括号开头,花括号结尾“}”,中间有任意数量其它字符,且其它任意字符尽可能少。我们的正则表达式就可以写成“\{.*?\}”,它可以匹配以下字符串中的“abcde{fghi{jklmn}op}xyz”。

正则表达式:“\\.*?\\” 表示已“\”开头和“\”结尾中间为任意数量且尽可能少的其它字符。它可以匹配以下字符串中的“abcde\fghi\jklmn\op\xyz”

2.10 零宽断言

在某些特殊的情况下,如果我们只是想要匹配某个字符有(或者没有)出现,但并不想去捕获它的时候,我们就需要零宽度断言。零宽度断言和\b等零宽度字符一样,都是匹配一个位置,并不消耗字符,但零宽度断言可以是由表达式构成,功能也就更加强大。零宽度断言分为四种情况。

 

零宽度正预测断言

“预测”表示向匹配内容的后方看,“正”表示匹配的意思

一般格式:“exp1(?=exp2)”

含义:匹配文本串中符合正则表达式exp1的内容,且文本串中已匹配exp1的字符串的后面必须匹配exp2,但不消耗文本串中匹配exp2的字符,且结果中不捕获exp2匹配的内容。不消耗匹配exp2的字符的意思是,下一次搜索从文本串中匹配exp1的后面开始,而不是从匹配exp2的后面开始。注意,exp2右括号后面一般不能再跟正则表达式否则,不会匹配到任何东西。

例子:下面的正则表达式会匹配一个单词,且这个单词必须以ing结尾。文本串中捕获的内容用红色标示,绿色表示正预言的匹配。

 

正则表达式:"\b\w+(?=ing\b)"

文本串内容:"i am singing while you are dancing"

 

注意,正则表达式不能写成"\b\w+(?=ing)\b",这样不会匹配任何字符串,因为不存在任何一个字符串后面是ing,同时又要求ing是结束的边界(由于不消耗文本串中的ing)。

同理,"\b\w+(?=ing)ing" 等价于 "\b\w+ing"

 

零宽度正回顾断言

“回顾”表示向匹配内容的前方看,“正”表示匹配的意思

一般格式:“(?<=exp2)exp1

含义:匹配文本串中符合正则表达式exp1的内容,且文本串中已匹配exp1的内容前面必须匹配exp2,但在结果中不捕获exp2的匹配内容。注意,exp2左括号前面不能再跟正则表达式否则,不会匹配到任何东西。

例子:下面的正则表达式会匹配一个单词,且这个单词必须以anti开头。文本串中捕获的内容用红色标示,绿色表示正回顾的匹配。

 

正则表达式:"(?<=\banti)\w+\b"

文本串内容:"this is an antibody, not an antivirus"

 

注意,正则表达式不能写成"\b(?<=anti)\w+\b",原因和正预测是一样的,因为“\b”和“anti”是互斥的,也就是说没有一个字符同时满足即使“\b”又是“anti”。

 

零宽度负预测断言

“预测”表示向匹配内容的后方看,“负”表示不匹配的意思

一般格式:“exp1(?!exp2)”

含义:匹配文本串中符合正则表达式exp1的内容,且匹配exp1的内容后面不能匹配exp2。和正预测不同,我们一般可以构成如下的正则表达式exp1(?!exp2)exp3,只要exp2和exp3不相同就不会构成互斥。

例子:下面的正则表达式会匹配一个单词,且这个单词不能以ing结尾。文本串中捕获的内容用红色标示。

例子:下面的正则表达式会匹配一个单词,且这个单词不能以anti开头。文本串中捕获的内容用红色标示。

 

正则表达式:"\\b(?!anti)\\w+\\b"

文本串:"this is an antibody, not an antivirus"

 

零宽度负回顾断言

“回顾”表示向匹配内容的前方看,“负”表示不匹配的意思

一般格式:“(?<!exp2)exp1

含义:捕获文本串中符合正则表达式exp1的内容,且捕获的内容前面不能匹配exp2。和正回顾不同,我们一般可以构成如下的正则表达式“exp3(?<!exp2)exp1”,只要exp2和exp3不相同就会构成互斥。

例子:下面的正则表达式会匹配一个单词,且这个单词不能以ing结尾。文本串中捕获的内容用红色标示。

 

正则表达式:"\\b\\w+(?<!ing)\\b";

文本串:"i am singing , you are danceing";

 

3. JAVA中正则表达式引擎的使用

对同一个正则表达式,从键盘上输入的形式和程序中由字符串表示的正则表达式的形式是不同的。比如我们最开始举例时使用的正则表达式 \d{4}-\d{2}-\d{2} ,在JAVA中用字符串表示的形式如下

String reg = “\\d{4}-\\d{2}-\\{2}”

因为在字符串中,需要用两个“\\”表示一个“\”

JAVA中使用正则表达式主要涉及到两个类,一个是Pattern类,另一个是Matcher类,他们都位于java.util.regex包中。Pattern主要的功能就是将正则表达式转换成NFA(不确定有限自动状态机)或者DFA(确定有限自动状态机)。Matcher的作用是通过Pattern产生的FNA或DFA对文本串进行匹配。

Pattern类的构造函数:

public static Pattern compile(String regex)

public static Pattern compile(String regex, int flags)

第二个构造函数中的flag,可以是下列属性值的组合,它们会影响匹配结果。

static int

CANON_EQ

Enables canonical equivalence.

static int

CASE_INSENSITIVE

大小写不敏感

static int

COMMENTS

允许正则表达式中出现注释

默认情况下正则表达式中不允许出现正则表达式规定的注释

static int

DOTALL

“.”可以匹配任何字符

默认情况下不能匹配 “\r\n”和“\n”

static int

LITERAL

该模式下将转义字符就表示字符本身

“\\d”就表示一个“\”和“d”而不表示数字的字符集

static int

MULTILINE

多行模式:^表示字符串中每一行的开始,$表示字符串中每一行的结束

默认情况下:^表示字符串的开始,$表示字符串的结束

static int

UNICODE_CASE

Enables Unicode-aware case folding.

static int

UNICODE_CHARACTER_CLASS

使用该选项使得原先已定义好的字符集兼容Unicode编码

static int

UNIX_LINES

类Unix下的换行:“\n”

默认情况下使用Windows下的换行,即:“\r\n”或者 “\n”

Pattern类下还有一个matcher方法,我们通过该matcher方法产生Matcher对象,该方法参数表示待匹配的文本串。

public Matcher matcher(CharSequence input)

Matcher下的find方法用于对文本串的匹配,如果发现匹配就返回真,当再次调用find函数时,会从上次已匹配的位置继续搜索。

Matcher下的group方法用于返回匹配的字符串,start和end方法用于返回匹配的字符串在文本串中的开始和结束位置,注意不包含结束位置。

package javalearning;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegularExpTest {
	public static void main(String[] args){
		
		String reg = "[a-z]((?<digtal>\\d+)(b|c)d)[A-Z]";
		String txt = "zdfasdfre1234bdXrt";
		
		Pattern p = Pattern.compile(reg);
		Matcher m = p.matcher(txt);
		
		while(m.find()){
			System.out.println("--------匹配结果----------");
			System.out.printf("[%2d, %2d) : %s\n", m.start(), m.end(), m.group());
			System.out.println("--------自动命名组匹配结果--------");
			for(int i = 0; i < m.groupCount(); i++){
				System.out.printf("group %2d  [%2d, %2d) : %s\n",i, m.start(i), m.end(i), m.group(i));
			}
			System.out.println("--------自定义命名组匹配结果--------");
			System.out.printf("digtal    [%2d, %2d) : %s\n", m.start("digtal"), m.end("digtal"), m.group("digtal"));
			System.out.println();
			System.out.println();
			System.out.println();
		}
	}
}

运行结果

--------匹配结果----------

[ 8, 16) : e1234bdX

--------自动命名组匹配结果--------

group 0 [ 8, 16) : e1234bdX

group 1 [ 9, 15) : 1234bd

group 2 [ 9, 13) : 1234

--------自定义命名组匹配结果--------

digtal [ 9, 13) : 1234

4. 参考内容

[1] 30分钟学会正则表达式

[2] 正则表达式在线测试工具

[3] 正则表达式在线测试工具

posted @ 2017-06-28 23:23  nullzx  阅读(5098)  评论(6编辑  收藏  举报