1. 程式人生 > >1000行程式碼徒手寫正則表示式引擎【1】--JAVA中正則表示式的使用

1000行程式碼徒手寫正則表示式引擎【1】--JAVA中正則表示式的使用

簡介

本文是系列部落格的第一篇,主要講解和分析正則表示式規則以及JAVA中原生正則表示式引擎的使用。在後續的文章中會涉及基於NFA的正則表示式引擎內部的工作原理,並在此基礎上用1000行左右的JAVA程式碼,實現一個支援常用功能的正則表示式引擎。它支援貪婪匹配和懶惰匹配;支援零寬度字元(如“\b”, “\B”);支援常用字符集(如“\d”, “\s”等);支援自定義字符集(“[a-f]”,“[^b-k] ”等);支援所有重複操作(“*”,“+”,“?”,“{n,m}”等);支援萬用字元(“. ”);支援運算子本身的轉義字元(“\*”,“\.”等);支援命名捕獲組。本系列部落格的目的是理解正則表示式的內部工作原理,所以沒有考慮正則表示式引擎的工作效率以及正負斷言和非命名捕獲組等不常用的功能,後續的工作將會對其優化並完備其功能。由於正則表示式引擎的實現需要運用資料結構中的相關內容,閱讀本系列部落格前請熟悉棧,佇列,圖以及JAVA中容器等相關知識。

歡迎探討,如有錯誤敬請指正

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. 參考內容