計算機程式的思維邏輯 (88) - 正規表示式 (上)

swiftma發表於2017-06-05

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (88) - 正規表示式 (上)

上節我們提到了正規表示式,它提升了文字處理的表達能力,本節就來討論正規表示式,它是什麼?有什麼用?各種特殊字元都是什麼含義?如何用Java藉助正規表示式處理文字?都有哪些常用正規表示式?由於內容較多,我們分為三節進行探討,本節先簡要探討正規表示式的語法。

正規表示式是一串字元,它描述了一個文字模式,利用它可以方便的處理文字,包括文字的查詢、替換、驗證、切分等。

正規表示式中的字元有兩類,一類是普通字元,就是匹配字元本身,另一類是元字元,這些字元有特殊含義,這些元字元及其特殊含義就構成了正規表示式的語法。

正規表示式有一個比較長的歷史,各種與文字處理有關的工具、編輯器和系統都支援正規表示式,大部分程式語言也都支援正規表示式。雖然都叫正規表示式,但由於歷史原因,不同語言、系統和工具的語法不太一樣,本文主要針對Java語言,其他語言可能有所差別。

下面,我們就來簡要介紹正規表示式的語法,我們先分為以下部分分別介紹:

  • 單個字元
  • 字元組
  • 量詞
  • 分組
  • 特殊邊界匹配
  • 環視邊界匹配

最後針對轉義、匹配模式和各種語法進行總結。

單個字元

大部分的單個字元就是用字元本身表示的,比如字元'0','3','a','馬'等,但有一些單個字元使用多個字元表示,這些字元都以斜槓''開頭,比如:

  • 特殊字元,比如tab字元'\t',換行符'\n',回車符'\r'等;
  • 八進位制表示的字元,以\0開頭,後跟1到3位數字,比如\0141,對應的是ASCII編碼為97的字元,即字元'a';
  • 十六進位制表示的字元,以\x開頭,後跟兩位字元,比如\x6A,對應的是ASCII編碼為106的字元,即字元'j';
  • Unicode編號表示的字元,以\u開頭,後跟四位字元,比如\u9A6C,表示的是中文字元'馬',這隻能表示編號在0xFFFF以下的字元,如果超出0XFFFF,使用\x{...}形式,比如對於字元'?',可以使用\x{1f48e};
  • 斜槓\本身,斜槓\是一個元字元,如果要匹配它自身,使用兩個斜槓表示,即'\\'
  • 元字元本身,除了'',正規表示式中還有很多元字元,比如. * ? +等,要匹配這些元字元自身,需要在前面加轉義字元'',比如'\.'

字元組

任意字元

點號字元'.'是一個元字元,預設模式下,它匹配除了換行符以外的任意字元,比如正規表示式:

a.f
複製程式碼

既匹配字串"abf",也匹配"acf"。

可以指定另外一種匹配模式,一般稱為單行匹配模式或者叫點號匹配模式,在此模式下,'.'匹配任意字元,包括換行符。

可以有兩種方式指定匹配模式,一種是在正規表示式中,以(?s)開頭,s表示single line,即單行匹配模式,比如:

(?s)a.f
複製程式碼

另外一種是在程式中指定,在Java中,對應的模式常量是Pattern.DOTALL,下節我們再介紹Java API。

指定的多個字元之一

在單個字元和任意字元之間,有一個字元組的概念,匹配組中的任意一個字元,用中括號[]表示,比如:

[abcd]
複製程式碼

匹配a, b, c, d中的任意一個字元。

[0123456789]
複製程式碼

匹配任意一個數字字元。

字元區間

為方便表示連續的多個字元,字元組中可以使用連字元'-',比如:

[0-9]
[a-z]
複製程式碼

可以有多個連續空間,可以有其他普通字元,比如:

[0-9a-zA-Z_]
複製程式碼

在字元組中,'-'是一個元字元,如果要匹配它自身,可以使用轉義,即'-',或者把它放在字元組的最前面,比如:

[-0-9]
複製程式碼

排除型字元組

