
轉義、處理形式、優先級、回車和換行1、轉義1.1、字符串轉義與正則轉義1.2、元字符的轉義1.3、徹底消除元字符的特殊含義1.4、字符組中的轉義2、正則表達式的處理形式2.1、函數式處理2.2、面向對象式處理2.3、比較2.4、線程安全性3、表達式中的優先級4、回車和換行1、轉義正則表達式中的轉義是一個麻煩的問題經常會搞得人頭疼。雖然之前各章都提到了轉義仍然有必要專門介紹。1.1、字符串轉義與正則轉義理解轉義的基礎是明白字符串與正則表達式的關系。通常說的string字符串中string稱為字符串文字String Literal??它是某個字符串的值在源代碼中的表現形式。比如字符串文字\n它包含\和n兩個字符意義或者說它的值是一個換行符為方便觀察表示為NL?。在生成字符串時應當進行“字符串轉義”?才能準確識別字符串文字中\n的意思如下表所示。另一方面在源代碼中看到的“正則表達式”regex其中的regex稱為正則表達式文字Regular ExpressionLiteral以下簡稱正則文字?是正則表達式的表現形式。比如正則表達式\d其正則文字包含\和d兩個字符它的意義或者說值是匹配數字字符的字符組簡記法。在生成正則表達式時應當進行“正則轉義”?才能將正則文字中的\d識別為字符組簡記法。這兩點都不難理解但從字符串轉到正則表達式的過程就比較復雜。因為不少正則表達式都是以字符串形式提供的所以必須經過從字符串文字到正則表達式的轉換。根據上面的介紹字符串文字首先必須經過“字符串轉義”?才能得到真正的字符串接下來這個字符串作為正則文字經過“正則轉義”?最終得到正則表達式。下圖說明了從字符串文字到正則表達式的轉義過程下表則列出了若干常用字符串的轉義。換行符和制表符的情況比較容易理解它們的正則文字是\n和\t也就是字符串的值。但是從字符串文字必須經過字符串轉義才能得到作為正則文字的\n和\t根據字符串轉義的規則反斜線字符\必須寫成\所以字符串文字必須寫成\n和\t。如果要表示正則表達式中的\必須使用正則文字\\這樣在正則轉義時才能正確識別。同時正則文字中的每個\都必須用字符串文字\\表示所以正則表達式\對應的字符串文字就是\\\\。網絡上常有人問某個正則表達式中為什么需要連續用到4個反斜線字符原因就在這里。\的轉義有時候情況更復雜正則表達式中的換行符或者制表符在字符串文字中必須寫成\\n或\\t但是用\n或\t也沒有問題。原因在于在處理字符串轉義時它們已經被解釋為換行符或制表符所以傳遞給正則表達式的字符串中就包含了換行符或者制表符具體情況如下表所示。從下例可以看到字符串文字中的\\n和\n表示的是同一個字符只是前者先做字符串轉義再做正則轉義而后者只做了字符串轉義。看來似乎還是\n更符合直覺一些—如果你確實能夠理解為什么字符串文字中\\n和\n的結果竟然一樣使用\n確實可行。但是正則表達式中\的字符串文字還是必須寫成\\\\。\n和\t的轉義要特別注意的是\b。在一般字符串中\b是預定義的轉義序列表示退格符backspace表示為BS?但是在正則表達式中它表示單詞邊界記為\B?。如果在字符串文字中寫\b字符串轉義為退格符作為正則表達式交給正則表達式正則表達式真正得到的就是退格符\BS而不是單詞邊界\B如下表和下例所示。所以如果用到了單詞邊界在字符串文字中一定要寫成\\b。所以最保險的辦法是正則表達式中的每一個\在字符串文字中都要寫成\\。\b的轉義在Python中也可以這樣如下例所示。Python中 \d的轉義看起來足夠奇怪因為在字符串中\d和\(都不是合法的轉義序列所以如果在Java中這樣寫是會報編譯錯誤的。為什么Python中可以呢這是因為Python對字符串有特殊的規定如果遇到無法識別的轉義序列則將它原封不動地保存下來具體的處理見下表。在PHP中也有同樣的規定所以在PHP的字符串文字中也可以直接寫\d、\w、\s等效果和\d、\w、\s是一樣的。這樣看來使用字符串形式的正則表達式轉義的處理確實比較復雜。最好是能省去這些麻煩—正則表達式是怎樣的正則文字中就怎樣寫。要做到這一點有兩種辦法第一使用原生字符串Raw String?也就是完全忽略字符串轉義的特殊字符串在Python、Ruby、.NET、Golang中都提供了原生字符串。第二直接使用正則文字在Ruby和JavaScript中可以這么做。下面以正則表達式a\.\nb\b為例。這種方法一般是直接在正則表達式兩端添加分隔符/。此時字符串轉義會被略過不過另外還有一個字符需要轉義就是分隔符/。總的來看轉義的情況確實很復雜。我推薦的做法是第一如果可以使用原生字符串或者正則文字直接表示正則表達式應當盡量使用這種做法因為它簡單直觀方便理解?而且本書從此開始如果能使用原生字符串表示正則表達式則使用原生字符串在Python示例代碼中使用rregex的形式。第二如果確定必須使用字符串文字請盡量堅持這條原則正則表達式中的每個反斜線\在字符串文字中都必須寫成\只有\n、\t中的反斜線例外但是\n和\t也不難理解?我不推薦使用語言自身提供的“字符串轉義時將無法識別的轉義序列保留下來”的規定—比如要在正則表達式中使用字符組簡記法\d正則文字中要寫為\d而不是\d—這樣有利于使用其他語言的程序員閱讀。1.2、元字符的轉義了解了字符串與表達式的關系再來看正則表達式中元字符的轉義。元字符是有特殊含義的字符如果要匹配“元字符”自身則必須轉義也就是在元字符之前添加反斜線\。比如元字符點號.可以匹配除換行符以外的任何字符如果要準確匹配字符串中的點號.正則表達式中就必須寫\.。也有些時候匹配元字符自身并非一定要轉義下表列出了各種結構的轉義。從表中可以看到對稱出現的元字符在轉義時并不是“對稱”的比如與開方括號[對應的閉方括號]與開花括號{對應的閉花括號}這兩個字符是否是元字符取決于之前是否出現了開方括號或開花括號。如果出現了則作為元字符出現否則作為普通字符出現。下表以方括號為例說明了這一點。字符組內部的閉方括號]在任何情況下都要轉義否則類似[?]?]的正則表達式會出現二義性造成識別錯誤。所以能匹配字符a、字符b、字符]的字符組應當寫為[ab\]?]而不是[ab]?]。同樣道理括號內部的任何閉括號)都要轉義比如包含ab和b)的多選結構的正則表達式就應當寫為(ab|c\))而不是(ab|c))。另一點容易忽略的是在進行正則表達式替換時replacement字符串中也可能出現轉義。比如在Java中replacement里通過$num引用對應的捕獲分組。如果想在替換時使用一個單獨的$符號而不是引用分組比如生成價格字符串?則會報錯必須做轉義才可以解決問題在Java中使用\$代碼見下例。Java中$的轉義.NET中的replacement中同樣使用$num引用對應的捕獲分組但是.NET中$的轉義并不是\$而是$$代碼見下例。.NET中$的轉義Condoe.Write(Regex.Replace(the price is 12.99, \\d\\.\\d{0, 2}, $$$0)); the price is $12.991.3、徹底消除元字符的特殊含義有些時候需要消除所有元字符的特殊含義全部作為普通字符。這種情況經常發生在處理用戶輸入的場合用戶輸入了某個字符串需要根據這個字符串進行正則表達式查找。假設某個文件中包含多個文本片段用空格分隔現在需要查找包含用戶輸入內容的片段用戶輸入cat查找包含cat的行直接的思路是^.*cat.*$使用多行模式同時不能指定單行模式?假設用戶輸入的內容保存在變量userInput中就應當用^.* userInput .*$得到查找用的正則表達式。如果用戶輸入cat這么做當然沒有問題。如果用戶輸入的是ca*t得到的正則表達式就是^.*ca*t.*$本意是查找包含字符串ca*t的行但*是正則表達式中的元字符正則表達式ca*t能匹配字符串cat、caat、caaat卻不能匹配ca*t這樣就會產生錯誤。更麻煩的是惡意用戶可能會輸入a(b*b*)b*之類的字符串這樣的正則表達式匹配起來會消耗非常多的資源這就是正則表達式拒絕服務攻擊?嚴重時可能把服務程序打垮。為解決這類問題一些語言中的正則表達式提供了特殊結構徹底消除元字符的特殊含義提供真正“安全”的表達式也就是普通字符串?。最常見的做法是在需要消除元字符特殊含義的正則表達式兩端添加\Q和\E也就是\Q…\E其中Q表示“引用文本Quoted”?而E表示“引用文本結束End”??像下例那樣。此時正則表達式\Qca*t\E就不再能匹配cat、caat、caaat只能匹配ca*t。\Q…\E只對在它內部的子表達式生效所以在正則表達式^.*\Qca*t\E.*$中^.$和.*$會按照正則表達式的默認規則匹配只有中間的ca*t作為普通字符串匹配整個表達式能夠匹配的就是確定包含ca*t的行。用\Q…\E消除元字符的特殊含義\Q…\E并不是所有語言都支持的在本書介紹的語言中明確支持它的有Java、PHP、Objective-C、Golang。其他語言雖然不支持\Q…\E的記法但其中一些也提供了專門的處理函數消除元字符的特殊含義比如.NET的Regex.Escape(text)它接收一個字符串返回的字符串中的所有元字符都做了轉義處理消除了特殊含義從其結果字符串生成的正則表達式就只能匹配與text完全一樣的字符串代碼見下例。用專門的函數消除元字符的特殊含義下表列出了各種語言中消除元字符特殊含義的函數。PHP中的正則表達式兩端必須出現分隔符所以preg_quote()可以設定第二個參數明確指定分隔符否則假如分隔符是/而text又包含/就可能發生沖突。請記住使用正則表達式時仔細分辨并消除用戶輸入字符串中元字符的特殊含義是不可忽略的步驟。1.4、字符組中的轉義在正則表達式中如果需要表示作為元字符的普通字符比如*、?、(等?就需要使用轉義這一點不存在疑義。特殊的是常見的元字符出現在字符組內部基本都不算元字符也就是說它們在字符組內部出現時不需要轉義。代碼如下例所示。字符組內部幾乎沒有元字符之前講過字符組有自己的元字符規定也有相應的轉義規定在字符組內部只有三個字符需要轉義。一個是閉方括號]?如果不是作為字符組結束標志的閉方括號則必須寫成\]比如[0\]9]它可以匹配的字符是0、?]?、9一個是橫線-如果不是用于范圍表示法比如[0-9]?必須寫成\-比如[0\-9]它可以匹配的字符是0、-、9當然如果它緊跟在開方括號之后也可以不用轉義[0\-9]和[-09]是等價的我更推薦后一種寫法因為更清晰簡潔?還有一個需要轉義的字符是^如果它不是用于排除型字符組[^ab]?則應當寫成\^比如[\^ab]它可以匹配除^、a、b之外的任何字符如果它不是緊跟在開方括號之后也不用轉義[\^ab]、[a^b]、[ab^]是完全等價的我更推薦后兩種寫法因為更清晰簡潔?。從下例可以看到這兩種寫法的結果是相同的。字符組中三個需要轉義的元字符2、正則表達式的處理形式在之前講解正則表達式時主要使用的Python語言中其具體辦法是調用re這個package中的函數方法?比如re.search()、re.findall()、re.sub()。選擇合適的函數將正則表達式和字符串傳入這是一種常見的方式比如PHP也是如此。但是正則表達式并不只有這一種處理形式比如Java語言的處理形式就與Python和PHP大不一樣所以下面簡單介紹常見的處理形式。2.1、函數式處理在函數式處理中正則表達式的常見操作查找、替換等都有對應的函數執行這些操作時調用對應的函數將正則表達式和字符串作為參數傳入即可Python、PHP是函數式處理的典型代表。2.2、面向對象式處理Java、.NET、Objective-C、Golang之類的語言中的正則表達式采取了不同的處理形式進行正則表達式處理之前必須生成專門的正則表達式對象在不同的語言里對象所屬的類名不同?再調用此對象的成員函數。2.3、比較同樣是進行查找操作如果使用函數式處理只需要調用對應的函數但使用面向對象式處理需要分步進行首先生成各種對象再調用對象自身的方法。看起來后者要麻煩很多這種處理方式有什么好處呢之前講過正則表達式并不等于字符串即便正則表達式是以字符串形式給出的進行正則表達式操作之前必須首先生成專用的“正則表達式對象”?。函數式處理隱去了生成的過程感覺更加直接面向對象式處理則暴露了生成的過程感覺更加細致。只要執行正則表達式操作就會產生正則表達式對象所以面向對象式處理的代碼雖然更煩瑣但是如果需要重復用到某些表達式它的效率往往遠高于函數式處理—在面向對象式處理中可以將已經生成的對象作為變量保存起來就不必重復生成了。面向對象式處理的步驟分明、效率更高而函數式處理的好處是方便順手、代碼簡潔。所以最好的辦法是根據應用場合的不同采取不同的處理方式如果正則表達式只是單次使用則選擇函數式處理如果正則表達式需要重復使用則選擇面向對象式處理。實際上許多編程語言也提供了對應的設計比如Python提供了一些面向對象式處理的方法Java和.NET也提供了一些函數式處理的方法。在Python中進行面向對象式處理可以先調用re.compile()生成專門的RegexObject對象也就是正則表達式對象?在進行正則表達式操作時可以把這個對象作為參數傳給對應的函數另一方面re中各個函數也可作為對象自身的成員方法所以調用生成好的RegexObject對象的成員方法也是可以的如下例所示。Python中可以使用兩種處理方式Java語言雖然采用面向對象式處理但也提供了一些便捷函數比如Pattern.matches(regex,input)它用來驗證input能否由regex匹配如下例所示。Java中也可以使用函數式處理.NET中也提供了類似的方法比如Regex.IsMatch(regex,input)它也用來驗證input能否由regex匹配代碼如下例所示。.NET(C#)中也可以使用函數式處理2.4、線程安全性如果采用面向對象式處理可以將生成的正則表達式對象存儲在變量中反復使用以提高效率。在單線程編程的環境下?“反復使用”當然沒有問題如果多個線程共用同一個正則表達式對象結果會如何呢仔細觀察面向對象式處理可以發現在面向對象式處理中一般會出現兩個對象一個是單獨對應正則表達式的可以叫作“正則表達式對象”?比如Java中的Pattern、.NET中的Regex、Python中的RegexObject、Objective-C中的NSRegularExpression、Golang中的Regexp另一個是在正則表達式與希望處理的字符串聯系起來時生成的對象可以叫作“匹配結果對象”?比如Java中的Matcher、.NET中的Match、Python中的MatchObject。?這幾個對象之間的關系如下圖所示。正則表達式對象本身基本沒有什么狀態可言所以這個對象總是線程安全的可以由多個線程共享。匹配結果對象則需要維護自身狀態比如當前匹配是否成功當前匹配的開始或結束偏移值等。如果多個線程共享同一個匹配結果對象有可能遇到這樣的情況某個線程正在查詢當前匹配的開始位置另一個線程調用了matcher.find()Java中或者Match.nextMatch().NET中之類的方法嘗試進行下一次匹配就可能產生混亂。所以匹配結果對象一般不是線程安全的也不應由多個線程共享。因此最理想的辦法是多個線程可以共享同一個正則表達式對象節省時間但操作不同文本時應當針對各個線程生成專屬的匹配結果對象如下圖所示。多線程環境下錯誤使用正則表達式以Java為例多線程環境下正確使用正則表達式以Java為例3、表達式中的優先級正則表達式千變萬化都是由之前介紹的字符組、括號、量詞等基本結構組合而成的只要掌握了組合的規則面對再復雜的表達式都能把結構梳理清楚。仔細觀察會發現正則表達式的元素之間的組合關系只有4種。除列出的4種組合關系外正則表達式中的其他結構比如環視(?…)都可以視為單獨的元素其中的正則表達式最終可以歸類到上面的4種組合關系中。所以真正需要關心的就是這4種組合的優先級詳見下表。有了上表就可以拆解各種表達式了下表給出了幾個簡單的例子。如果你覺得上面這幾個例子都不難理解來看一個更復雜的表達式ab*(cd|e)?|fg它的優先級劃分關系如下圖所示。判斷優先級并不困難一般來說根據上面介紹的知識逐步拆解即可但是多選結構值得單獨強調。常見的多選結構是以豎線配合括號(option1|option2)的形式給出來的但是多選結構并非必須與括號連用如果沒有括號單獨出現豎線|也可以實現多選結構的功能比如正則表達式ab|cd它能匹配的字符串和(ab|cd)是完全一樣的。比較起來后一種寫法更加清楚因為兩端的括號明確限定了豎線所標識的多選結構的范圍所以很難混淆^(ab|cd)$和^ab|cd$后者等于(^ab|cd$)而且在匹配之后還可以使用分組及反向引用等功能\1、\2之類?明確得到匹配的文本不用括號則做不到這點?再進行下一步操作。但是分組又會降低效率尤其是在正則表達式非常復雜或者要處理的文本非常多時使用分組可能會嚴重影響效率。考慮到這些因素我推薦的做法是應用多選結構一定不能省略括號但同時為了避免分組影響效率如果不需要提取捕獲文本應當把普通的括號(…)改為非捕獲型括號(?:…)這樣既明確標識了多選結構又免去了分組捕獲的成本。不過在本書中為了格式簡單清晰考慮除非特別說明一般不使用非捕獲型括號。4、回車和換行許多程序員經常搞不清楚回車\r和換行\n到底有什么區別只知道兩個字符寫法不同。然而鍵盤上只有回車鍵它起到的作用卻是“換行”?。在大多數時候我們只是在編輯文本時敲一下回車鍵然后本行結束光標自動轉到下一行的開始位置。因為平臺和軟件的不同有時候輸入的是換行\n有時候輸入的是回車換行\r\n歷史久遠的某些平臺可能會輸入\r。為什么會這樣其中的區別到底在哪里其實這是一個歷史問題。在文字處理軟件還沒有發明的年代大家只能用機械打字機來打字如今還可以在一些電影里看到它的樣子。不同于今天計算機中的文字處理軟件可以由計算機來排版機械打字機的每個鍵“打”在紙上的位置都是相同的為了避免字符輸入重疊每打印一個字符打印紙就向左移動一格這樣下一個字符才會打在當前字符的右側。實現這個功能的裝置的中文名是“打印頭”?英文名是carriage—大概因為它比較像馬車吧。在使用機械打字機時每打印完一行就必須敲“回車”?讓打印頭回到初始位置再次打字仍然從最左側開始。同時還不要忘記敲“換行”?讓打印紙向上移動一行否則兩行就會重疊在一起后一行的字符直接打在前一行已有的字符上。今天在計算機中無論回車還是換行都不會讓兩行文字重疊在一起所以把回車和換行分開顯得有點怪異。不過講清楚它們背后的歷史確實能解開許多人的疑惑。