iOS开发之详解正则表达式
本文由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是很方便的资源。
假使你之前错过了这些链接,看一下这些我们为你准备的资源: