官术网_书友最值得收藏!

  • 正則指引
  • 余晟著
  • 1119字
  • 2019-01-09 16:29:09

第一部分

第1章 字符組

1.1 普通字符組

字符組(Character Class)在有的資料中,寫作Character Set,所以也有人翻譯為“字符類”或者“字符集”。不過(guò)在計(jì)算機(jī)術(shù)語(yǔ)中,“類”是和“對(duì)象”相關(guān)的,“字符集”常常表示Character Set(比如GBK、UTF-8之類),所以本書中沒(méi)有采用這兩個(gè)名字。是正則表達(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è)位置上的字符只能是012、…、89這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è)字符組,表示“這里可以是012、…、89中的任意一個(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)用客觀地說(shuō),Perl是正則表達(dá)式處理最方便的編程語(yǔ)言,考慮到今天使用Perl的人數(shù),以及Perl程序員一般都熟練掌握正則表達(dá)式的現(xiàn)實(shí),本書沒(méi)有給出Perl語(yǔ)言的例子。

      .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)本書寫作時(shí),2.x最新的版本為2.7,示例以此為準(zhǔn)。Python 3雖然已經(jīng)正式發(fā)行,但相對(duì)2.x變化較大,而2.x較為流行,所以采用了2.x版本。關(guān)于2.x和3.x的差別,在Python一章有詳細(xì)介紹。。請(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]的形式表示xy整個(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è)字符-、09的字符組;[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)的方式具體來(lái)說(shuō),在Java、PHP、Python、.NET等語(yǔ)言中,正則表達(dá)式都是以字符串的形式給出的,在Ruby和JavaScript中則不是這樣。詳細(xì)的說(shuō)明,請(qǐng)參考第96頁(yè)。提供的,而字符串本身也有關(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è)字符組,它只能匹配012這三個(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ù)字字符(比如A8x6一般來(lái)說(shuō),計(jì)算機(jī)中的偏移值都是從0 開始的。此處考慮到敘述自然,使用了“第一個(gè)字符”和“第二個(gè)字符”的說(shuō)法,其中“第一個(gè)字符”指最左端,也就是偏移值為0的字符;“第二個(gè)字符”指緊跟在它右側(cè),也就是偏移值為1的字符。——應(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è)-、09之外的字符
      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è)012之外的字符
      re.search(r"^[^012]$", "^") != None          #  => True
      #匹配4個(gè)字符之一:0^12
      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)于正則表達(dá)式的文檔說(shuō):點(diǎn)號(hào).能匹配“任意字符”。但在默認(rèn)情況下,點(diǎn)號(hào)其實(shí)不能匹配換行符,具體請(qǐng)參考第84頁(yè)。

關(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字符。

主站蜘蛛池模板: 乐清市| 肥乡县| 南平市| 河北省| 南投县| 武隆县| 东莞市| 南宫市| 西宁市| 武宁县| 山西省| 定安县| 通化市| 新田县| 维西| 西平县| 辛集市| 如东县| 大足县| 丰镇市| 阿荣旗| 怀宁县| 崇阳县| 鲜城| 台前县| 祥云县| 临潭县| 五家渠市| 东至县| 肥西县| 毕节市| 隆化县| 沛县| 敦化市| 于都县| 永福县| 龙游县| 昌平区| 玛沁县| 安阳市| 马关县|