第一部分
第1章 字符組
1.1 普通字符組
字符組(Character Class)是正則表達(dá)式最基本的結(jié)構(gòu)之一,要理解正則表達(dá)式的“靈活”,認(rèn)識(shí)它是第一步。
顧名思義,字符組就是一組字符,在正則表達(dá)式中,它表示“在同一個(gè)位置可能出現(xiàn)的各種字符”,其寫法是在一對(duì)方括號(hào)[和]之間列出所有可能出現(xiàn)的字符,簡(jiǎn)單的字符組比如[ab]、[314]、[#.?]在解決一些常見(jiàn)問(wèn)題時(shí),使用字符組可以大大簡(jiǎn)化操作,下面舉“匹配數(shù)字字符”的例子來(lái)說(shuō)明。
字符可以分為很多類,比如數(shù)字、字母、標(biāo)點(diǎn)等。有時(shí)候要求 “只出現(xiàn)一個(gè)數(shù)字字符”,換句話說(shuō),這個(gè)位置上的字符只能是0、1、2、…、8、9這10個(gè)字符之一。要進(jìn)行這種判斷,通常的思路是:用10個(gè)條件分別判斷字符是否等于這10個(gè)字符,對(duì)10個(gè)結(jié)果取“或”,只要其中一個(gè)條件成立,就返回True,表示這是一個(gè)數(shù)字字符,其偽代碼如例1-1所示。
例1-1判斷數(shù)字字符的偽代碼
charStr == "0" || charStr == "1" … || charStr == "9"
注:因?yàn)檎齽t表達(dá)式處理的都是“字符串”(String)而不是“字符”,所以這里假設(shè)變量charStr(雖然它只包含一個(gè)字符)也是字符串類型,使用了雙引號(hào),在有些語(yǔ)言中字符串也用單引號(hào)表示。
這種解法的問(wèn)題在于太煩瑣——如果要判斷是否是一個(gè)小寫英文字母,就要用||連接26個(gè)判斷;如果還要兼容大寫字母,則要連接52 個(gè)判斷,代碼長(zhǎng)到幾乎無(wú)法閱讀。相反,用字符組解決起來(lái)卻異常簡(jiǎn)單,具體思路是:列出可能出現(xiàn)的所有字符(在這個(gè)例子里就是10個(gè)數(shù)字字符),只要出現(xiàn)了其中任何一個(gè),就返回True。例1-2 給出了使用字符組判斷的例子,程序語(yǔ)言使用Python。
例1-2用正則表達(dá)式判斷數(shù)字字符
re.search("[0123456789]", charStr) != None
re.search()是Python提供的正則表達(dá)式操作函數(shù),表示“進(jìn)行正則表達(dá)式匹配”;charStr仍然是需要判斷的字符串,而[0123456789]則是以字符串形式給出的正則表達(dá)式,它是一個(gè)字符組,表示“這里可以是0、1、2、…、8、9中的任意一個(gè)字符。只要charStr與其中任何一個(gè)字符相同(或者說(shuō)“charStr可以由[0123456789]匹配”),就會(huì)得到一個(gè)MatchObject對(duì)象(這個(gè)對(duì)象暫時(shí)不必關(guān)心,在第21頁(yè)會(huì)詳細(xì)講解);否則,返回None。所以判斷結(jié)果是否為None,就可以判斷charStr是否是數(shù)字字符。
當(dāng)今流行的編程語(yǔ)言大多支持正則表達(dá)式,上面的例子在各種語(yǔ)言中的寫法大抵相同,唯一的區(qū)別在于如何調(diào)用正則表達(dá)式的功能,所以用法其實(shí)大同小異。例1-3列出了常見(jiàn)語(yǔ)言中的表示,如果你現(xiàn)在就希望知道語(yǔ)言的細(xì)節(jié),可以參考本書第三部分的具體章節(jié)。
例1-3用正則表達(dá)式判斷數(shù)字字符在各種語(yǔ)言中的應(yīng)用
.NET(C#) //能匹配則返回true,否則返回false Regex.IsMatch(charStr, "[0123456789]"); Java //能匹配則返回true,否則返回false charStr.matches("[0123456789]"); JavaScript //能匹配則返回true,否則返回false /[0123456789]/.test(charStr); PHP //能匹配則返回1,否則返回0 preg_match("/[0123456789]/", charStr); Python #能匹配則返回RegexObject,否則返回None re.search("[0123456789]", charStr) Ruby #能匹配則返回0,否則返回nil charStr =~ /[0123456789]/
可以看到,不同語(yǔ)言使用正則表達(dá)式的方法也不相同。如果仔細(xì)觀察會(huì)發(fā)現(xiàn)Java、.NET、Python、PHP中的正則表達(dá)式,都要以字符串形式給出,兩端都有雙引號(hào)";而Ruby和JavaScript中的正則表達(dá)式則不必如此,只在首尾有兩個(gè)斜線字符/,這也是不同語(yǔ)言中使用正則表達(dá)式的不同之處。不過(guò),這個(gè)問(wèn)題現(xiàn)在不需要太關(guān)心,因?yàn)楸緯写蟛糠掷右訮ython程序來(lái)講解,下面講解關(guān)于Python的基礎(chǔ)知識(shí),其他語(yǔ)言的細(xì)節(jié)留到后文會(huì)詳細(xì)介紹。
1.2 關(guān)于Python的基礎(chǔ)知識(shí)
本書選擇使用Python語(yǔ)言來(lái)演示實(shí)際的匹配結(jié)果,因?yàn)樗茉诙喾N操作系統(tǒng)中運(yùn)行,安裝也很方便;另一方面,Python是解釋型語(yǔ)言,輸入代碼就能看到結(jié)果,方便動(dòng)手實(shí)踐。考慮到不是所有人都熟悉Python,這里專門用一節(jié)來(lái)介紹。
如果你的機(jī)器上沒(méi)有安裝Python,可以從http://python.org/download/下載,目前Python有2和3兩個(gè)版本,本書的例子以2版本為準(zhǔn)。請(qǐng)選擇自己平臺(tái)對(duì)應(yīng)的程序下載并安裝(目前MacOS、Linux的各種發(fā)行版一般帶有Python,具體可以在命令行下輸入python,看是否啟動(dòng)對(duì)應(yīng)的程序)。
然后可以啟動(dòng)Python,在MacOS和Linux下是輸入python,會(huì)顯示出Python提示符,進(jìn)入交互模式,如圖1-1(Linux下的提示符與MacOS下的差不多,所以此處不列出);而在Windows下,需要在“開始”菜單的“程序”中,選擇Python目錄下的Python(command line),如圖1-2所示。

圖1-1 MacOS下的Python提示符

圖1-2 Windows下的Python提示符
Python中常用的關(guān)于正則表達(dá)式的函數(shù)是re.search(),使用它必須首先導(dǎo)入正則表達(dá)式對(duì)應(yīng)的包(package),也就是輸入下面的代碼。
#導(dǎo)入正則表達(dá)式對(duì)應(yīng)的包 import re
通常的用法是提供兩個(gè)參數(shù):re.search(pattern, string),其中pattern是字符串形式提供的正則表達(dá)式,string 是需要匹配的字符串;如果能匹配,則返回一個(gè)MatchObject(詳細(xì)介紹請(qǐng)參考第241 頁(yè),暫時(shí)可以不必關(guān)心),這時(shí)提示符會(huì)顯示類似<_sre.SRE_Match object at 0x0000000001D8E578>之類的結(jié)果;如果不能匹配,結(jié)果是None(這是Python中的一個(gè)特殊值,類似其他某些語(yǔ)言中的Null),不會(huì)有任何顯示。圖1-3演示了運(yùn)行Python語(yǔ)句的結(jié)果。

圖1-3 觀察re.search()匹配的返回值
注:>>>是等待輸入的提示符,以>>>開頭的行,之后文本是用戶輸入的語(yǔ)句;其他行是系統(tǒng)生成的,比如打印出語(yǔ)句的結(jié)果(在交互模式下,匹配結(jié)果會(huì)自動(dòng)輸出,便于觀察;真正程序運(yùn)行時(shí)不會(huì)如此)。
為講解清楚、形象、方便,本書中的程序部分需要做兩點(diǎn)修改。
第一,因?yàn)闀簳r(shí)還不需要關(guān)心匹配結(jié)果的細(xì)節(jié),只關(guān)心有沒(méi)有結(jié)果,所以在re.search()之后添加判斷返回值是否為None,如果為True,則表示匹配成功,否則返回False表示匹配失敗。為節(jié)省版面,盡可能用注釋表示這類匹配結(jié)果,如# => True或者 # => False,附在語(yǔ)句之后。
第二,目前我們關(guān)心的是整個(gè)字符串是否能由正則表達(dá)式匹配。但是,在默認(rèn)情況下re.search(pattern,string)只判斷string的某個(gè)子串能否由pattern匹配,即便pattern只能匹配string的一部分,也不會(huì)返回None。為了測(cè)試整個(gè)string能否由pattern匹配,在pattern兩端加上^和$。^和$是正則表達(dá)式中的特殊字符,它們并不匹配任何字符,只是表示“定位到字符串的起始位置”和“定位到字符串的結(jié)束位置”(原理如圖1-4 所示,如果你現(xiàn)在就希望詳細(xì)了解這兩個(gè)特殊字符,可以參考第61頁(yè)),這樣就保證;只有在整個(gè)string都可以由pattern匹配時(shí),才算匹配成功,不返回None,如例1-4所示。

