Swift中的正则表达式

从更一般的意义上讲,正则表达式是我们在学校中学到的东西,但是它们经常出现在编程任务中,尤其是在处理文本时。 关于Stackoverflow有一个问题,询问有关识别段落中文本的子集的问题。 问题的前提是要预览新闻文章,其结尾包含整篇文章中剩​​余的字符数,例如[+6025个字符]。 目的是用显示“更多”的超链接替换该字符计数文本?

我首先想到的是,我们需要使用正则表达式在方括号之间找到文本。 然后,我们将文本替换为所需的文本,最后,我们需要添加文本属性,尤其是超链接。 哎呀,这似乎有很多事情要做,所以让我们一次一步。

Swift没有正则表达式类,因此您认为您需要使用来自objective-c世界的NSRegularExpressions ,但是,这是错误的。 Swift字符串会自动桥接到NSString ,这意味着String具有一些不错的功能,使我们能够轻松地(通过正则表达式)对String执行模式匹配。 最好的例子是NSString类的range(of:options:)方法。 该方法在字符串中搜索字符串,但是选项使它真正强大。 有少数可用的选项,例如不区分大小写,向后且正则表达式感兴趣。 这些选项是不言自明的,也许是正则表达式除外,它会将搜索字符串视为“与ICU兼容的正则表达式 ”。 太好了,我们只需要定义一个正则表达式并将其传递给方法range(of:options:) ,它将告诉我们是否存在匹配项以及找到文本的Range 。 现在,我们只需要创建一个用于输入的正则表达式即可。

定义正则表达式完全是另一回事。 如果您曾经在SO或现实世界中看过任何东西,它们很快就会变得非常复杂。 regexr.com是一个很好的参考站点,您可以在其中测试表达式并获得有关构造表达式的详细信息。 让我们花一点时间描述一下我们如何构造正则表达式模式。 我们将在这里采用介绍性方法,仅介绍一些基础知识。

如果我们要在字符串中搜索字母a ,则正则表达式将只是一个仅包含“ a”的模式。 同样,如果我们要按特定顺序搜索特定字符(例如cat ,则模式将为“ cat”。 这一切都很好,但是出于我们的目的,我们需要找到方括号[ ]以及这些方括号之间的所有文本。 我们需要一种更强大的模式。

输入元字符,即在正则表达式模式中具有特殊含义和功能的字符。 一些最重要的元字符是点. ,问号? ,星号* ,脱字号^ ,括号, ()和方括号[] 。 让我们快速回顾一下这些字符在正则表达式中的含义。

. 表示基本上匹配任何字符,但换行符除外。 这对于我们的目的将是有用的,因为这就是我们想要放在方括号之间的任何字符。 方括号[]表示要匹配一个字符类,这表示要在方括号之间匹配几个字符中的一个。 例如,将您的模式定义为[if] ,它将在输入“ Swift”匹配i找到匹配项( if将是匹配项),以及在“ wit”或“ half”上匹配的项if分别。 您通常会看到这与字母[az]或大写[AZ]匹配,对于数字字符则为[0–9] 。 我们可以进一步将它们组合起来,以在输入中找到更复杂的模式,例如[a-zA-Z] 。 我们可以将其视为拉丁字母中的任何单个字符,但不能将其视为其他任何字符。

 让模式=“ [a-zA-Z]” 
  let text =“如果有时间,那是最好的” 
让结果= text.range(pattern,options:.regularExpression)
结果=匹配索引为零,长度为1
 让文字=“ 555–123–4567” 
让结果= text.range(pattern,options:.regularExpression)
result = nil //不匹配
  let pattern =“ [az] at” 
让文字=“猫”
让结果= text.range(pattern,options:.regularExpression)
结果=匹配索引为零,长度为3

回顾上面的示例,我们首先看到如果第一个字符是任何字母,就会发生匹配。 在第二个中,我们将没有匹配项(结果为nil )。 在最后一种情况下,在任何字符串上都将发生匹配,该字符串以任何字母开头,后跟精确的at ,例如catbathatmat ,您就会明白。 需要注意的一件重要事情是,如果要将这些元字符中的任何一个用作正则表达式中的文字(在我们的例子中为括号),则需要使用反斜杠将其转义。 那么我们的[+6025 characters]模式将如何匹配? 我们只需要定义一个模式,该模式包括一个开括号,然后是任何字符,然后是一个右括号。 看起来就像这样let pattern = “\\[\\]” 。 注意,我们用两个斜杠\\每个括号进行了转义,因此它们将完全匹配。 在Swift中,我们需要使用两个斜杠对文字进行转义。 我们需要转义转义字符,这是由于Swift中的字符串插值。 但是,我们需要更多的时间来使我们的正则表达式匹配[+6025 characters]

在继续进行示例之前,让我们看一下其他有用的元字符。 问号? 表示字符是可选的。 因此,对于可选字符,我们将使用问号。 如果我们想检查字符串中是否包含颜色一词(包括欧洲变体),我们会写一个类似

 让pattern =“ colo?r” 
让文字=“颜色”
让结果= text.range(pattern,options:.regularExpression)
结果=匹配索引为零,长度为5
 让文字=“颜色” 
让结果= text.range(pattern,options:.regularExpression)
结果=匹配索引为零,长度为5

星号*表示匹配零个或多个前一个字符( +表示一个或多个)。 星号是棘手的,应谨慎使用,因为如果使用不正确,可能会导致匹配为空。

 让模式=“ [0-9] *” 
let text =“ 123 Main Street”
让结果= text.range(pattern,options:.regularExpression)
结果=匹配索引为零,长度为3
 让文字=“大街” 
让结果= text.range(pattern,options:.regularExpression)
result =匹配索引0,长度为0 // UNEXPECTED!
 让模式=“ [0-9] +” 
让文字=“大街”
让结果= text.range(pattern,options:.regularExpression)
结果=零

我们可以在上面的示例中看到,在第一种情况下使用星号可以按我们预期的那样工作,找到123。在第二种情况下,我们可能不希望它会找到匹配项,尽管长度匹配为零。 这样做是因为它是零个或多个数字字符。 在第三种情况下,结果产生nil ,因为输入文本中没有这样的范围。 该模式需要一个或多个数字字符。 经验教训,请谨慎使用* ,您可能想要的是+

沿着这行,花括号{}是量词,它们与指定数量的前一个字符匹配(例如{3}恰好是3,而{1,3}是1到3)。 表示计数。 如果我们尝试匹配电话号码,则可以将其分解为三位数字的任何一组[0–9] ,后接破折号- ,然后再将三位数字的任何一组后接破折号,最后再将其分为四组数字[0–9] 。 因此,要匹配美国电话号码,我们可能会写类似以下内容:

let pattern = “[0–9]{3}-[0–9]{3}-[0–9]{4}”

我们需要一个更复杂的模式来考虑国家/地区代码,前三位数字的括号,空格等,但这应该使您了解如何一起使用量词和类。 匹配电话号码的表达式很复杂,一个可能的版本是^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ 。 这里没有一些多余的字符(例如, \d表示数字, \s空格),因为它超出了本介绍的范围。

最后,让我们看一下括号()和插入符号^ 。 括号是分组运算符。 它将多个字符组合在一起,然后可用于提取子字符串。 括号内的任何字符都将匹配在一起。 插入符号^可以表示几件事情,例如匹配字符串的开头,行的开头,或者也可以是否定字符。

  let pattern =“ ^ abc” 
让文字=“ abc街”
让范围= text.range(of:模式,选项:.regularExpression)
结果=匹配索引0,长度为3
 让inputText =“ 123 abc街” 
让范围= text.range(of:模式,选项:.regularExpression)
结果=零
  let pattern =“ [^ abc]” 
让文字=“ abc街”
让范围= text.range(of:模式,选项:.regularExpression)
result =在索引3处匹配,长度为1 //第一个空格
  let pattern =“(abc)” 
让文字=“主要abc街”
让范围= text.range(of:模式,选项:.regularExpression)
结果=匹配索引5,长度为3

让我们快速回顾一下上面的示例,插入符号用于检查字符串是否以abc开头。在第一种情况下,成功,而在第二种情况下,失败。 然后,我们将其用于排除abc类。 括号用于对字母abc进行分组,并查找字符串中是否进行了分组。

现在,我们了解了更多有关如何构造正则表达式的知识,让我们回到当前的问题上。 我们如何做一个正则表达式来查找类似[+6025 characters]东西? 对range(of:options:)的调用将如下所示:

  let text =“这是一些我们可以替换最后一部分的文本... [+6025个字符]” 
让模式=“ \\ [。* \\]”
让范围= text.range(of:模式,选项:.regularExpression)
结果=匹配索引56,长度为18

我们的模式很简单,匹配右方括号,方括号内任意数量的字符,然后是右方括号。 太好了,现在我们有了表达式,需要找出文本是否包含替换条件。 因此,现在我们如何去替换文本? 很高兴您问,我创建了两个示例来说明如何替换文本并添加超链接,一个示例使用Swift的String ,另一个使用NSRegularExpression 。 让我们首先看一下Swift String

我们定义模式并使用range(of:options:)函数获取要替换的文本的位置。 然后,我们调用文本替换方法replacingCharacters(in:with:)以“阅读更多”文本替换找到的内容。 这些方法实际上都是我们通过免费桥接获得的NSString方法。 然后,我们获得了修改后的文本的范围,以便我们可以添加链接属性。 Swift没有属性字符串,因此我们需要使用NSMutableAttributedString添加链接。 这还不错,大约有8行代码。 如果改用NSRegularExpression会更好吗?

在这里,我们创建NSRegularExpression ,使用方法stringByReplacingMatches(in:options:range:withTemplate:)将目标文本替换为“阅读更多”,然后再次找到范围(这是一个NSRange因此无需转换),并添加链接属性。 它有点短,但我认为它读起来容易一些。 如果我们不添加超链接,则可能需要使用range(of:options:)更好的解决方案,具体取决于您需要对范围进行什么操作。

希望您喜欢学习更多有关正则表达式的知识,以及我们如何快速轻松地使用它们。

资料来源

高级正则表达式
Regexr.com
正则表达式
Swift和正则表达式:Swift
ICU用户指南