本文由Charles翻自raywenderlich
原文:NSRegularExpression Tutorial: Getting Started
更新提示:本教程被James Frost更新到了iOS8和swift。Tutorial團隊成員的Soheil Azarpour完成最初發布。
正則表達式(廣為所知的“regex”)是一個字符串或一個字符序列來說明一種模式,把它作為一個搜索字符串-非常強大!
在一個文本編輯器或文字處理器中普通的舊式搜索只允許你進行簡單的匹配。正則表達式可以實現這樣簡單的搜索,它還能讓你更進一步地按模式搜索,例如,在兩個數字後跟一個字母,或者,三個字母後跟一個連字符。
這種模式匹配能讓你做更有用的事,如驗證字段(電話號碼,郵箱地址),檢查用戶輸入,執行更高級的文本操作等等。
如果你渴望了解更多關於正則表達式在iOS中的用法,看一些本教程之外的內容--不需要有相關的經驗。
在這篇NSRegularExpression教程中,你將實現一個在文本中按模式搜索的功能,用你希望的值替代那些匹配的值,驗證你的輸入信息,在文字塊中找到並高亮顯示復雜字符串。
此外,我還將給你提供一個NSRegularExpression Cheat Sheet PDF,你可以打印出來,在你開發過程中作為參考,Swift playground 包含了很多例子,你能用它試驗出許多不同形式的正則表達式!實際上,所有正則表達式的例子都會出現在本教程中,用很生動的例子展現在playground中,一定要查看它們哦。
閒話少說,是時候來處理正則表達式了。
/The (Basics|Introduction)/
Note:如果你已經有正則表達式基礎了,可以跳過頭部,直接看Implementing Regex in iOS.
如果你是剛接觸正則表達式,並且想知道所說的這些是什麼意思,這是一個簡短的定義:正則表達式提供了一種在指定文本文檔中按指定模式進行搜索,並能基於匹配模式進行修改文本的一種方式。有許多關於正則表達式的有意思的書和教程--在本教程的結尾,你會看到一個簡短的列表。
Regular Expressions Playground
在本教程中,你將會創建許多正則表達式,假使你想要可視化的使用它們,那麼用Swift Playground 是一個絕佳的方式 !
這個starter project 包含了這個教程用到的playground。下載項目,在Xcode中運行並打開iRegex.playground.你也可以單獨下載這個playground(download the playground) 。
Playground 頂部包含許多函數來高亮顯示在一小段文本中應用正則表達式的搜索結果,展示了一系列的匹配項和分組,還有替換文本。目前不要擔心這些方法的實現,之後你會再看到它們的,在Basic Examples和Cheat Sheet部分接著看這個例子。
在playground的側邊欄,你會看到每個例子的匹配結果。比如“highlight”這個例子,你可以把鼠標指針懸浮在結果上並點擊“眼”或者空的圓圈圖標來顯示在文本中高亮的匹配內容。
你之後將學會如何創建NSRegularExpressions,現在你可以用這個playground來感受下不同的正則表達式是怎樣工作的,也可以試驗一下你自己的模式。在任一點上你都可以用Xcode中的EEditor > Reset Playground菜單按鈕來重置你的改動。
Examples
讓我們以一個小例子來展示正則表達式的樣子。
這是一個來匹配單詞“jump”的正則表達式的例子:
jump
這是一個如此簡單的正則表達式。你可以使用iOS中可用的API來查詢一個文本中的字符串來匹配這個正則表達式—一旦你找到了匹配項,你能發現它在哪兒,你也可以替換它。
這是一個略微復雜點的例子—它會匹配單詞“jump”或“jumping”:
jump(ing)?
這是應用正則表達式支持的特殊字符的例子。這個圓括號創建了一個組,這個標志是說“匹配前面的元素(這種情況下的組)0次或1次”。
現在來看一個復雜的例子。它會匹配一對開合的HTML標簽和他們之間的內容。
]*>(.*?)
喔,看起來好復雜,呃?不要擔心,在本教程的後面你將會學到正則表達式中的這些特殊字符,到時候你就能理解這是怎麼實現的了!
如果你像了解之前的正則表達式的更多細節,請參考this discussion的解釋。
Note: 在實際的應用中,你可能不會單獨用正則表達式來解析HTML(probably shouldn’t use regular expressions alone to parse HTML),相反而是用標准的XML解析器。
Overall Concepts
在看更深入的內容之前,理解一些關於正則表達式的核心概念很重要。
字面字符(Literal characters)是最簡單地一種正則表達式。你已經很熟悉他們了,比如,文字處理機或文本編輯器中得“find”操作。例如,單個字符的正則表達式 t 就會找到字母“t”出現的所有地方,正則表達式 jump 會找出所有出現“jump”的地方。優美,簡潔!
就像一種編程語言一樣,正則表達式的語法中也有一些保留字,如下:
[
( and )
\
*
+
?
{ and }
^
$
.
| (pipe)
/
這些字符被用作高級模式匹配。如果你想搜索這些字符中的一個,你需要用反斜線(\)轉義它,例如,為了搜索一個文本塊中的句號,不是用.,而是用\.。
每種環境,在Python、Perl、Java、C#、Ruby或者其他環境,在實現正則表達式時都有一些特殊的細微差別,在Swift中也不例外!
無論Objective-C還是Swift,你在字面量字符串中都需要轉義一些特殊字符(在他們之前添加\字符)。這其中一個字符就是反斜線自身\!既然這個被用來創建正則表達式的模式也是字符串,在你處理String 和 NSRegularExpression,你需要轉義反斜線時, 這就增加了復雜性。
這意味著在Swift(或者Objective-C)代碼中標准的\.將會顯示為\\.。
用以下兩點來澄清以上概念:
字面的“\\.”定義了一個字符串:\.
正則表達式\.則是匹配一個單個的句號字符.。
截獲圓括號(capturing parentheses) 被用作組模式的一部分。例如:3 (pm|am)會匹配文本“3 pm” ,也會匹配“3 am”。豎線字符(|)執行的是或操作。只要你樂意,你可以包含多個豎線字符在你的正則表達式中。例如,(Tom|Dick|Harry)是一個有效的模式,它能匹配那三個名字中的任一個。
當你需要選擇性的匹配特定的字符串時,圓括號組用起來很方便。比方說你要在一個文本中查找“November”,但是它可能被簡寫為“Nov”.你就能定義一個模式 Nov(ember)?,在捕獲圓括號(capturing parentheses)後加上問號,意味著這個圓括號內的內容是可選的。
這個圓括號(parentheses)被定義為術語捕獲(capturing)因為他們捕獲匹配的內容,並允許在你的正則表達式的其他地方引用它。
舉個例子,假使你有一個字符串“Say hi to Harry”.如果你創建一個搜索並替換的正則表達式,用that guy $1 來替換任一處出現的(Tom|Dick|Harry),結果就會是“Say hi to that guy Harry”.$1允許你引用前面規則中的第一個截獲組。
捕獲組和非捕獲組是一些高級的話題,在本教程的後面你會遇到關於他們的例子。
字符組(Character classes)相當於一組字符中匹配單個字符。字符組出現在中括號([ 和 ])之間。
例如,正則表達式 t[aeiou]會匹配“ta”、“te”、“ti”、“to”或“tu”。你可以放任意多的字符在中括號中,但是請記住,只能匹配一個字符。[aeiou]看起來是五個字符,但它真實意義卻是“a”或”e“或”i“或”o“或”u“。
如果字符連續出現,你也能在字符組中定義一個范圍。例如,為了搜索在100到109的數字,模式應該用10[0-9]。這和10[0123456789]會返回同樣地結果,不過,使用范圍來定義你的正則表達式看起來更簡潔和易於理解。
字符組不止局限於數字,你同樣可以用字符來這樣做。比如,[a-f]會匹配”a“,”b“,”c“,”d“,”e“或”f“。
字符集通常包含你想要匹配的字符,但是如果你想明確指出不要匹配的字符該怎麼辦?同樣你能定義除此之外的字符組,把^放在前面。例如,模式t[^o]就會匹配包含”t“並且後面緊跟的字符是非o的字符。
NSRegularExpressions Cheat Sheet
正則表達式是一個語法簡單但能組合成非常復雜的結果的絕佳例子!即使是一個正則表達式能手,也會再一些古怪的邊界問題上參考一些小抄。
為了能幫助你理解,我們為你提供了正式的 raywenderlich.com的NSRegularExpression Cheat SheetPDF!請下載下來查看。
除此之外,下面是cheat sheet的縮小版,和一些簡短的解釋:
.匹配任一字符。p.p匹配pop,pup,pmp,p@p等等。
\w匹配任意“word-like”字符,包括數字,字母,下劃線,不過不能匹配標點符號和其他字符。hello\w會匹配”hello_“,”hello9”和”helloo”,但不匹配”hello!”。
\d 匹配數字,大部分情況下是[0-9]。\d\d?:\d\d會匹配時間格式的字符串,比如”9:30“和”12:45“。
\b 匹配額外的字符,例如空格,標點符號。to\b 會匹配”to the moon”和“to!”中得“to”,但是不會匹配“tomorrow”。\b 用在整個單詞的匹配方面和方便。
\s 會匹配空白字符,比如,空格,制表符,換行符。hello\s 會匹配“Well,hello there!”中的 “hello ”。
^用在一行的開始。記住,這個特殊的^不同於方括號中的^!例如,^Hello 會匹配字符串“Hello there”,而不會去匹配“He said Hello”。
$ 用在一行的結束,例如,the end$ 會匹配“It was the end” 而不會去匹配 “the end was near”。
* 匹配 它之前的元素0次或多次。12*3 會匹配 13, 123, 1223, 122223, 和 1222222223。
+ 匹配 它之前的元素1次或多次. 12+3 會匹配 123, 1223, 122223, 和 1222222223。
花括號{}包含了匹配的最大和值最小個數。例如,10{1,2}1會匹配“101”和“1001”,而不會匹配“10001”,因為匹配的最小個數為1,最大個數為2。He[LI]{2,}o會匹配“HeLLo”和“HellLLLIo”和任意其他的“hello”添加多個L的變種,所以沒有限制,因為,最少的個數是2,最大的個數沒有設置。
有了這些基礎知識,就可以繼續向下學習了。
是時候你自己親自體驗一下這些例子了,它們都包含在上面提到的Playground裡了。
Implementing Regex in iOS
既然你有了這些基礎,就在APP中應用正則表達式吧。
如果你還沒有這樣做,下載 starter project 開始本教程吧。下載下來,用Xcode打開並運行它。
APP的UI部分已經完成了大部分,但這個APP的核心功能依賴與正則表達式,這個還沒有…!你的任務就是添加正則表達式來時這個APP更出色。
下面所視的幾個截圖的例子展示了這個應用的內容:
這個簡單的應用涵蓋兩個正則表達式的通用用例:
1.執行搜索:高亮顯示搜索和替換
2.驗證用戶輸入
這就開始直接使用正則表達式:文本搜索
/Search( and replace)?/
這是一個搜索/替換的簡單功能的概述:
搜索視圖控制器SearchViewController 有一個只讀的UITextView,其內容是《傲慢與偏見》的一個片段。
navigation bar包含一個搜索按鈕,點擊會呈現一個模態的SearchOptionsViewController。
用戶輸入一些信息並點擊“Search”按鈕。
APP會隱藏這個search view 並高亮顯示textview中所有匹配的內容。
如果用戶選擇了SearchOptionsViewController中的“Replace”選項,APP會執行搜索並替換文本中所有匹配的內容,不再是高亮顯示結果。
Note:你的APP會用到UITextView的NSAttributedString屬性來高亮顯示搜索的結果。更多這方面的內容請參考 iOS 6 by Tutorials的第15章--“What’s New with Attributed Strings”。
你也可以用text kit來實現高亮的功能。確保找到Text Kit Tutorial in Swift 來查看更多內容。
還有一個“Bookmark”按鈕,允許用戶高亮顯示文本中的日期,時間,位置。為簡單起見,不會涵蓋文本中出現的各種格式的日期時間位置。在教程的結尾你可以實現這個高亮功能。
開始實現這個功能的第一步是跳轉到標准字符串正則表達式的NSRegularExpression對象。
打開SearchOptionsViewController.swift。SearchViewController模態顯示這個view controller,且允許用戶鍵入他們的搜索條件,也可以指定是否區分大小寫。
看一下文件頭部的SearchOptions結構體,SearchOptions是一個封裝了用戶搜索選項的簡答結構體。代碼傳遞SearchOptions的一個實例給SearchViewController。它用這種方式很好的構造一個合適的NSRegularExpression,你可以通過運用擴展自定義的NSRegularExpression來實現。
選擇File > New > File… 選擇Swift File,命名為RegexHelpers.swift。打開新建的文件並添加如下代碼:
extension NSRegularExpression { convenience init?(options: SearchOptions) { let searchString = options.searchString let isCaseSensitive = options.matchCase let isWholeWords = options.wholeWords let regexOption: NSRegularExpressionOptions = (isCaseSensitive) ? .allZeros : .CaseInsensitive let pattern = (isWholeWords) ? "\\b\(searchString)\\b" : searchString self.init(pattern: pattern, options: regexOption, error: nil) } }
代碼為NSRegularExpression增加了一個便利構造方法。它通過SearchOptions實例的不同設置來做一些正確的配置。
當用戶請求一個不區分大小寫的搜索,正則表達式使用.CaseInsensitive的CaseInsensitiveNSRegularExpressionOptions值。NSRegularExpression默認是區分大小寫的,這個例子中,你使用的是更有好的不區分大小寫。
如果用戶請求一個完整的單詞,APP把正則表達式包含在\b字符組之內。在單詞邊界字符組中放入\b,因此,搜索模式之前和之後加上\b就會返回一個完整的單詞搜索(舉例來說,模式“\bcat\b”只會匹配單詞“cat”,而不會匹配“catch”)。
如果以任何理由都不能創建NSRegularExpression,構造函數就會失敗並返回nil。既然你有了NSRegularExpression對象,你就能伴隨著其他操作來匹配文本了。
打開SearchViewController.swift,找到searchForText,用下面的代碼替換它。
func searchForText(searchText: String, replaceWith replacementText: String, inTextView textView: UITextView) { let beforeText = textView.text let range = NSMakeRange(0, countElements(beforeText)) if let regex = NSRegularExpression(options: self.searchOptions!) { let afterText = regex.stringByReplacingMatchesInString(beforeText, options: .allZeros, range: range, withTemplate: replacementText) textView.text = afterText } }
首先,這個方法捕獲UITextView中得當前文本,並計算文本的長度。可能會把正則表達式應用在文本的一個子集上,所以你需要指定一個范圍。這種情況下,你要用字符串的整個長度才能保證正則表達式被運用在整個文本上。
不可思議的事發生在調用stringByReplacingMatchesInString的時候。這個方法返回一個新字符串並沒有改變舊字符串。然後,這個方法給UITextView設置這個新字符串,所以用戶看到了正確的結果。
繼續留在SearchViewController,找到highlightText,用下面的代碼替換它。
func highlightText(searchText: String, inTextView textView: UITextView) { // 1 let attributedText = textView.attributedText.mutableCopy() as NSMutableAttributedString // 2 let attributedTextRange = NSMakeRange(0, attributedText.length) attributedText.removeAttribute(NSBackgroundColorAttributeName, range: attributedTextRange) // 3 if let regex = NSRegularExpression(options: self.searchOptions!) { let range = NSMakeRange(0, countElements(textView.text)) let matches = regex.matchesInString(textView.text, options: .allZeros, range: range) // 4 for match in matches as [NSTextCheckingResult] { let matchRange = match.range attributedText.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: matchRange) } } // 5 textView.attributedText = attributedText.copy() as NSAttributedString }
這兒就一步一步的解釋上面的代碼:
1.首先,得到一個textview的attributedText的可變拷貝,
2.然後,創建一個整個文本長度的NSRange,並刪除已經有背景色的文本的背景色,
3.正如找到和替換,緊接著用你的便利構造方法創建一個正則表達式,獲取一個存放正則表達式與textview中文本匹配的所有匹配項的數組。
4. 輪詢每一個匹配項(把它們轉換成NSTextCheckingResult對象),並為每一項添加黃色背景。
5.最後,用高亮的結果更新UITextView。
編譯和運行你的APP,試著搜索一些不同的單詞和詞組!整個文本的匹配項都會高亮顯示,就像下面的圖片所示:
試著使用不同的選項(options)搜索單詞“the”看看效果。注意,例如,當搜索整個單詞時,‘them’中得‘the’不會高亮顯示。
再者,測試一下搜索和替換功能,看看你的文本字符串是怎樣如期替換的,試一下’match case‘和‘whole words’選項。
高亮顯示和替換都是很有用的,但是你怎樣在你的APP中更有效的利用正則表達式呢?
數據驗證
許多Apps都有某種用戶輸入,比如用戶輸入Email地址或電話號碼。你會對這個用戶的輸入執行某種級別的數據驗證,確保數據的完整,如果用戶輸入中有錯誤,通知用戶。
對這類數據驗證,正則表達式是可以完美解決的,因為他們在模式匹配方面是如此出色。
有兩個東西你需要添加到你的APP,驗證模式本身,提供一個機制驗證用戶輸入和和這些模式。確保這些對用戶來說簡單易用,你的APP內的驗證不區分大小寫,這樣,你就可以在你的模式中只使用小寫字母。
作為練習,試著想出一個正則表達式來驗證下面的字符串(不用考慮大小寫問題):
First name - 應該包含一到十個字符長度的標准英語字母。
Middle initial - 應該包含一個英語字母。
Last name - 應該包含標准英語字母加上撇號‘(apostrophe)(如這樣的名字 O’Brian) 並且二到十個字符長度。
Date of birth - 應該是以下格式之一:dd/mm/yyyy, dd-mm-yyyy, 或 dd.mm.yyyy, 且要落在 1/1/1900 和 31/12/2099之間.
當然了,當你開發的時候,你可以使用iRegex playground 來試驗你的表達式
你是怎麼想到需要的正則表達式的?如果你在這兒卡住了,回過頭去看看上面的小抄(cheat sheet)上的片段,上面的方案會對你有幫助的.
下面的劇透會展示給你你要用的正則表達式。在向下看之前,首先你自己先理解它,然後檢查你自己的結果!
Solution Inside
"^[a-z]{1,10}$", // First name "^[a-z]$", // Middle Initial "^[a-z']{2,10}$", // Last Name "^(0[1-9]|1[012])[-/.](0[1-9]|[12][0-9]|3[01])[-/.](19|20)\\d\\d$" // Date of Birth
打開 SignUpViewController.swift 用下面的代碼替換 viewDidLoad :
override func viewDidLoad() { super.viewDidLoad() textFields = [ firstNameField, middleInitialField, lastNameField, dateOfBirthField ] let patterns = [ "^[a-z]{1,10}$", // First name "^[a-z]$", // Middle Initial "^[a-z']{2,10}$", // Last Name "^(0[1-9]|1[012])[-/.](0[1-9]|[12][0-9]|3[01])[-/.](19|20)\\d\\d$" ] // Date of Birth regexes = patterns.map { NSRegularExpression(pattern: $0, options: .CaseInsensitive, error: nil) } }
在此view controller中創建一個text fields數組,和一個字符串模式數組。然後用swift的map函數 創建一個NSRegularExpression對象的數組,一一對應。
為了創建正則表達式來驗證first name ,首先要匹配字符串的開頭,然後匹配一個從a到z范圍的字符組,最後匹配字符串的結尾來確保它是在1到10個字符的長度。
接下來的第二個模式,middle initial,和last name,遵循同樣地邏輯。middle initial的情況下,你不必指定長度{1}--因為^[a-z]$默認匹配一個字符。
Note:此處你不必擔心大小寫問題--當實例化正則表達式時,會處理它。
對於出生日期,可能要麻煩一些。匹配字符串的開頭,然後是“月”部分,你要有一個捕獲組來匹配01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11 或 12中得一個,後面跟一個捕獲組匹配-,/或.。
對於”天“部分,你需要另一個捕獲組來匹配01, 02, … 29, 30, or 31,後面跟一個捕獲組匹配-,/或.。
最後,需要一個捕獲組來匹配19或20,後面跟兩個數字字符。
你就得到了創造性的正則表達式。也有另外的方式解決上面的問題,比如用\d代替[0-9]。當然了,只要能正常工作的方案就是最好的方案。
Note:實際應用中,你很可能不會用正則表達式來驗證時間(更不會在一個日期范圍內檢查它)。相反,你很可能會使用NSDateFormatter來從字符串解析成日期,然後比較解析的NSDate和引用的日期。
既然你掌握了這個模式,你需要驗證每個text field中輸入的文本。
還是停留在SignUpViewController.swift,找到validateTextField,並用下面的內容替換實現部分。
func validateTextField(textField: UITextField) { let index = find(textFields, textField) if let regex = regexes[index!] { let text = textField.text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) let range = NSMakeRange(0, countElements(text)) let matchRange = regex.rangeOfFirstMatchInString(text, options: .ReportProgress, range: range) let valid = matchRange.location != NSNotFound textField.textColor = (valid) ? UIColor.trueColor() : UIColor.falseColor() } }
這與在SearchViewController.swift所做很相似,從regexes數組中拿取相關的正則表達式開始,去掉用戶在textfield中輸入內容中含有的所有空白字符,然後創建一個包含整個文本的范圍。
為了准確的檢查匹配,代碼測試了rangeOfFirstMatchInString(_:options:range:)的結果。這可能是檢查匹配最有效的方式,因為這個調用在它發現第一個匹配後就早早退出了。然而,假使你需要知道所有的匹配,也有其他的選擇,如numberOfMatchesInString(_:options:range:)
運行程序,點擊右下方的Contacts 按鈕,試著在sign up中輸入一些信息,當你完成每個field,你會看到它的文本依據是否有效變綠或變紅效果如下方截圖:
上面的代碼用了stringByTrimmingCharactersInSet來刪除用戶輸入的開頭和結尾的空格-否則如果有空格在的話就會驗證失敗。
這是stringByTrimmingCharactersInSet的一個固定用法,但是作為怎麼處理正則表達式的教程,還有一個更有趣的方向,考慮怎樣通過正則表達式來實現。
去掉空格,一個小插曲。
這種情況你有兩種選擇:
要麼更新你的模式來處理首尾處的空格,要麼在你一個用驗證模式之前創建並應用另一個模式來去掉首尾的空格。第二種方法保證了驗證模式的簡單簡潔,也可以在它的方法內部重構,你需要的只是一個正則表達式。
你想到一個去除首位空格的正則表達式了嗎?在你看下面的結果前自己試一下。
Solution Inside
(?:^\\s+)|(?:\\s+$)
好極了,模式^\s+會發現開頭的空格,\s+$會找到結尾處的空格。現在有了一個匹配空格的正則表達式,是時候用用它了。在SignUpViewController.swift的底部,類定義花括號外面,添加如下代碼:
extension String { func stringByTrimmingLeadingAndTrailingWhitespace() -> String { let leadingAndTrailingWhitespacePattern = "(?:^\\s+)|(?:\\s+$)" if let regex = NSRegularExpression(pattern: leadingAndTrailingWhitespacePattern, options: .CaseInsensitive, error: nil) { let range = NSMakeRange(0, countElements(self)) let trimmedString = regex.stringByReplacingMatchesInString(self, options: .ReportProgress, range:range, withTemplate:"$1") return trimmedString } else { return self } } }
就像你在NSRegularExpression中所作的一樣,添加一個String的實例方法,在我們看這個方法如何實現之前,改變validateTextField,通過改變下面這個去除空格的行使用這個新方法:
let text = textField.text.stringByTrimmingLeadingAndTrailingWhitespace()
這個String的新方法用上邊的模式創建了一個正則表達式,返回一個匹配出都用$1替代的新字符串,但是$1是什麼意思呢?
當你有個正則表達式的捕獲組(用小括號()來表示),你可以用$和數字來引用組的內容。正則表達式文檔指出叫反引用。數子指出引用那個捕獲組。
作為例子,給出一個正則表達式:
Customer ID: (\d{3})-(\d{4})
匹配下面的文本:
Customer ID: 123-0089
$1的值會試123,$2的值會是0089,多有用的工具!
回頭看這個空格的例子,$1只是用它自身--一個捕獲組來取代空格,實際什麼也沒做?
這種情況下,圓括號中的?:告訴正則表達式引擎創建一個非捕獲組。也就是說,匹配的文本不會像正常情況下那樣存在緩沖區。
既然第一個捕獲組是一個非捕獲組,引擎自然不會捕獲任何東西--因此它是空的!這樣,引擎匹配空格結束,去掉空格,有效的移除了首尾的空格。
當然了,這是捕獲值的特殊用法。實際上你可以只用一個空字符串,“”,作為你的模板值。
More on Capture Groups
舉個更實際的例子,假設你想要選擇一個文件中你所有的用戶ID,讓四位數字的部分在三位數字的前面,你想要兩組數字之前的間隔更大一些,一個連字符換成兩個?更具體的來說是你想這樣:
Bob ID: 143-5546 Ellen ID: 447-6091 Piper ID: 314-1596 Easy ID: 217-1828
變換成:
Bob ID: 5546 -- 143 Ellen ID: 6091 -- 447 Piper ID: 1596 -- 314 Easy ID: 1828 -- 217
你回怎樣做呢?下面的劇透就是答案。但是你先自己試試。
Solution Inside
let cutomerIDS = ["Bob ID: 143-5546","Ellen ID: 447-6091","Piper ID: 314-1596", "Easy ID: 217-1828"] // To reverse the ID's, you'd ordinarily iterate over the array above, // but this is what would happen in each iteration. let regexSearchPattern = "^(\\w+\\s+ID:\\s+)(\\d{3})-(\\d{4})$" let regexReplacementPattern = "$1$3 -- $2" let newCustomerID1 = replaceMatches(regexSearchPattern, inString: "Bob ID: 143-5546", withString:regexReplacementPattern) let newCustomerID2 = replaceMatches(regexSearchPattern, inString: "Ellen ID: 447-6091", withString:regexReplacementPattern) let newCustomerID3 = replaceMatches(regexSearchPattern, inString: "Piper ID: 314-1596", withString:regexReplacementPattern) let newCustomerID4 = replaceMatches(regexSearchPattern, inString: "Easy ID: 217-1828", withString:regexReplacementPattern)
在這個例子包含的playground結尾處,你能看到這個例子的效果。
Note:這個正則表達式在name和ID之間允許任意數量的空格,在“ID:”和真正的ID值中間也是如此。
如果你一直糾結於非捕獲,捕獲和反向引用,在playground中試試下面的不同情況,看看會是什麼結果(建議:你可以使用‘replaceMatches’函數):
用“(^\\s+)|(\\s+$)”替換上面的空格模式,template參數用“BOO”替換
用“(?:^\\s+)|(\\s+$)”替換上面的空格模式,template參數用“$1BOO”替換
用“(?:^\\s+)|(\\s+$)”替換上面的空格模式,template參數用“$2BOO”替換
Handling Multiple Search Results
還沒有實現導航條上的書簽按鈕。當用戶點擊它時,APP應該高亮顯示文本中的日期,時間,位置。
打開SearchViewController.swift,找到書簽按鈕的實現:
//MARK: Underline dates, times, and locations @IBAction func underlineInterestingData(sender: AnyObject) { underlineAllDates() underlineAllTimes() underlineAllLocations() }
上面這個方法調用三個其他的輔助方法來給日期,時間,地點加下劃線。如果你看向者三個輔助方法,你會看到他們都是空方法!
首先,填上每個方法的實現。用下面的內容替換他們:
func underlineAllDates() { if let regex = NSRegularExpression.regularExpressionForDates() { let matches = matchesForRegularExpression(regex, inTextView: textView) highlightMatches(matches) } } func underlineAllTimes() { if let regex = NSRegularExpression.regularExpressionForTimes() { let matches = matchesForRegularExpression(regex, inTextView: textView) highlightMatches(matches) } } func underlineAllLocations() { if let regex = NSRegularExpression.regularExpressionForLocations() { let matches = matchesForRegularExpression(regex, inTextView: textView) highlightMatches(matches) } }
每個方法調用NSRegularExpression的一個工廠方法來創建一個合適的正則表達式。這些還不存在,但是這是一個方便的地方封裝這個行為。這個方法找到匹配項,調用highlightMatches來給文本中的每個字符串著色和添加下劃線。如果你有興趣看它如何實現,查看它的實現。
現在填入正則表達式方法。打開RegexHelpers.swift 並在NSRegularExpression 的擴展內添加下面的內容。
class func regularExpressionForDates() -> NSRegularExpression? { let pattern = " " return NSRegularExpression(pattern: pattern, options: .CaseInsensitive, error: nil) } class func regularExpressionForTimes() -> NSRegularExpression? { let pattern = " " return NSRegularExpression(pattern: pattern, options: .CaseInsensitive, error: nil) } class func regularExpressionForLocations() -> NSRegularExpression? { let pattern = " " return NSRegularExpression(pattern: pattern, options: .allZeros, error: nil) }
現在你來實現這些模式,這是一些你需要的內容:
Date Requirements:
xx/xx/xx or xx.xx.xx or xx-xx-xx格式。日,月,年,的防治不是很重要,因為代碼只是高亮顯示他們。例如:10-05-12.
全稱和縮寫月的名字(如,Jan或January,Feb或February等),之後跟著一兩個數字(如:x或xx).日可能是序數詞(如:1st, 2nd, 10th, 21st等),之後跟一個逗號作為分隔符,然後是一個四位的數字(如,xxxx).在日月年兩兩之間可能包含零至多個空白。例如:March 13th, 2001。
Time requirements:
找出像“9am” 或 “11 pm”的簡單時間:一兩位數字跟著一個或多個空格,再後面跟著小寫的“am” 或 “pm”。
Location requirements:
至少一個字符的任意單詞,緊跟著一個逗號,再跟著零個或多個空格,再跟著兩個大寫的英語字母組合。例如“Boston, MA”。
你可以用playground試驗一下。看是否能勾勒出需要的正則表達式!
這是三個簡單的模式。用下面的內容替換regularExpressionForDates中的空模式
let pattern = "(\\d{1,2}[-/.]\\d{1,2}[-/.]\\d{1,2})|(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?)\\s*(\\d{1,2}(st|nd|rd|th)?+)?[,]\\s*\\d{4}"
這個模式被|(或)分成了兩部分。意味著或者第一部分匹配或者第二部分匹配。
第一部分內容:(\d{1,2}[-/.]\d{1,2}[-/.]\d{1,2})。意味著兩個數字之後跟著一個-或/或.。之後再跟著兩個數字,再跟著-或/或.,最後跟兩個數字。
第二部分以 (Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?)開頭,它會匹配一個全稱或簡稱的月的名字。
接下來是 \\s*\\d{1,2}(st|nd|rd|th)?,它會匹配零個或多個空格,後面跟一到兩個數字,再之後跟著一個可選的序數詞後綴。例如,它會匹配“1” 和 “1st”。
最後,[,]\\s*\\d{4}會匹配一個逗號,之後會跟著零個或多個空格,再之後跟著一個表示年的四位數字。
多恐怖的一個正則表達式!不過,你可以看到正則表達式的簡潔,和把大量信息包裝成一個看似神秘的字符串的功能的強大!
接下來,是regularExpressionForTimes和regularExpressionForLocations的模式,把下面的內容填進空白的模式。
// Times let pattern = "\\d{1,2}\\s*(pm|am)" // Locations let pattern = "[a-zA-Z]+[,]\\s*([A-Z]{2})"
作為練習,看看你能否根據上面的要求解釋一下這個正則表達式模式。
編譯並運行這個APP,點擊Bookmark圖標。你應該會看到高亮顯示的日期,時間,位置,如下所示:
這個例子就到這兒了,你能明白為什麼這個對於時間的正則表達式不能正確進行更通用的搜索嗎?按現在的情況,它不會匹配3:15pm,它會匹配28pm。
這是一個有挑戰性的問題!想想怎樣重寫這個關於時間的正則表達式,來讓它匹配更通用的時間格式。
具體來說,你的答案應該匹配12小時制的ab:cd am/pm時間格式。所以它應該能匹配11:45 am, 10:33pm, 04:12am 但不能匹配 2pm, 0:00am 18:44am 9:63pm 或 7:4 am。在am/pm前應該有至少一個空格。如果它匹配了14:33am中得4:33am,這也是可以接受的。
下面是一個可行的答案,但是你自己先試一下。在附帶的playground尾部看一下它的效果。
Solution Inside
"(1[0-2]|0?[1-9]):([0-5][0-9]\\s?(am|pm))"
接下來要做什麼呢?
是你依據上邊的教程開發的最終例子example project 。
恭喜你!現在你已經有了一些正則表達式使用方面的實踐經驗。
正則表達式是強大的,使用它也很有趣,他們很像解決數學問題。正則表達式的彈性讓我們有很多種方法來創建一個模式去適應你的需求,例如過濾輸入字符串的空格,在解析前去除HTML或XML標簽,或者是,找出特殊的XML或HTML標簽等等!
有很多現實世界的字符串例子,你可以用正則表達式去驗證。
作為最後的練習,試圖解開下面這個正則表達式來驗證一個郵箱地址(validates an email address):
[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?
乍看起來,它看起來是一堆雜亂的字符串,但是,用你新發現的知識(下面的鏈接很有用),你會一步一步理解它,並成為正則表達式的高手!
這是一些關於正則表達式很有用的資源列表:
www.regular-expressions.info 是Jan Goyvaerts的一個非常有信息量的站點。他也出版了一些關於正則表達式很全面的書。
NSRegularExpression Class Reference 也是你用正則表達式API最好的參考。
一些正則模式的快速測試,regexpal.com是很方便的資源。
假使你之前錯過了這些鏈接,看一下這些我們為你准備的資源:
Regular expression playground
NSRegularExpression cheat sheet
希望你喜歡這個教程,如果你有任何意見和問題,請加入下面的論壇!