字元組支援排除的概念,在[後緊跟一個字元^,比如:

[^abcd]
複製程式碼

表示匹配除了a, b, c, d以外的任意一個字元。

[^0-9]
複製程式碼

表示匹配一個非數字字元。

排除不是不能匹配,而是匹配一個指定字元組以外的字元,要表達不能匹配的含義,需要使用後文介紹的環視語法。

^只有在字元組的開頭才是元字元,如果不在開頭,就是普通字元,匹配它自身,比如:

[a^b]
複製程式碼

就是匹配字元a, ^或b。

字元組內的元字元

在字元組中,除了^ - [ ] \外,其他在字元組外的元字元不再具備特殊含義,變成了普通字元,比如'.',[.*]就是匹配'.'或者'*'本身。

字元組運算

字元組內可以包含字元組,比如:

[[abc][def]]
複製程式碼

最後的字元組等同於[abcdef],內部多個字元組等同於並集運算。

字元組內還支援交集運算,語法是使用&&,比如:

[a-z&&[^de]]
複製程式碼

匹配的字元是a到z,但不能是d或e。

需要注意的是,其他語言可能不支援字元組運算。

預定義的字元組

有一些特殊的以\開頭的字元,表示一些預定義的字元組,比如:

  • \d:d表示digit,匹配一個數字字元,等同於[0-9] ;
  • \w:w表示word,匹配一個單詞字元,等同於[a-zA-Z_0-9];
  • \s:s表示space,匹配一個空白字元,等同於[ \t\n\x0B\f\r]。

它們都有對應的排除型字元組,用大寫表示,即:

  • \D:匹配一個非數字字元,即[^\d] ;
  • \W:匹配一個非單詞字元,即[^\w];
  • \S:匹配一個非空白字元,即[^\s]。

POSIX字元組

還有一類字元組,稱為POSIX字元組,POSIX是一個標準,POSIX字元組是POSIX標準定義的一些字元組,在Java中,這些字元組的形式是\p{...},比如:

  • \p{Lower}:小寫字母,等同於[a-z];
  • \p{Upper}:大寫字母,等同於[A-Z];
  • \p{Digit}:數字,等同於[0-9];
  • \p{Punct} :標點符號,匹配!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~中的一個。

POSIX字元組比較多,本文就不列舉了。

量詞

常用量詞 + * ?

量詞指的是指定出現次數的元字元,有三個常見的元字元+ * ?:

  • +:表示前面字元的一次或多次出現,比如正規表示式ab+c,既能匹配abc,也能匹配abbc,或abbbc;
  • *:表示前面字元的零次或多次出現,比如正規表示式ab*c,既能匹配abc,也能匹配ac,或abbbc;
  • ?:表示前面字元可能出現,也可能不出現,比如正規表示式ab?c,既能匹配abc,也能匹配ac,但不能匹配abbc。

通用量詞 {m,n}

更為通用的表示出現次數的語法是{m,n},出現次數從m到n,包括m和n,如果n沒有限制,可以省略,如果m和n一樣,可以寫為{m},比如:

  • ab{1,10}c:b可以出現1次到10次
  • ab{3}c:b必須出現三次,即只能匹配abbbc
  • ab{1,}c:與ab+c一樣
  • ab{0,}c:與ab*c一樣
  • ab{0,1}c:與ab?c一樣

需要注意的是,語法必須是嚴格的{m,n}形式,逗號左右不能有空格。

?, *, +, {是元字元,如果要匹配這些字元本身,需要使用''轉義,比如

a\*b
複製程式碼

匹配字串"a*b"。

這些量詞出現在字元組中時,不是元字元,比如表示式

[?*+{]
複製程式碼

就是匹配其中一個字元本身。

貪婪與懶惰

關於量詞,它們的預設匹配是貪婪的,什麼意思呢?看個例子,正規表示式是:

<a>.*</a>
複製程式碼

如果要處理的字串是:

<a>first</a><a>second</a>
複製程式碼

目的是想得到兩個匹配,一個匹配:

<a>first</a>
複製程式碼

另一個匹配:

<a>second</a>
複製程式碼

但預設情況下,得到的結果卻只有一個匹配,匹配所有內容。

這是因為.*可以匹配第一個<a>和最後一個</a>之間的所有字元,只要能匹配,.*就儘量往後匹配,它是貪婪的。如果希望在碰到第一個匹配時就停止呢?應該使用懶惰量詞,在量詞的後面加一個符號'?',針對上例,將表示式改為:

<a>.*?</a>
複製程式碼

就能得到期望的結果。

所有量詞都有對應的懶惰形式,比如:x??, x*?, x+?, x{m,n}?等。

分組

表示式可以用括號()括起來,表示一個分組,比如a(bc)d,bc就是一個分組,分組可以巢狀,比如a(de(fg))。

捕獲分組

分組預設都有一個編號,按照括號的出現順序,從1開始,從左到右依次遞增,比如表示式:

a(bc)((de)(fg))
複製程式碼

字串abcdefg匹配這個表示式,第1個分組為bc,第2個為defg,第3個為de,第4個為fg。分組0是一個特殊分組,內容是整個匹配的字串,這裡是abcdefg。

分組匹配的子字串可以在後續訪問,好像被捕獲了一樣,所以預設分組被稱為捕獲分組。關於如何在Java中訪問和使用捕獲分組,我們下節再介紹。

分組量詞

可以對分組使用量詞,表示分組的出現次數,比如a(bc)+d,表示bc出現一次或多次。

分組多選

中括號[]表示匹配其中的一個字元,括號()和元字元'|'一起,可以表示匹配其中的一個子表示式,比如

(http|ftp|file)
複製程式碼

匹配http或ftp或file。

需要注意區分|和[],|用於[]中不再有特殊含義,比如

[a|b]
```
它的含義不是匹配a或b,而是a或|或b。

### 回溯引用

在正規表示式中,可以使用斜槓\加分組編號引用之前匹配的分組,這稱之為<span style="color:blue">回溯引用</span>,比如:
```
<(\w+)>(.*)</\1>
```
\1匹配之前的第一個分組(\w+),這個表示式可以匹配類似如下字串:
```
<title>bc</title>
```
這裡,第一個分組是"title"### 命名分組

使用數字引用分組,可能容易出現混亂,可以對分組進行命名,通過名字引用之前的分組,對分組命名的語法是`(?<name>X)`,引用分組的語法是`\k<name>`,比如,上面的例子可以寫為:
```
<(?<tag>\w+)>(.*)</\k<tag>>
```
### 非捕獲分組

預設分組都稱之為捕獲分組,即分組匹配的內容被捕獲了,可以在後續被引用,實現捕獲分組有一定的成本,為了提高效能,如果分組後續不需要被引用,可以改為<span style="color:blue">非捕獲分組</span>,語法是(?:...),比如:
```
(?:abc|def)
```
## 特殊邊界匹配

在正規表示式中,除了可以指定字元需滿足什麼條件,還可以指定字元的邊界需滿足什麼條件,或者說匹配特定的邊界,常用的表示特殊邊界的元字元有^, $, \A, \Z, \z和\b。

### 邊界 ^

預設情況下,^匹配整個字串的開始,^abc表示整個字串必須以abc開始。

需要注意的是^的含義,在字元組中它表示排除,但在字元組外,它匹配開始,比如表示式^[^abc],表示以一個不是a,b,c的字元開始。

### 邊界 $

預設情況下,$匹配整個字串的結束,不過,如果整個字串以換行符結束,$匹配的是換行符之前的邊界,比如表示式abc$,表示整個表示式以abc結束,或者以abc\r\n或abc\n結束。

### 多行匹配模式

以上^和$的含義是預設模式下的,可以指定另外一種匹配模式,<span style="color:blue">多行匹配模式</span>,在此模式下,會以行為單位進行匹配,^匹配的是行開始,$匹配的是行結束,比如表示式是^abc$,字串是"abc\nabc\r\n",就會有兩個匹配。

可以有兩種方式指定匹配模式,一種是在正規表示式中,以(?m)開頭,m表示multiline,即多行匹配模式,上面的正規表示式可以寫為:
```
(?m)^abc$
```
另外一種是在程式中指定,在Java中,對應的模式常量是Pattern.MULTILINE,下節我們再介紹Java API。

需要說明的是,多行模式和之前介紹的單行模式容易混淆,其實,它們之間沒有關係,<span style="color:blue">單行模式影響的是字元'.'的匹配規則</span>,使得'.'可以匹配換行符,<span style="color:blue">多行模式影響的是^和$的匹配規則</span>,使得它們可以匹配行的開始和結束,兩個模式可以一起使用。

### 邊界 \A

\A與^類似,但不管什麼模式,它匹配的總是整個字串的開始邊界。

### 邊界 \Z和\z

\Z和\z與$類似,但不管什麼模式,它們匹配的總是整個字串的結束,\Z與\z的區別是,如果字串以換行符結束,\Z與$一樣,匹配的是換行符之前的邊界,而\z匹配的總是結束邊界。在進行輸入驗證的時候,為了確保輸入最後沒有多餘的換行符,可以使用\z進行匹配。

### 單詞邊界 \b

\b匹配的是單詞邊界,比如\bcat\b,匹配的是完整的單詞cat,它不能匹配category,<span style="color:blue">\b匹配的不是一個具體的字元,而是一種邊界,這種邊界滿足一個要求,即一邊是單詞字元,另一邊不是單詞字元</span>。在Java中,\b識別的單詞字元除了\w,還包括中文字元。

## 到底什麼是邊界匹配?

邊界匹配可能難以理解,我們強調下,到底什麼是邊界匹配。<span style="color:red">邊界匹配不同於字元匹配,可以認為,在一個字串中,每個字元的兩邊都是邊界,而上面介紹的這些特殊字元,匹配的都不是字元,而是特定的邊界</span>,看個例子:

![](https://lc-gold-cdn.xitu.io/ffc991471704495a6e9b.jpg)
上面的字串是"a cat\n",我們用粗線顯示出了每個字元兩邊的邊界,並且顯示出了每個邊界與哪些邊界元字元匹配。

## 環視邊界匹配

### 定義

對於邊界匹配,除了使用上面介紹的邊界元字元,還有一種更為通用的方式,那就是環視,<span style="color:red">環視的字面意思就是左右看看,需要左右符合一些條件,本質上,它也是匹配邊界,對邊界有一些要求,這個要求是針對左邊或右邊的字串的</span>,根據要求不同,分為四種環視:

-    <span style="color:blue">肯定順序環視</span>,語法是(?=...),要求右邊的字串匹配指定的表示式,比如表示式abc(?=def),(?=def)在字元c右面,即匹配c右面的邊界,對這個邊界的要求是,它的右邊有def,比如abcdef,如果沒有,比如abcd,則不匹配;
-    <span style="color:blue">否定順序環視</span>,語法是(?!...),要求右邊的字串不能匹配指定的表示式,比如表示式s(?!ing),匹配一般的s,但不匹配後面有ing的s;
-    <span style="color:blue">肯定逆序環視</span>,語法是(?<=...),要求左邊的字串匹配指定的表示式,比如表示式(?<=\s)abc,(?<=\s)在字元a左邊,即匹配a左邊的邊界,對這個邊界的要求是,它的左邊必須是空白字元;
-    <span style="color:blue">否定逆序環視</span>,語法是(?<!...),要求左邊的字串不能匹配指定的表示式,比如表示式(?<!\w)cat,(?<!\w)在字元c左邊,即匹配c左邊的邊界,對這個邊界的要求是,它的左邊不能是單詞字元。

可以看出,環視也使用括號(),不過,它不是分組,不佔用分組編號。

這些環視結構也被稱為<span style="color:blue">斷言,斷言的物件是邊界,邊界不佔用字元,沒有寬度,所以也被稱為零寬度斷言</span>。

### 否定順序環視與排除型字元組

關於否定順序環視,我們要避免與排除型字元組混淆,即區分s(?!ing)與s[^ing],s[^ing]匹配的是兩個字元,第一個是s,第二個是i, n, g以外的任意一個字元。還要注意,寫法s(^ing)是不對的,^匹配的是起始位置。

### 出現在左邊的順序環視

順序環視也可以出現在左邊,比如表示式:
```
(?=.*[A-Z])\w+
```
這個表示式是什麼意思呢?

\w+匹配多個單詞字元,(?=.*[A-Z])匹配單詞字元的左邊界,這是一個肯定順序環視,對這個邊界的要求是,它右邊的字串匹配表示式:
```
.*[A-Z]
```
也就是說,它右邊至少要有一個大寫字母。

### 出現在右邊的逆序環視

逆序環視也可以出現在右邊,比如表示式:
```
[\w.]+(?<!\.)
```
[\w.]+匹配單詞字元和字元'.'構成的字串,比如"hello.ma"。`(?<!\.)`匹配字串的右邊界,這是一個逆序否定環視,對這個邊界的要求是,它左邊的字元不能是'.',也就是說,如果字串以'.'結尾,則匹配的字串中不能包括這個'.',比如,如果字串是"hello.ma.",則匹配的子字串是"hello.ma"### 並行環視

<span style="color:blue">環視匹配的是一個邊界,裡面的表示式是對這個邊界左邊或右邊字串的要求,對同一個邊界,可以指定多個要求</span>,即寫多個環視,比如表示式:
```
(?=.*[A-Z])(?=.*[0-9])\w+
```
\w+的左邊界有兩個要求,`(?=.*[A-Z])`要求後面至少有一個大寫字母,`(?=.*[0-9])`要求後面至少有一位數字。

## 轉義與匹配模式

### 轉義

我們知道,字元'\'表示轉義,轉義有兩種:

-   <span style="color:blue">把普通字元轉義,使其具備特殊含義</span>,比如```'\t', '\n', '\d', '\w', '\b', '\A'```等,也就是說,這個轉義把普通字元變為了元字元;
-   <span style="color:blue">把元字元轉義,使其變為普通字元</span>,比如```'\.', '\*', '\?','\(', '\\'```等。

記住所有的元字元,並在需要的時候進行轉義,這是比較困難的,有一個簡單的辦法,可以將所有元字元看做普通字元,就是在開始處加上\Q,在結束處加上\E,比如:
```
\Q(.*+)\E
```
\Q和\E之間的所有字元都會被視為普通字元。

正規表示式用字串表示,在Java中,字元'\'也是字串語法中的元字元,這使得正規表示式中的'\',在Java字串表示中,要用兩個'\',即```'\\'```,而要匹配字元'\'本身,在Java字串表示中,要用四個'\',即```'\\\\'```,關於這點,下節我們會進一步說明。

### 匹配模式

前面提到了兩種匹配模式,還有一種常用的匹配模式,就是不區分大小寫的模式,指定方式也有兩種,一種是在正規表示式開頭使用(?i),i為ignore,比如:
```
(?i)the
```
既可以匹配the,也可以匹配THE,還可以匹配The。

也可以在程式中指定,Java中對應的變數是Pattern.CASE_INSENSITIVE。

需要說明的是,匹配模式間不是互斥的關係,它們可以一起使用,在正規表示式中,可以指定多個模式,比如(?smi)。

### 語法總結

下面,我們用表格的形式簡要彙總下正規表示式的語法。

![](https://lc-gold-cdn.xitu.io/ee3aa3c3bffb7ddc9bef.jpg)

![](https://lc-gold-cdn.xitu.io/db2ff0d463eb55e03173.jpg)

![](https://lc-gold-cdn.xitu.io/37a735bc02b5dee04f8d.jpg)

![](https://lc-gold-cdn.xitu.io/5ed8f72b41a7c5fd6c91.jpg)

![](https://lc-gold-cdn.xitu.io/512efeb05dc0e8fbb576.jpg)

![](https://lc-gold-cdn.xitu.io/c8869193c2f6acbf5d5f.jpg)

## 小結

本節簡要介紹了正規表示式中的語法,下一節,我們來探討相關的Java API。

----------------

未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),從入門到高階,深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

![](https://lc-gold-cdn.xitu.io/4eaeda72b158f3aadaf5.jpg)

複製程式碼

相關文章