详解正则表达式
本文由Charles翻自raywenderlich 正则表达式(广为所知的“regex”)是一个字符串或一个字符序列来说明一种模式,把它作为一个搜索字符串-非常强大! 在一个文本编辑器或文字处理器中普通的旧式搜索只允许你进行简单的匹配。正则表达式可以实现这样简单的搜索,它还能让你更进一步地按模式搜索,例如,在两个数字后跟一个字母,或者,三个字母后跟一个连字符。 这种模式匹配能让你做更有用的事,如验证字段(电话号码,邮箱地址),检查用户输入,执行更高级的文本操作等等。 如果你渴望了解更多关于正则表达式在iOS中的用法,看一些本教程之外的内容--不需要有相关的经验。 在这篇NSRegularExpression教程中,你将实现一个在文本中按模式搜索的功能,用你希望的值替代那些匹配的值,验证你的输入信息,在文字块中找到并高亮显示复杂字符串。 此外,我还将给你提供一个NSRegularExpression Cheat Sheet PDF,你可以打印出来,在你开发过程中作为参考,Swift playground包含了很多例子,你能用它试验出许多不同形式的正则表达式!实际上,所有正则表达式的例子都会出现在本教程中,用很生动的例子展现在playground中,一定要查看它们哦。 闲话少说,是时候来处理正则表达式了。 如果你是刚接触正则表达式,并且想知道所说的这些是什么意思,这是一个简短的定义:正则表达式提供了一种在指定文本文档中按指定模式进行搜索,并能基于匹配模式进行修改文本的一种方式。有许多关于正则表达式的有意思的书和教程--在本教程的结尾,你会看到一个简短的列表。 在本教程中,你将会创建许多正则表达式,假使你想要可视化的使用它们,那么用SwiftPlayground 是一个绝佳的方式! 这个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”的地方。优美,简洁! 就像一种编程语言一样,正则表达式的语法中也有一些保留字,如下:
这些字符被用作高级模式匹配。如果你想搜索这些字符中的一个,你需要用反斜线()转义它,例如,为了搜索一个文本块中的句号,不是用.,而是用.。 每种环境,在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的缩小版,和一些简短的解释:
有了这些基础知识,就可以继续向下学习了。 是时候你自己亲自体验一下这些例子了,它们都包含在上面提到的Playground里了。 Implementing Regex in iOS 既然你有了这些基础,就在APP中应用正则表达式吧。 如果你还没有这样做,下载starter project开始本教程吧。下载下来,用Xcode打开并运行它。 APP的UI部分已经完成了大部分,但这个APP的核心功能依赖与正则表达式,这个还没有…!你的任务就是添加正则表达式来时这个APP更出色。 下面所视的几个截图的例子展示了这个应用的内容:
这个简单的应用涵盖两个正则表达式的通用用例: 这就开始直接使用正则表达式:文本搜索 /Search( and replace)?/ 这是一个搜索/替换的简单功能的概述:
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。打开新建的文件并添加如下代码: extensionNSRegularExpression{ convenienceinit?(options:SearchOptions){ letsearchString=options.searchString letisCaseSensitive=options.matchCase letisWholeWords=options.wholeWords letregexOption:NSRegularExpressionOptions=(isCaseSensitive)?.allZeros:.CaseInsensitive letpattern=(isWholeWords)?"b(searchString)b":searchString self.init(pattern:pattern,options:regexOption,error:nil) } } 代码为NSRegularExpression增加了一个便利构造方法。它通过SearchOptions实例的不同设置来做一些正确的配置。
如果以任何理由都不能创建NSRegularExpression,构造函数就会失败并返回nil。既然你有了NSRegularExpression对象,你就能伴随着其他操作来匹配文本了。 打开SearchViewController.swift,找到searchForText,用下面的代码替换它。 funcsearchForText(searchText:String,replaceWithreplacementText:String,inTextViewtextView:UITextView){ letbeforeText=textView.text letrange=NSMakeRange(0,countElements(beforeText)) ifletregex=NSRegularExpression(options:self.searchOptions!){ letafterText=regex.stringByReplacingMatchesInString(beforeText,options:.allZeros,range:range,withTemplate:replacementText) textView.text=afterText } } 首先,这个方法捕获UITextView中得当前文本,并计算文本的长度。可能会把正则表达式应用在文本的一个子集上,所以你需要指定一个范围。这种情况下,你要用字符串的整个长度才能保证正则表达式被运用在整个文本上。 不可思议的事发生在调用stringByReplacingMatchesInString的时候。这个方法返回一个新字符串并没有改变旧字符串。然后,这个方法给UITextView设置这个新字符串,所以用户看到了正确的结果。 继续留在SearchViewController,找到highlightText,用下面的代码替换它。 funchighlightText(searchText:String,inTextViewtextView:UITextView){ //1 letattributedText=textView.attributedText.mutableCopy()asNSMutableAttributedString //2 letattributedTextRange=NSMakeRange(0,attributedText.length) attributedText.removeAttribute(NSBackgroundColorAttributeName,range:attributedTextRange) //3 ifletregex=NSRegularExpression(options:self.searchOptions!){ letrange=NSMakeRange(0,countElements(textView.text)) letmatches=regex.matchesInString(textView.text,range:range) //4 formatchinmatchesas[NSTextCheckingResult]{ letmatchRange=match.range attributedText.addAttribute(NSBackgroundColorAttributeName,value:UIColor.yellowColor(),range:matchRange) } } //5 textView.attributedText=attributedText.copy()asNSAttributedString } 这儿就一步一步的解释上面的代码: 1.首先,得到一个textview的attributedText的可变拷贝, 2.然后,创建一个整个文本长度的NSRange,并删除已经有背景色的文本的背景色, 3.正如找到和替换,紧接着用你的便利构造方法创建一个正则表达式,获取一个存放正则表达式与textview中文本匹配的所有匹配项的数组。 4.轮询每一个匹配项(把它们转换成NSTextCheckingResult对象),并为每一项添加黄色背景。 5.最后,用高亮的结果更新UITextView。 编译和运行你的APP,试着搜索一些不同的单词和词组!整个文本的匹配项都会高亮显示,就像下面的图片所示: 试着使用不同的选项(options)搜索单词“the”看看效果。注意,例如,当搜索整个单词时,‘them’中得‘the’不会高亮显示。 高亮显示和替换都是很有用的,但是你怎样在你的APP中更有效的利用正则表达式呢? 数据验证 许多Apps都有某种用户输入,比如用户输入Email地址或电话号码。你会对这个用户的输入执行某种级别的数据验证,确保数据的完整,如果用户输入中有错误,通知用户。 对这类数据验证,正则表达式是可以完美解决的,因为他们在模式匹配方面是如此出色。 作为练习,试着想出一个正则表达式来验证下面的字符串(不用考虑大小写问题):
当然了,当你开发的时候,你可以使用iRegexplayground来试验你的表达式 你是怎么想到需要的正则表达式的?如果你在这儿卡住了,回过头去看看上面的小抄(cheat sheet)上的片段,上面的方案会对你有帮助的. 下面的剧透会展示给你你要用的正则表达式。在向下看之前,首先你自己先理解它,然后检查你自己的结果! Solution Inside "^[a-z]{1,10}$",//Firstname "^[a-z]$",//MiddleInitial "^[a-z']{2,//LastName "^(0[1-9]|1[012])[-/.](0[1-9]|[12][0-9]|3[01])[-/.](19|20)dd$"//DateofBirth 打开SignUpViewController.swift用下面的代码替换viewDidLoad: overridefuncviewDidLoad(){ super.viewDidLoad() textFields=[firstNameField,middleInitialField,lastNameField,dateOfBirthField] letpatterns=["^[a-z]{1,//LastName "^(0[1-9]|1[012])[-/.](0[1-9]|[12][0-9]|3[01])[-/.](19|20)dd$"]//DateofBirth regexes=patterns.map{ NSRegularExpression(pattern:$0,options:.CaseInsensitive,250)"> 在此view controller中创建一个text fields数组,和一个字符串模式数组。然后用swift的map函数创建一个NSRegularExpression对象的数组,一一对应。 在这个例子包含的playground结尾处,你能看到这个例子的效果。 Note:这个正则表达式在name和ID之间允许任意数量的空格,在“ID:”和真正的ID值中间也是如此。 如果你一直纠结于非捕获,捕获和反向引用,在playground中试试下面的不同情况,看看会是什么结果(建议:你可以使用‘replaceMatches’函数):
Handling Multiple Search Results 还没有实现导航条上的书签按钮。当用户点击它时,APP应该高亮显示文本中的日期,时间,位置。 打开SearchViewController.swift,找到书签按钮的实现: //MARK:Underlinedates,times,andlocations @IBActionfuncunderlineInterestingData(sender:AnyObject){ underlineAllDates() underlineAllTimes() underlineAllLocations() } 上面这个方法调用三个其他的辅助方法来给日期,时间,地点加下划线。如果你看向者三个辅助方法,你会看到他们都是空方法! 首先,填上每个方法的实现。用下面的内容替换他们: funcunderlineAllDates(){ ifletregex=NSRegularExpression.regularExpressionForDates(){ letmatches=matchesForRegularExpression(regex,inTextView:textView) highlightMatches(matches) } } funcunderlineAllTimes(){ ifletregex=NSRegularExpression.regularExpressionForTimes(){ letmatches=matchesForRegularExpression(regex,inTextView:textView) highlightMatches(matches) } } funcunderlineAllLocations(){ ifletregex=NSRegularExpression.regularExpressionForLocations(){ letmatches=matchesForRegularExpression(regex,inTextView:textView) highlightMatches(matches) } } 每个方法调用NSRegularExpression的一个工厂方法来创建一个合适的正则表达式。这些还不存在,但是这是一个方便的地方封装这个行为。这个方法找到匹配项,调用highlightMatches来给文本中的每个字符串着色和添加下划线。如果你有兴趣看它如何实现,查看它的实现。 现在填入正则表达式方法。打开RegexHelpers.swift并在NSRegularExpression的扩展内添加下面的内容。 classfuncregularExpressionForDates()->NSRegularExpression?{ letpattern="" returnNSRegularExpression(pattern:pattern,error:nil) } classfuncregularExpressionForTimes()->NSRegularExpression?{ letpattern="" returnNSRegularExpression(pattern:pattern,error:nil) } classfuncregularExpressionForLocations()->NSRegularExpression?{ letpattern="" returnNSRegularExpression(pattern:pattern,error:nil) } 现在你来实现这些模式,这是一些你需要的内容: Date Requirements:
Time requirements:
Location requirements:
你可以用playground试验一下。看是否能勾勒出需要的正则表达式! 这是三个简单的模式。用下面的内容替换regularExpressionForDates中的空模式 letpattern="(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})。意味着两个数字之后跟着一个-或/或.。之后再跟着两个数字,再跟着-或/或.,最后跟两个数字。 第二部分以(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 letpattern="d{1,2}s*(pm|am)" //Locations letpattern="[a-zA-Z]+[,]s*([A-Z]{2})" 作为练习,看看你能否根据上面的要求解释一下这个正则表达式模式。 编译并运行这个APP,点击Bookmark图标。你应该会看到高亮显示的日期,时间,位置,如下所示: 这个例子就到这儿了,你能明白为什么这个对于时间的正则表达式不能正确进行更通用的搜索吗?按现在的情况,它不会匹配3:15pm,它会匹配28pm。 下面是一个可行的答案,但是你自己先试一下。在附带的playground尾部看一下它的效果。 "(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])? 乍看起来,它看起来是一堆杂乱的字符串,但是,用你新发现的知识(下面的链接很有用),你会一步一步理解它,并成为正则表达式的高手!
假使你之前错过了这些链接,看一下这些我们为你准备的资源:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |