[译]Swift中的正则表达式

432 阅读8分钟

原文地址 Regular Expressions in Swift

像太平洋西北部的其他人一样,我们周末被雪困住了。为了打发时间,我们决定打开我们的棋盘游戏:卡尔卡松、马奇·库罗、电网、大流行病;我们有很多优秀的选择。但是下午我们被关在家里,我们决定了一个经典:线索

我,我是《独领风骚》的狂热粉丝——是的,我就是这么称呼它的。因为尽管我在美国出生和长大,但我坚持用它的真名来称呼它,因为在美国,这款游戏的销售和营销完全是以“线索”的名字进行的。(否则,我们如何参考**1985年的电影改编?**)

唉,我无情的迂腐经常导致我错过邀请去玩。如果有人问:

var invitation = "Hey, would you like to play Clue?"invitation.contains("Cluedo") // false

……我不知道他们在说什么。只要他们费心措辞得当,他们的意图是毫无疑问的:

invitation = "Fancy a game of Cluedo™?"invitation.contains("Cluedo") // true

当然,正则表达式将允许我放松我的严格标准。我可以听/Clue吗?™?/,永远不要错过另一个邀请。

但是谁会费心在斯威夫特中找出正则表达式呢?

好吧,削尖你的铅笔,收集你的侦探笔记,预热你的六面骰子,因为本周在NS Hipster上,我们正在破解被称为NS正则表达式的笨重类的案例。

谁在Swift中杀死了正则表达式?

我有一个建议:

在API中,它是具有繁琐可用性的NS正则表达式。

在任何其他语言中,正则表达式都是可以在一行中使用的。

  • 需要用一个词代替另一个词吗?

  • Boom**:**正则表达式。

  • 需要从模板字符串中提取值吗?

  • Boom**:**正则表达式。

  • 需要解析XML?

  • Boom**:**正则表达式实际上,在这种情况下,您应该真正使用XML解析器

但是在Swift中,您必须经历初始化NS Reg rar Express对象和从String范围到NS Range值来回转换的麻烦。这完全是一种拖累。

好消息是:

  1. 在Swift中使用正则表达式不需要NS正则表达式。

  2. 最近在Swift 4和5中添加的功能使您在需要时使用NS正则表达式变得非常非常好。

让我们依次询问以下每一点:

没有NS正则表达式

你可能会惊讶地发现,事实上,你可以在Swift一行中使用正则表达式——你只需要完全绕过NS正则表达式。

匹配字符串与模式

导入Foundation框架时,Swift String类型自动获得对NS String实例方法和初始化器的访问权限。其中包括range(of:选项: range: locale:),它查找并返回指定字符串的第一个范围。

通常情况下,这执行按书搜索子字符串的操作*。Meh.*

但是,如果您传递。正则表达式选项,字符串参数将作为模式匹配。尤里卡!

让我们利用这个鲜为人知的功能,将我们的“线索”意识转向*“美国*”环境。

