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

swiftma發表於2019-03-02

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

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

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

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

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

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

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

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

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

單個字元

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

  • 特殊字元,比如tab字元` `,換行符`
    `,回車符`
    `等;
  • 八進位制表示的字元,以 開頭,後跟1到3位數字,比如 141,對應的是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,匹配一個空白字元,等同於[
    x0Bf
    ]。

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

  • 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和。

### 邊界 ^

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

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

### 邊界 $

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

### 多行匹配模式

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

可以有兩種方式指定匹配模式,一種是在正規表示式中,以(?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進行匹配。

### 單詞邊界 

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

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

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

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

## 環視邊界匹配

### 定義

對於邊界匹配,除了使用上面介紹的邊界元字元,還有一種更為通用的方式,那就是環視,<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>,比如````	`, `
`, `d`, `w`, ``, `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)

複製程式碼

相關文章