圖1-4 ^[0123456789]$的匹配
例1-4使用^和$測(cè)試string由pattern完整匹配
# 只要字符串中包含數(shù)字字符,就可以匹配 re.search("[0123456789]", "2") != None # => True re.search("^[0123456789]$", "12") != None # => False re.search("[0123456789]", "a2") != None # => True # 整個(gè)字符串就是一個(gè)數(shù)字字符,才可以匹配 re.search("[0123456789]", "2") != None # => True re.search("^[0123456789]$", "12") != None # => False re.search("^[0123456789]$", "a2") != None # => False
1.3 普通字符組(續(xù))
介紹完關(guān)于Python的基礎(chǔ)知識(shí),繼續(xù)講解字符組。字符組中的字符排列順序并不影響字符組的功能,出現(xiàn)重復(fù)字符也不會(huì)影響,所以[0123456789]完全等價(jià)于 [9876543210]、[1029384756]、[9988876543210]。
不過(guò),代碼總是要容易編寫,方便閱讀,正則表達(dá)式也是一樣,所以一般并不推薦在字符組中出現(xiàn)重復(fù)字符。而且,還應(yīng)該讓字符組中的字符排列更符合認(rèn)知習(xí)慣,比如[0123456789]就好過(guò)[0192837465]。為此,正則表達(dá)式提供了-范圍表示法(range),它更直觀,能進(jìn)一步簡(jiǎn)化字符組。
所謂“-范圍表示法”,就是用[x-y]的形式表示x到y整個(gè)范圍內(nèi)的字符,省去一一列出的麻煩,這樣[0123456789]就可以表示為[0-9]。如果你覺(jué)得這不算什么,那么確實(shí)比[abcdefghijklmnopqrstuvwxyz]簡(jiǎn)單太多了。
你可能會(huì)問(wèn),“-范圍表示法”的范圍是如何確定的?為什么要寫作[0-9],而不寫作[9-0]?
要回答這個(gè)問(wèn)題,必須了解范圍表示法的實(shí)質(zhì)。在字符組中,-表示的范圍,一般是根據(jù)字符對(duì)應(yīng)的碼值(Code Point,也就是字符在對(duì)應(yīng)編碼表中的編碼的數(shù)值)來(lái)確定的,碼值小的字符在前,碼值大的字符在后。在ASCII編碼中(包括各種兼容ASCII的編碼中),字符0的碼值是48(十進(jìn)制),字符9的碼值是57(十進(jìn)制),所以[0-9]等價(jià)于[0123456789];而[9-0]則是錯(cuò)誤的范圍,因?yàn)?span id="evhob7z" class="bgcolor_105">9的碼值大于0,所以會(huì)報(bào)錯(cuò)。程序代碼見(jiàn)例1-5。
例1-5 [0-9]是合法的,[9-0]會(huì)報(bào)錯(cuò)
re.search("^[0-9]$", "2") != None # => True re.search("^[9-0]$", "2") != None Traceback (most recent call last): error: bad character range
如果知道0~9的碼值是48~57,a~z的碼值是97~122,A~Z的碼值是65~90,能不能用[0-z]統(tǒng)一表示數(shù)字字符、小寫字母、大寫字母呢?
答案是:勉強(qiáng)可以,但不推薦這么做。根據(jù)慣例,字符組的范圍表示法都表示一類字符(數(shù)字字符是一類,字母字符也是一類),所以雖然[0-9]、[a-z]都是很好理解的,但[0-z]卻很難理解,不熟悉ASCII編碼表的人甚至不知道這個(gè)字符組還能匹配大寫字母,更何況,在碼值48到122之間,除去數(shù)字字符(碼值48~57)、小寫字母(碼值97~122)、大寫字母(碼值65~90),還有不少標(biāo)點(diǎn)符號(hào)(參見(jiàn)表1-1),從字符組[0-z]中卻很難看出來(lái),使用時(shí)就容易引起誤會(huì),例1-6所示的程序就很可能讓人莫名其妙。
表1-1 ASCII編碼表(片段)

