計算機程序的思維邏輯 (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。
回溯引用
在正則表達式中,可以使用斜杠\加分組編號引用之前匹配的分組,這稱之為回溯引用,比如:
<(\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) - 正則表達式 (上)