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

圖1-1 MacOS下的Python提示符

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

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

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

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

注:標記*的字符組簡記法并不是POSIX規范中的,但使用很多,一般語言中都提供,文檔中也會出現。
POSIX字符組的使用也與PCRE字符組簡記法的使用有所不同,主要區別在于,PCRE字符組簡記法可以脫離方括號直接出現,而POSIX字符組必須出現在方括號內。所以同樣是匹配數字字符,PCRE中可以直接寫\d,而POSIX字符組必須寫成[[:digit:]]。
在本書介紹的6種語言中,Java、PHP、Ruby支持使用POSIX字符組。
在PHP中可以直接使用POSIX字符組,但是PHP中的POSIX字符組只識別ASCII字符,也就是說,任何非ASCII字符(比如中文字符)都不能由任何一個POSIX字符組匹配。
Ruby的情況稍微復雜一點。Ruby 1.8中的POSIX字符組只能匹配ASCII字符,而且不支持[:word:]和[:ASCII:];Ruby 1.9中的POSIX字符組可以匹配Unicode字符,而且支持[:word:]和[:ASCII:]。
Java中的情況更加復雜。POSIX字符組[[:name:]]必須使用\p{name}的形式,其中name為POSIX字符組對應的名字,比如[:space:]就應當寫作\p{Space},請注意第一個字母要大寫,其他POSIX字符組都是這樣,只有[:xdigit:]要寫作\p{XDigit}。并且Java中的POSIX字符組,只能匹配ASCII字符。
- 輕松學PHP
- 網頁編程技術
- Dreamweaver CS3網頁設計與網站建設詳解
- 離散事件系統建模與仿真
- 小型電動機實用設計手冊
- C語言寶典
- Nginx高性能Web服務器詳解
- Learn CloudFormation
- Enterprise PowerShell Scripting Bootcamp
- 空間站多臂機器人運動控制研究
- Hands-On Data Warehousing with Azure Data Factory
- 從零開始學JavaScript
- 傳感器原理與工程應用
- SolarWinds Server & Application Monitor:Deployment and Administration
- 暗戰強人:黑客及反黑客工具快速精通