例1-6 [0-z]的奇怪匹配
re.search("^[0-z]$", "A") != None # => True re.search("^[0-z]$", ":") != None # => True
在字符組中可以同時(shí)并列多個(gè)“-范圍表示法”,字符組[0-9a-zA-Z]可以匹配數(shù)字、大寫字母或小寫字母;字符組[0-9a-fA-F]可以匹配數(shù)字,大、小寫形式的a~f,它可以用來(lái)驗(yàn)證十六進(jìn)制字符,代碼見(jiàn)例1-7。
例1-7 [0-9a-fA-F]準(zhǔn)確判斷十六進(jìn)制字符
re.search("^[0-9a-fA-F]$", "0") != None # => True re.search("^[0-9a-fA-F]$", "c") != None # => True re.search("^[0-9a-fA-F]$", "i") != None # => False re.search("^[0-9a-fA-F]$", "C") != None # => True re.search("^[0-9a-fA-F]$", "G") != None # => False
在不少語(yǔ)言中,還可以用轉(zhuǎn)義序列\(zhòng)xhex來(lái)表示一個(gè)字符,其中\(zhòng)x是固定前綴,表示轉(zhuǎn)義序列的開頭,num是字符對(duì)應(yīng)的碼值(Code Point,詳見(jiàn)第127頁(yè),下文用?127表示),是一個(gè)兩位的十六進(jìn)制數(shù)值。比如字符A的碼值是41(十進(jìn)制則為65),所以也可以用\x41表示。
字符組中有時(shí)會(huì)出現(xiàn)這種表示法,它可以表現(xiàn)一些難以輸入或者難以顯示的字符,比如\x7F;也可以用來(lái)方便地表示某個(gè)范圍,比如所有ASCII字符對(duì)應(yīng)的字符組就是[\x00-\x7F],代碼見(jiàn)例1-8。這種表示法很重要,在第120頁(yè)還會(huì)講到它,依靠這種表示法可以很方便地匹配所有的中文字符。
例1-8 [\x00-\x7F]準(zhǔn)確判斷ASCII字符
re.search("^[\x00-\x7F]$", "c") != None # => True re.search("^[\x00-\x7F]$", "I") != None # => True re.search("^[\x00-\x7F]$", "0") != None # => True re.search("^[\x00-\x7F]$", "<") != None # => True
1.4 元字符與轉(zhuǎn)義
在上面的例子里,字符組中的橫線-并不能匹配橫線字符,而是用來(lái)表示范圍,這類字符叫做元字符(meta-character)。字符組的開方括號(hào)[、閉方括號(hào)]和之前出現(xiàn)的^、$都算元字符。在匹配中,它們有著特殊的意義。但是,有時(shí)候并不需要表示這些特殊意義,只需要表示普通字符(比如“我就想表示橫線字符-”),此時(shí)就必須做特殊處理。
先來(lái)看字符組中的-,如果它緊鄰著字符組中的開方括號(hào)[,那么它就是普通字符,其他情況下都是元字符;而對(duì)于其他元字符,取消特殊含義的做法都是轉(zhuǎn)義,也就是在正則表達(dá)式中的元字符之前加上反斜線字符\。
如果要在字符組內(nèi)部使用橫線-,最好的辦法是將它排列在字符組的最開頭。[-09]就是包含三個(gè)字符-、0、9的字符組;[0-9]是包含0~9這10個(gè)字符的字符組,[-0-9]則是由“-范圍表示法”0-9和橫線-共同組成的字符組,它可以匹配11個(gè)字符,例1-9說(shuō)明了使用橫線-的各種情況。
例1-9 –出現(xiàn)在不同位置,含義不同
#作為普通字符 re.search("^[-09]$", "3") != None # => False re.search("^[-09]$", "-") != None # => True #作為元字符 re.search("^[0-9]$", "3") != None # => True re.search("^[0-9]$", "-") != None # => False #轉(zhuǎn)義之后作為普通字符 re.search("^[0\\-9]$", "3") != None # => False re.search("^[0\\-9]$", "-") != None # => True
仔細(xì)觀察會(huì)發(fā)現(xiàn),在正文里說(shuō)“在正則表達(dá)式中的元字符之前加上反斜線字符\”,而在代碼里寫的卻不是[0\-9],而是[0\\-9]。這并不是輸入錯(cuò)誤。
因?yàn)樵谶@段程序里,正則表達(dá)式是以字符串(String)的方式提供的,而字符串本身也有關(guān)于轉(zhuǎn)義的規(guī)定(你或許記得,在字符串中有\n、\t之類的轉(zhuǎn)義序列)。上面說(shuō)的“正則表達(dá)式”,其實(shí)是經(jīng)過(guò)“字符串轉(zhuǎn)義處理”之后的字符串的值,正則表達(dá)式[0\-9]包含6個(gè)字符:[、0、\、-、9、],在字符串中表達(dá)這6個(gè)字符;但是在源代碼里,必須使用7個(gè)字符: \需要轉(zhuǎn)義成\\,因?yàn)樘幚碜址畷r(shí),反斜線和之后的字符會(huì)被認(rèn)為是轉(zhuǎn)義序列(Escape Sequence),比如\n、\t都是合法的轉(zhuǎn)義序列,然而\-不是。
這個(gè)問(wèn)題確實(shí)有點(diǎn)麻煩。正則表達(dá)式是用來(lái)處理字符串的,但它又不完全等于字符串,正則表達(dá)式中的每個(gè)反斜線字符\,在字符串中(也就是正則表達(dá)式之外)還必須轉(zhuǎn)義為\\。所以之前所說(shuō)的是“正則表達(dá)式[0\-9]”,程序里寫的卻是[0\\-9],這確實(shí)有點(diǎn)麻煩。
不過(guò),Python提供了原生字符串(Raw String),它非常適合于正則表達(dá)式:正則表達(dá)式是怎樣,原生字符串就是怎樣,完全不需要考慮正則表達(dá)式之外的轉(zhuǎn)義(只有雙引號(hào)字符是例外,原生字符串內(nèi)的雙引號(hào)字符必須轉(zhuǎn)義寫成\")。原生字符串的形式是r"string",也就是在普通字符串之前添加r,示例代碼如例1-10。
例1-10原生字符串的使用
#原生字符串和字符串的等價(jià) r"^[0\-9]$" == "^[0\\-9]$" # => True #原生字符串的轉(zhuǎn)義要簡(jiǎn)單許多 re.search(r"^[0\-9]$", "3") != None # => False re.search(r"^[0\-9]$", "-") != None # => True
原生字符串清晰易懂,省去了煩瑣的轉(zhuǎn)義,所以從現(xiàn)在開始,本書中的Python示范代碼都會(huì)使用原生字符串來(lái)表示正則表達(dá)式。另外,.NET和Ruby中也有原生字符串,也有一些語(yǔ)言并沒(méi)有提供原生字符串(比如Java),所以在第6章(?93)會(huì)專門講解轉(zhuǎn)義問(wèn)題。不過(guò),現(xiàn)在只需要知道Python示范代碼中使用了原生字符串即可。
繼續(xù)看轉(zhuǎn)義,如果希望在字符組中列出閉方括號(hào)],比如[012]345],就必須在它之前使用反斜線轉(zhuǎn)義,寫成[012\]345];否則,結(jié)果就如例1-11所示,正則表達(dá)式將]與最近的[匹配,這個(gè)表達(dá)式就成了“字符組[012]加上4 個(gè)字符 345]”,它能匹配的是字符串 0345]或 1345]或2345],卻不能匹配]。
例1-11 ]出現(xiàn)在不同位置,含義不同
#未轉(zhuǎn)義的] re.search(r"^[012]345]$", "2345") != None # => True re.search(r"^[012]345]$", "5") != None # => False re.search(r"^[012]345]$", "]") != None # => False #轉(zhuǎn)義的] re.search(r"^[012\]345]$", "2345") != None # => False re.search(r"^[012\]345]$", "5") != None # => True re.search(r"^[012\]345]$", "]") != None # => True
除去字符組內(nèi)部的-,其他元字符的轉(zhuǎn)義都必須在字符之前添加反斜線,[的轉(zhuǎn)義也是如此。如果只希望匹配字符串[012],直接使用正則表達(dá)式[012]是不行的,因?yàn)檫@會(huì)被識(shí)別為一個(gè)字符組,它只能匹配0、1、2這三個(gè)字符中的任意一個(gè);而必須轉(zhuǎn)義,把正則表達(dá)式寫作\[012],請(qǐng)注意,只有開方括號(hào)[需要轉(zhuǎn)義,閉方括號(hào)]不需要轉(zhuǎn)義,如例1-12所示。
例1-12取消其他元字符的特殊含義
re.search(r"^[012]345]$", "3") != None # => False re.search(r"^[012\\]345]$", "3") != None # => True re.search(r"^[012]$", "[012]") != None # => False re.search(r"^\[012]$", "[012]") != None # => True
1.5 排除型字符組
在方括號(hào)[…]中列出希望匹配的所有字符,這種字符組叫做“普通字符組”,它的確非常方便。不過(guò),也有些問(wèn)題是普通字符組不能解決的。
給定一個(gè)由兩個(gè)字符構(gòu)成的字符串 str,要判斷這兩個(gè)字符是否都是數(shù)字字符,可以用[0-9][0-9]來(lái)匹配。但是,如果要求判斷的是這樣的字符串——第一個(gè)字符不是數(shù)字字符,第二個(gè)字符才是數(shù)字字符(比如A8、x6)——應(yīng)當(dāng)如何辦?數(shù)字字符的匹配很好處理,用[0-9]即可;“不是數(shù)字”則很難辦——不是數(shù)字的字符太多了,全部列出幾乎不可能,這時(shí)就應(yīng)當(dāng)使用排除型字符組。
排除型字符組(Negated Character Class)非常類似普通字符組[…],只是在開方括號(hào)[之后緊跟一個(gè)脫字符^,寫作[^…],表示“在當(dāng)前位置,匹配一個(gè)沒(méi)有列出的字符”。所以[^0-9]就表示“0~9 之外的字符”,也就是“非數(shù)字字符”。那么,[^0-9][0-9]就可以解決問(wèn)題了,如例1-13所示。
例1-13使用排除型字符組
re.search(r"^[^0-9][0-9]$", "A8") != None # => True re.search(r"^[^0-9][0-9]$", "x6") != None # => True
排除型字符組看起來(lái)很簡(jiǎn)單,不過(guò)新手常常會(huì)犯一個(gè)錯(cuò)誤,就是把“在當(dāng)前位置,匹配一個(gè)沒(méi)有列出的字符”理解成“在當(dāng)前位置不要匹配列出的字符”兩者其實(shí)是不同的,后者暗示“這里不出現(xiàn)任何字符也可以”。例1-14很清楚地說(shuō)明:排除型字符組必須匹配一個(gè)字符,這點(diǎn)一定要記住。
例1-14排除型字符組必須匹配一個(gè)字符
re.search(r"^[^0-9][0-9]$", "8") != None # => False re.search(r"^[^0-9][0-9]$", "A8") != None # => True
除了開方括號(hào)[之后的^,排除型字符組的用法與普通字符組幾乎完全相同,唯一需要改動(dòng)的是:在排除型字符組中,如果需要表示橫線字符-(而不是用于“-范圍表示法”),那么-應(yīng)該緊跟在^之后;而在普通字符組中,作為普通字符的橫線-應(yīng)該緊跟在開方括號(hào)之后,如例1-15所示。
例1-15在排除型字符組中,緊跟在^之后的-不是元字符
#匹配一個(gè)-、0、9之外的字符 re.search(r"^[^-09]$", "-") != None # => False re.search(r"^[^-09]$", "8") != None # => True #匹配一個(gè)0~9之外的字符 re.search(r"^[^0-9]$", "-") != None # => True re.search(r"^[^0-9]$", "8") != None # => False
在排除型字符組中,^是一個(gè)元字符,但只有它緊跟在[之后時(shí)才是元字符,如果想表示“這個(gè)字符組中可以出現(xiàn)^字符”,不要讓它緊挨著[即可,否則就要轉(zhuǎn)義。例1-16給出了三個(gè)正則表達(dá)式,后兩個(gè)表達(dá)式實(shí)質(zhì)是一樣的,但第三種寫法很麻煩,理解起來(lái)也麻煩,不推薦使用。
例1-16排除型字符組的轉(zhuǎn)義
#匹配一個(gè)0、1、2之外的字符 re.search(r"^[^012]$", "^") != None # => True #匹配4個(gè)字符之一:0、^、1、2 re.search(r"^[0^12]$", "^") != None # => True #^緊跟在[之后,但經(jīng)過(guò)轉(zhuǎn)義變?yōu)槠胀ㄗ址扔谏弦粋€(gè)表達(dá)式,不推薦 re.search(r"^[\^012]$", "^") != None # => True
1.6 字符組簡(jiǎn)記法
用[0-9]、[a-z]等字符組,可以很方便地表示數(shù)字字符和小寫字母字符。對(duì)于這類常用的字符組,正則表達(dá)式提供了更簡(jiǎn)單的記法,這就是字符組簡(jiǎn)記法(shorthands)。
常見(jiàn)的字符組簡(jiǎn)記法有\d、\w、\s。從表面上看,它們與[…]完全沒(méi)聯(lián)系,其實(shí)是一致的。其中\d等價(jià)于[0-9],其中的d代表“數(shù)字(digit)”;\w等價(jià)于[0-9a-zA-Z_],其中的w代表“單詞字符(word)”;\s 等價(jià)于[ \t\r\n\v\f](第一個(gè)字符是空格),s表示“空白字符(space)”。例1-17說(shuō)明了這幾個(gè)字符組簡(jiǎn)記法的典型匹配。
例1-17字符組簡(jiǎn)記法\d、\w、\s
#如果沒(méi)有原生字符串,\d就必須寫作\\d re.search(r"^\d$", "8") != None # => True re.search(r"^\d$", "a") != None # => False re.search(r"^\w$", "8") != None # => True re.search(r"^\w$", "a") != None # => True re.search(r"^\w$", "_") != None # => True re.search(r"^\s$", " ") != None # => True re.search(r"^\s$", "\t") != None # => True re.search(r"^\s$", "\n") != None # => True
一般印象中,單詞字符似乎只包含大小寫字母,但是字符組簡(jiǎn)記法中的“單詞字符”不只有大小寫單詞,還包括數(shù)字字符和下畫線_,其中的下畫線_尤其值得注意:在進(jìn)行數(shù)據(jù)驗(yàn)證時(shí),有可能只容許輸入“數(shù)字和字母”,有人會(huì)偷懶用\w 驗(yàn)證,而忽略了\w 能匹配下畫線,所以這種匹配并不嚴(yán)格,[0-9a-zA-Z]才是準(zhǔn)確的選擇。
“空白字符”并不難定義,它可以是空格字符、制表符\t,回車符\r,換行符\n等各種“空白”字符,只是不方便展現(xiàn)(因?yàn)轱@示和印刷出來(lái)都是空白)。不過(guò)這也提醒我們注意,匹配時(shí)看到的“空白”可能不是空格字符,因此,\s才是準(zhǔn)確的選擇。
字符組簡(jiǎn)記法可以單獨(dú)出現(xiàn),也可以使用在字符組中,比如[0-9a-zA-Z]也可以寫作[\da-zA-Z],所以匹配十六進(jìn)制字符的字符組可以寫成[\da-fA-F]。字符組簡(jiǎn)記法也可以用在排除型字符組中,比如[^0-9]就可以寫成[^\d],[^0-9a-zA-Z_]就可以寫成[^\w],代碼如例1-18。
例1-18字符組簡(jiǎn)記法與普通字符組混用
#用在普通字符組內(nèi)部 re.search(r"^[\da-zA-Z]$", "8") != None # => True re.search(r"^[\da-zA-Z]$", "a") != None # => True re.search(r"^[\da-zA-Z]$", "C") != None # => True #用在排除型字符組內(nèi)部 re.search(r"^[^\w]$", "8") != None # => False re.search(r"^[^\w]$", "_") != None # => False re.search(r"^[^\w]$", ",") != None # => True
相對(duì)于\d、\w和\s這三個(gè)普通字符組簡(jiǎn)記法,正則表達(dá)式也提供了對(duì)應(yīng)排除型字符組的簡(jiǎn)記法:\D、\W 和\S——字母完全一樣,只是改為大寫。這些簡(jiǎn)記法匹配的字符互補(bǔ):\s 能匹配的字符,\S一定不能匹配;\w能匹配的字符,\W一定不能匹配;\d能匹配的字符,\D一定不能匹配。例1-19示范了這幾個(gè)字符組簡(jiǎn)記法的應(yīng)用。
例1-19 \D、\W、\S的使用
#\d和\D re.search(r"^\d$", "8") != None # => True re.search(r"^\d$", "a") != None # => False re.search(r"^\D$", "8") != None # => False re.search(r"^\D$", "a") != None # => True #\w和\W re.search(r"^\w$", "c") != None # => True re.search(r"^\w$", "!") != None # => False re.search(r"^\W$", "c") != None # => False re.search(r"^\W$", "!") != None # => True #\s和\S re.search(r"^\s$", "\t") != None # => True re.search(r"^\s$", "0") != None # => False re.search(r"^\S$", "\t") != None # => False re.search(r"^\S$", "0") != None # => True
妥善利用這種互補(bǔ)的屬性,可以得到一些非常巧妙的效果,最簡(jiǎn)單的應(yīng)用就是字符組[\s\S]。初看起來(lái),在同一個(gè)字符組中并列兩個(gè)互補(bǔ)的簡(jiǎn)記法,這種做法有點(diǎn)奇怪,不過(guò)仔細(xì)想想就會(huì)明白,\s 和\S 組合在一起,匹配的就是“所有的字符”(或者叫“任意字符”)。許多語(yǔ)言中的正則表達(dá)式并沒(méi)有直接提供“任意字符”的表示法,所以[\s\S]、[\w\W]、[\d\D]雖然看起來(lái)有點(diǎn)古怪,但確實(shí)可以匹配任意字符。
關(guān)于字符組簡(jiǎn)記法,最后需要補(bǔ)充兩點(diǎn):第一,如果字符組中出現(xiàn)了字符組簡(jiǎn)記法,最好不要出現(xiàn)單獨(dú)的-,否則可能引起錯(cuò)誤,比如[\d-a]就很讓人迷惑,在有些語(yǔ)言中,-會(huì)被作為普通字符,而在有些語(yǔ)言中,這樣寫會(huì)報(bào)錯(cuò);第二,以上說(shuō)的\d、\w、\s的匹配規(guī)則,都是針對(duì)ASCII編碼而言的,也叫ASCII匹配規(guī)則。但是,目前一些語(yǔ)言中的正則表達(dá)式已經(jīng)支持了Unicode字符,那么數(shù)字字符、單詞字符、空白字符的范圍,已經(jīng)不僅僅限于ASCII編碼中的字符。關(guān)于這個(gè)問(wèn)題,具體細(xì)節(jié)在后文有詳細(xì)的介紹,如果你現(xiàn)在就想知道,可以翻到第115頁(yè)。
1.7 字符組運(yùn)算
以上介紹了字符組的基本功能,它們?cè)诔S玫恼Z(yǔ)言中都有提供;還有些語(yǔ)言中為字符組提供了更強(qiáng)大的功能,比如Java和.NET就提供了字符組運(yùn)算的功能,可以在字符組內(nèi)進(jìn)行集合運(yùn)算,在某些情況下這種功能非常實(shí)用。
如果要匹配所有的元音字母(為講解簡(jiǎn)單考慮,暫時(shí)只考慮小寫字母的情況),可以用,但是要匹配所有的輔音字母卻沒(méi)有什么方便的辦法,最直接的寫法是[b-df-hj-np-tv-z],不但煩瑣,而且難理解。其實(shí),從26個(gè)字母中“減去”元音字母,剩下的就是輔音字母,如果有辦法做這個(gè)“減法”,就方便多了。[aeiou]
Java語(yǔ)言中提供了這樣的字符組:[[a-z]&&[^aeiou]],雖然初看有點(diǎn)古怪,但仔細(xì)看看,也不難理解。[a-z]表示26 個(gè)英文字母,[^aeiou]表示除元音字母之外的所有字符(還包括大寫字母、數(shù)字和各種符號(hào)),兩者取交集,就得到“26個(gè)英文字母中,除去5個(gè)元音字母,剩下的21個(gè)輔音字母”。
.NET中也有這樣的功能,只是寫法不一樣。同樣是匹配輔音字母的字符組,.NET中寫作[a-z-[aeiou]],其邏輯是:從[a-z]能匹配的26個(gè)字符中,“減去”[aeiou]能匹配的元音字母。相對(duì)于Java,這種邏輯更符合直覺(jué),但寫法卻有點(diǎn)古怪——不是[[a-z]-[aeiou]],而是[a-z-[aeiou]]。例1-20集中演示了Java和.NET中的字符組運(yùn)算。
例1-20字符組運(yùn)算
Java "a".matches("^[[a-z]&&[^aeiou]]$"); // => True "b".matches("^[[a-z]&&[^aeiou]]$"); // => False .NET Regex.IsMatch("^[a-z-[aeiou]]$", "a"); // => True Regex.IsMatch("^[a-z-[aeiou]]$", "b"); // => False
1.8 POSIX字符組
前面介紹了常用的字符組,但是在某些文檔中,你可能會(huì)發(fā)現(xiàn)類似[:digit:]、[:lower:]之類的字符組,看起來(lái)不難理解(digit就是“數(shù)字”,lower就是“小寫”),但又很奇怪,它們就是POSIX字符組(POSIX Character Class)。因?yàn)槟承┱Z(yǔ)言的文檔中出現(xiàn)了這些字符組,為避免困惑,這里有必要做個(gè)簡(jiǎn)要介紹。如果只使用常用的編程語(yǔ)言,可以忽略文檔中的POSIX字符組,也可以忽略本節(jié);如果想了解POSIX字符組,或者需要在Linux/UNIX下的各種工具(sed、awk、grep等)中使用正則表達(dá)式,最好閱讀本節(jié)。
之前介紹的字符組,都屬于Perl衍生出來(lái)的正則表達(dá)式流派(Flavor),這個(gè)流派叫做PCRE(Per Compatible Regular Expression)。在此之外,正則表達(dá)式還有其他流派,比如POSIX(Portable Operating System Interface for uniX),它是一系列規(guī)范,定義了UNIX操作系統(tǒng)應(yīng)當(dāng)支持的功能,其中也包括關(guān)于正則表達(dá)式的規(guī)范,[:digit:]之類的字符組就是遵循POSIX規(guī)范的字符組。
常見(jiàn)的[a-z]形式的字符組,在POSIX規(guī)范中仍然獲得支持,它的準(zhǔn)確名稱是POSIX方括號(hào)表達(dá)式(POSIX bracket expression),主要用在UNIX/Linux系統(tǒng)中。POSIX方括號(hào)表達(dá)式與之前所說(shuō)的字符組最主要的差別在于:在POSIX字符組中,反斜線\不是用來(lái)轉(zhuǎn)義的。所以POSIX方括號(hào)表達(dá)式[\d]只能匹配\和d兩個(gè)字符,而不是[0-9]對(duì)應(yīng)的數(shù)字字符。
為了解決字符組中特殊意義字符的轉(zhuǎn)義問(wèn)題,POSIX方括號(hào)表達(dá)式規(guī)定:如果要在字符組中表達(dá)字符](而不是作為字符組的結(jié)束標(biāo)記),應(yīng)當(dāng)讓它緊跟在字符組的開方括號(hào)之后,所以[]a]能匹配的字符就是]或a;如果要在字符組中標(biāo)識(shí)字符-(而不是“-范圍表示法”),就必須將它放在字符組的閉方括號(hào)]之前,所以[a-]能匹配的字符就是a或-。
另一方面,POSIX規(guī)范還定義了POSIX字符組(POSIX character class),它大致等于之前介紹的字符組簡(jiǎn)記法,都是使用類似[:digit:]、[:lower:]之類有明確意義的記號(hào)表示某類字符。
表1-2簡(jiǎn)要介紹了POSIX字符組,注意表格中與其對(duì)應(yīng)的是ASCII字符組,也就是能匹配的ASCII字符(ASCII編碼表中碼值在0~127之間的字符)。因?yàn)镻OSIX規(guī)范中有一個(gè)重要概念:locale(通常翻譯為“語(yǔ)言環(huán)境”),它是一組與語(yǔ)言和文化相關(guān)的設(shè)定,包括日期格式、貨幣幣值、字符編碼等。POSIX字符組的意義會(huì)根據(jù)locale的變化而變化,表1-2介紹的只是這些POSIX字符組在ASCII編碼中的意義;如果換用其他的locale(比如使用Unicode字符集),它們的意義可能會(huì)發(fā)生變化,具體請(qǐng)參考第129頁(yè)。
表1-2 POSIX字符組