import Foundationlet invitation = "Fancy a game of Cluedo™?"invitation.range(of: #"\bClue(do)?™?\b"#,                 options: .regularExpression) != nil // true

如果模式与指定的字符串匹配,则该方法返回Range<String。索引>对象。因此,检查非零值会告诉我们是否发生了匹配。

方法本身为选项、范围和区域设置参数提供默认参数;默认情况下,它对当前区域设置中的整个字符串执行本地化的无保留搜索。

在正则表达式中,?运算符匹配前面的字符或组零次或一次。我们在模式中使用它,使“线索”中的“-do”成为可选的(既能容纳美式拼写,也能容纳正确的拼写),并允许任何希望一本正经的人使用商标符号(™)。

如果当前位置是单词边界,则\b元字符匹配,这发生在单词(\w)和非单词(\W)字符之间。将我们的模式固定在单词边界上以匹配,可以防止“伪线索”等误报。

这解决了我们错过邀请的问题。下一个问题是如何以同样的方式回应。

搜索和检索匹配

我们实际上可以使用返回值来查看匹配的字符串,而不仅仅是检查非零值。

import Foundationfunc respond(to invitation: String) {  if let range = invitation.range(of: #"\bClue(do)?™?\b"#,                                  options: .regularExpression) {    switch invitation[range] {    case "Cluedo":        print("I'd be delighted to play!")    case "Clue":        print("Did you mean Cluedo? If so, then yes!")    default:        fatalError("(Wait... did I mess up my regular expression?)")    }  } else {    print("Still waiting for an invitation to play Cluedo.")  }}

方便的是,range(of:...) 方法返回的范围可以插入下标,以获得匹配范围的子字符串。

查找和替换匹配

一旦我们确定游戏已经开始,下一步就是阅读说明。尽管它相对简单,但玩家经常忘记《线索》中的重要规则,比如需要在房间里才能提出建议。)

自然,我们播放原版的英国版。但作为对美国玩家帮助,我会不厌其烦地立即将规则本地化。例如,最初版本中受害者的名字是“布莱克博士”,但美国是“博迪先生”。

我们使用替换事件(of: with:选项:)方法来自动化这个过程——再次传递。正则表达式选项。

import Foundationlet instructions = """The object is to solve by means of elimination and deductionthe problem of the mysterious murder of Dr. Black."""instructions.replacingOccurrences(    of: #"(Dr\.|Doctor) Black"#,    with: "Mr. Boddy",    options: .regularExpression)

正则表达式

我们可以使用range(of:选项: range: locale:)方法实现的目标是有限制的,并且替换出现(of: with:选项:)方法实现的目标也是有限制的。

具体来说,如果您想在字符串中多次匹配模式或从捕获组中提取值,您需要使用NS正则表达式。

枚举具有位置捕获组的匹配

正则表达式可以在字符串上匹配其模式一次或多次。在每个匹配中,可能有一个或多个捕获组,它们由正则表达式模式中的括号包围指定。

例如,假设您想使用正则表达式来确定需要多少玩家来玩独领风骚:

import Foundationlet description = """Cluedo is a game of skill for 2-6 players."""let pattern = #"(\d+)[ \p{Pd}](\d+) players"#let regex = try NSRegularExpression(pattern: pattern, options: [])

该模式包括一个或多个数字的两个捕获组,分别由+运算符和\d元字符表示。

在它们之间,我们在包含空格和**Unicode通用类别**Pd(标点符号,破折号)中的任何字符的集合上进行匹配。这使我们能够匹配连字符/减去 (-), en dash (–), em dash (—), 或任何其他我们可能遇到的外来印刷标记。

我们可以使用enumerate Matches(in: ption: range)方法尝试每个匹配,直到找到一个具有三个范围(整个匹配和两个捕获组)的匹配,其捕获的值可用于初始化有效范围。在所有这些过程中,我们使用新的(-ish)NS Range(: in:)和Range(: in:)初始化器在String和NS String索引范围之间进行转换。一旦找到这样的匹配,我们将第三个闭包参数(指向布尔值的指针)设置为true,作为告诉枚举停止的一种方式。

var playerRange: ClosedRange<Int>?let nsrange = NSRange(description.startIndex..<description.endIndex,                      in: description)regex.enumerateMatches(in: description,                       options: [],                       range: nsrange) { (match, _, stop) in    guard let match = match else { return }    if match.numberOfRanges == 3,       let firstCaptureRange = Range(match.range(at: 1),                                     in: description),       let secondCaptureRange = Range(match.range(at: 2),                                      in: description),       let lowerBound = Int(description[firstCaptureRange]),       let upperBound = Int(description[secondCaptureRange]),       lowerBound > 0 && lowerBound < upperBound    {        playerRange = lowerBound...upperBound        stop.pointee = true    }}print(playerRange!)// Prints "2...6"

通过调用匹配对象上的range(at:)方法,可以按位置访问每个捕获组。

*叹息。什么呀?*不,我们喜欢我们想出的解决方案——尽管冗长。只是……天哪,如果我们能单独玩独领风骚不是很好吗?

使用命名捕获组匹配多行模式

唯一一件让《线索》成为严格意义上的多人游戏的事情是,你需要某种方法来测试一个理论,而不会向自己透露答案。

如果我们想写一个程序来检查它,而不破坏任何东西,第一步是将一个建议解析成它的组成部分:嫌疑人、位置和武器

let suggestion = """I suspect it was Professor Plum, \in the Dining Room, \with the Candlestick."""

当编写复杂的正则表达式时,准确地知道您的平台支持哪些功能会有所帮助。在Swift的例子中,NS**正则表达式是一个围绕着正则表达式引擎**的包装器,它让我们可以做一些非常好的事情:

let pattern = #"""(?xi)(?<suspect>    ((Miss|Ms\.) \h Scarlett?) |    ((Colonel | Col\.) \h Mustard) |    ((Reverend | Mr\.) \h Green) |    (Mrs\. \h Peacock) |    ((Professor | Prof\.) \h Plum) |    ((Mrs\. \h White) | ((Doctor | Dr\.) \h Orchid))),?(?-x: in the )(?<location>    Kitchen        | Ballroom | Conservatory |    Dining \h Room      |       Library      |    Lounge         | Hall     | Study),?(?-x: with the )(?<weapon>      Candlestick    | Knife    | (Lead(en)?\h)? Pipe    | Revolver    | Rope    | Wrench)"""#let regex = try NSRegularExpression(pattern: pattern, options: [])

首先,用多行原始字符串文字声明模式在可读性方面是一个巨大的胜利。这与这些组中的x和i标志相结合,允许我们使用空白来组织我们的表达式,使其更容易理解。

另一个细节是这个模式如何使用命名的捕获组(由(?)),而不是前面示例中的标准位置捕获组。这样做允许我们通过调用匹配对象上的range(with Name:)方法按名称访问组。

除了更古怪的策略之外,我们还能承受地区差异,包括“斯嘉丽小姐”的拼写、“/牧师格林先生”的头衔,以及2016年后标准版中怀特夫人被兰花博士取代。

let nsrange = NSRange(suggestion.startIndex..<suggestion.endIndex,                      in: suggestion)if let match = regex.firstMatch(in: suggestion,                                options: [],                                range: nsrange){    for component in ["suspect", "location", "weapon"] {        let nsrange = match.range(withName: component)        if nsrange.location != NSNotFound,            let range = Range(nsrange, in: suggestion)        {            print("\(component): \(suggestion[range])")        }    }}// Prints:// "suspect: Professor Plum"// "location: Dining Room"// "weapon: Candlestick"

正则表达式是处理文本的强大工具,但如何在Swift中使用它们通常是个谜。我们希望这篇文章能帮助您找到解决方案。