1. 程式人生 > >計算機程序的思維邏輯 (88) - 正則表達式 (上)

計算機程序的思維邏輯 (88) - 正則表達式 (上)

區分大小寫 對象 java編程 工具 含義 col case tle 有一種

?上節我們提到了正則表達式,它提升了文本處理的表達能力,本節就來討論正則表達式,它是什麽?有什麽用?各種特殊字符都是什麽含義?如何用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。

回溯引用

在正則表達式中,可以使用斜杠\加分組編號引用之前匹配的分組,這稱之為回溯引用,比如:

<(\w+)>(.*)</\1>

\1匹配之前的第一個分組(\w+),這個表達式可以匹配類似如下字符串:

<title>bc</title>

這裏,第一個分組是"title"。

命名分組

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

<(?<tag>\w+)>(.*)</\k<tag>>

非捕獲分組

默認分組都稱之為捕獲分組,即分組匹配的內容被捕獲了,可以在後續被引用,實現捕獲分組有一定的成本,為了提高性能,如果分組後續不需要被引用,可以改為非捕獲分組,語法是(?:...),比如:

(?:abc|def)

特殊邊界匹配

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

邊界 ^

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

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

邊界 $

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

多行匹配模式

以上^和$的含義是默認模式下的,可以指定另外一種匹配模式,多行匹配模式,在此模式下,會以行為單位進行匹配,^匹配的是行開始,$匹配的是行結束,比如表達式是^abc$,字符串是"abc\nabc\r\n",就會有兩個匹配。

可以有兩種方式指定匹配模式,一種是在正則表達式中,以(?m)開頭,m表示multiline,即多行匹配模式,上面的正則表達式可以寫為:

(?m)^abc$

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

需要說明的是,多行模式和之前介紹的單行模式容易混淆,其實,它們之間沒有關系,單行模式影響的是字符‘.‘的匹配規則,使得‘.‘可以匹配換行符,多行模式影響的是^和$的匹配規則,使得它們可以匹配行的開始和結束,兩個模式可以一起使用。

邊界 \A

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

邊界 \Z和\z

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

單詞邊界 \b

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

到底什麽是邊界匹配?

邊界匹配可能難以理解,我們強調下,到底什麽是邊界匹配。邊界匹配不同於字符匹配,可以認為,在一個字符串中,每個字符的兩邊都是邊界,而上面介紹的這些特殊字符,匹配的都不是字符,而是特定的邊界,看個例子:

技術分享

上面的字符串是"a cat\n",我們用粗線顯示出了每個字符兩邊的邊界,並且顯示出了每個邊界與哪些邊界元字符匹配。

環視邊界匹配

定義

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

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

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

這些環視結構也被稱為斷言,斷言的對象是邊界,邊界不占用字符,沒有寬度,所以也被稱為零寬度斷言。

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

關於否定順序環視,我們要避免與排除型字符組混淆,即區分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"。

並行環視

環視匹配的是一個邊界,裏面的表達式是對這個邊界左邊或右邊字符串的要求,對同一個邊界,可以指定多個要求,即寫多個環視,比如表達式:

(?=.*[A-Z])(?=.*[0-9])\w+

\w+的左邊界有兩個要求,(?=.*[A-Z])要求後面至少有一個大寫字母,(?=.*[0-9])要求後面至少有一位數字。

轉義與匹配模式

轉義

我們知道,字符‘\‘表示轉義,轉義有兩種:

  • 把普通字符轉義,使其具備特殊含義,比如‘\t‘, ‘\n‘, ‘\d‘, ‘\w‘, ‘\b‘, ‘\A‘等,也就是說,這個轉義把普通字符變為了元字符;
  • 把元字符轉義,使其變為普通字符,比如‘\.‘, ‘\*‘, ‘\?‘,‘\(‘, ‘\\‘等。

記住所有的元字符,並在需要的時候進行轉義,這是比較困難的,有一個簡單的辦法,可以將所有元字符看做普通字符,就是在開始處加上\Q,在結束處加上\E,比如:

\Q(.*+)\E

\Q和\E之間的所有字符都會被視為普通字符。

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

匹配模式

前面提到了兩種匹配模式,還有一種常用的匹配模式,就是不區分大小寫的模式,指定方式也有兩種,一種是在正則表達式開頭使用(?i),i為ignore,比如:

(?i)the

既可以匹配the,也可以匹配THE,還可以匹配The。

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

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

語法總結

下面,我們用表格的形式簡要匯總下正則表達式的語法。

技術分享

技術分享

技術分享

技術分享

技術分享

小結

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

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

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

技術分享

計算機程序的思維邏輯 (88) - 正則表達式 (上)