注:標(biāo)記*的字符組簡(jiǎn)記法并不是POSIX規(guī)范中的,但使用很多,一般語(yǔ)言中都提供,文檔中也會(huì)出現(xiàn)。
POSIX字符組的使用也與PCRE字符組簡(jiǎn)記法的使用有所不同,主要區(qū)別在于,PCRE字符組簡(jiǎn)記法可以脫離方括號(hào)直接出現(xiàn),而POSIX字符組必須出現(xiàn)在方括號(hào)內(nèi)。所以同樣是匹配數(shù)字字符,PCRE中可以直接寫\d,而POSIX字符組必須寫成[[:digit:]]。
在本書介紹的6種語(yǔ)言中,Java、PHP、Ruby支持使用POSIX字符組。
在PHP中可以直接使用POSIX字符組,但是PHP中的POSIX字符組只識(shí)別ASCII字符,也就是說(shuō),任何非ASCII字符(比如中文字符)都不能由任何一個(gè)POSIX字符組匹配。
Ruby的情況稍微復(fù)雜一點(diǎn)。Ruby 1.8中的POSIX字符組只能匹配ASCII字符,而且不支持[:word:]和[:ASCII:];Ruby 1.9中的POSIX字符組可以匹配Unicode字符,而且支持[:word:]和[:ASCII:]。
Java中的情況更加復(fù)雜。POSIX字符組[[:name:]]必須使用\p{name}的形式,其中name為POSIX字符組對(duì)應(yīng)的名字,比如[:space:]就應(yīng)當(dāng)寫作\p{Space},請(qǐng)注意第一個(gè)字母要大寫,其他POSIX字符組都是這樣,只有[:xdigit:]要寫作\p{XDigit}。并且Java中的POSIX字符組,只能匹配ASCII字符。
- 零起步輕松學(xué)單片機(jī)技術(shù)(第2版)
- PPT,要你好看
- 西門子S7-200 SMART PLC從入門到精通
- 21天學(xué)通Visual C++
- Android游戲開發(fā)案例與關(guān)鍵技術(shù)
- 變頻器、軟啟動(dòng)器及PLC實(shí)用技術(shù)260問(wèn)
- 中國(guó)戰(zhàn)略性新興產(chǎn)業(yè)研究與發(fā)展·智能制造裝備
- Windows Server 2008 R2活動(dòng)目錄內(nèi)幕
- 深度學(xué)習(xí)與目標(biāo)檢測(cè)
- Building a BeagleBone Black Super Cluster
- 人工智能:語(yǔ)言智能處理
- 傳感器與自動(dòng)檢測(cè)
- 21天學(xué)通Linux嵌入式開發(fā)
- 步步驚“芯”
- Embedded Linux Development using Yocto Projects(Second Edition)