Swift提取正则表达式匹配结果

230
我想从一个字符串中提取符合正则表达式模式的子串。
所以我正在寻找像这样的东西:
func matchesForRegexInText(regex: String!, text: String!) -> [String] {
   ???
}

这就是我所拥有的:

func matchesForRegexInText(regex: String!, text: String!) -> [String] {

    var regex = NSRegularExpression(pattern: regex, 
        options: nil, error: nil)

    var results = regex.matchesInString(text, 
        options: nil, range: NSMakeRange(0, countElements(text))) 
            as Array<NSTextCheckingResult>

    /// ???

    return ...
}

问题在于,matchesInString会返回一个NSTextCheckingResult数组,其中NSTextCheckingResult.range的类型为NSRange

NSRangeRange<String.Index>不兼容,因此它阻止了我使用text.substringWithRange(...)

你有没有什么简单的方法来在Swift中实现这个功能,而不需要太多的代码?


@Fattie,已经给出的答案中缺少什么? - Joakim Danielson
@JoakimDanielson:“Fattie想要奖励一个已有的答案。” - Nick
@Nick 啊,现在我明白了。看起来和文档解释的赏金有点不同。 - Joakim Danielson
@JoakimDanielson 是的,但它在原因页面上有描述。 - Nick
随意删除已解决的评论 - Fattie
16个回答

381
即使matchesInString()方法的第一个参数是String类型,它在内部使用的仍然是NSString。因此,范围参数必须使用NSString的长度而不是Swift字符串的长度来提供。否则,针对"扩展字符簇"(例如"flags")可能会失败。
Swift 4(Xcode 9)开始,Swift标准库提供了在Range<String.Index>NSRange之间进行转换的函数。
func matches(for regex: String, in text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex)
        let results = regex.matches(in: text,
                                    range: NSRange(text.startIndex..., in: text))
        return results.map {
            String(text[Range($0.range, in: text)!])
        }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

例子:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]

注意: 强制解包 Range($0.range, in: text)! 是安全的,因为 NSRange 引用了给定字符串 text 的子字符串。但是,如果你想要避免使用强制解包,则可以使用

        return results.flatMap {
            Range($0.range, in: text).map { String(text[$0]) }
        }

所以你应该将给定的Swift字符串转换为 NSString ,然后提取范围。结果将自动转换为Swift字符串数组。

(适用于Swift 1.2的代码可以在编辑历史中找到。)

Swift 2(Xcode 7.3.1):

改为使用NSString来处理给定的Swift字符串,然后提取范围。结果会自动转换为Swift字符串数组。

func matchesForRegexInText(regex: String, text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex, options: [])
        let nsString = text as NSString
        let results = regex.matchesInString(text,
                                            options: [], range: NSMakeRange(0, nsString.length))
        return results.map { nsString.substringWithRange($0.range)}
    } catch let error as NSError {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

例子:

let string = "€4€9"
let matches = matchesForRegexInText("[0-9]", text: string)
print(matches)
// ["4", "9"]
Swift 3 (Xcode 8)
func matches(for regex: String, in text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex)
        let nsString = text as NSString
        let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
        return results.map { nsString.substring(with: $0.range)}
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

例子:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]

11
你救了我,让我没有疯掉。不是开玩笑,非常感谢你! - mitchkman
1
@MathijsSegers:我已经更新了Swift 1.2/Xcode 6.3的代码。感谢您让我知道! - Martin R
1
但是如果我想在标签之间搜索字符串怎么办?我需要与此相同的结果(匹配信息),例如:https://regex101.com/r/cU6jX8/2。你会建议使用哪个正则表达式模式? - Peter Kreinz
@pnollet:奇怪。我刚刚再次确认“Swift 2”版本可以在Xcode 7 GM上编译和运行,而且按预期工作。你收到了什么错误信息? - Martin R
1
谢谢!如果你只想从正则表达式中提取()中的内容怎么办?例如,在“[0-9]{3}([0-9]{6})”中,我只想获取最后6个数字。 - p4bloch
显示剩余17条评论

75

我的回答基于给定的答案,但通过添加额外的支持使正则表达式匹配更加健壮:

  • 不仅返回匹配项,而且还返回每个匹配项的所有捕获组(见下面的示例)
  • 此解决方案支持可选匹配,而不是返回一个空数组
  • 通过不打印到控制台并使用guard语句,避免了do/catch
  • 作为将matchingStrings添加为String的扩展,增加了匹配字符串

Swift 4.2

//: Playground - noun: a place where people can play

import Foundation

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.range(at: $0).location != NSNotFound
                    ? nsString.substring(with: result.range(at: $0))
                    : ""
            }
        }
    }
}

"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]

"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]

"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here

// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")

Swift 3

//: Playground - noun: a place where people can play

import Foundation

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.rangeAt($0).location != NSNotFound
                    ? nsString.substring(with: result.rangeAt($0))
                    : ""
            }
        }
    }
}

"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]

"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]

"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here

// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")

Swift 2

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matchesInString(self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.rangeAtIndex($0).location != NSNotFound
                    ? nsString.substringWithRange(result.rangeAtIndex($0))
                    : ""
            }
        }
    }
}

1
关于捕获组的想法很好。但是为什么“guard”比“do/catch”更Swift? - Martin R
在Swift中,try/catch是本地错误处理。如果您只对调用的结果感兴趣而不关心可能的错误消息,则可以使用try?。所以,是的,guard try? ..是可以的,但是如果您想打印错误,则需要一个do-block。两种方法都是Swifty的。 - Martin R
我同意在示例中需要使用do/catch,如果你想在控制台上看到错误。由于我想要提供一个可以在生产代码中重复使用而无需修改的函数(对我来说print是一个不必要的副作用),因此guard try?变得更加Swift风格化了(如果您不需要副作用)。修改了答案以进行澄清。谢谢! - Lars Blumberg
3
我已经为您出色的代码片段添加了单元测试,https://gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522 - neoneye
2
直到看到这个,我才想写一个基于@MartinR答案的自己的东西。谢谢! - Oritm
显示剩余3条评论

37

用Swift 5返回所有匹配项和捕获组的最快方法

extension String {
    func match(_ regex: String) -> [[String]] {
        let nsString = self as NSString
        return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)).map { match in
            (0..<match.numberOfRanges).map { match.range(at: $0).location == NSNotFound ? "" : nsString.substring(with: match.range(at: $0)) }
        } ?? []
    }
}

返回一个二维字符串数组:

"prefix12suffix fix1su".match("fix([0-9]+)su")

返回...

[["fix12su", "12"], ["fix1su", "1"]]

// First element of sub-array is the match
// All subsequent elements are the capture groups

"options: []" 是否真的是必需的? - Higgs
我们怎么知道这是最快的方法呢? - undefined

15
如果您想从一个字符串中提取子字符串,不仅是位置,而是实际包括表情符号的字符串。那么,以下可能是一个更简单的解决方案。
extension String {
  func regex (pattern: String) -> [String] {
    do {
      let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions(rawValue: 0))
      let nsstr = self as NSString
      let all = NSRange(location: 0, length: nsstr.length)
      var matches : [String] = [String]()
      regex.enumerateMatchesInString(self, options: NSMatchingOptions(rawValue: 0), range: all) {
        (result : NSTextCheckingResult?, _, _) in
        if let r = result {
          let result = nsstr.substringWithRange(r.range) as String
          matches.append(result)
        }
      }
      return matches
    } catch {
      return [String]()
    }
  }
} 

示例用法:

"someText ⚽️ pig".regex("⚽️")

将返回以下内容:

["⚽️"]

请注意使用 "\w+" 可能会产生意外的 "" 结果

"someText ⚽️ pig".regex("\\w+")

将返回此字符串数组

["someText", "️", "pig"]

1
这就是我想要的。 - Kyle KIM
1
不错!需要对Swift 3进行一些调整,但很棒。 - Jelle
@Jelle 它需要什么调整?我正在使用 Swift 5.1.3。 - Peter Schorn

11

我发现被接受的答案不幸地在Swift 3 for Linux上无法编译。这是一个修改后的可行版本:

import Foundation

func matches(for regex: String, in text: String) -> [String] {
    do {
        let regex = try RegularExpression(pattern: regex, options: [])
        let nsString = NSString(string: text)
        let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
        return results.map { nsString.substring(with: $0.range) }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

主要差别如下:
  1. 在Linux上,Swift需要删除Foundation对象上的NS前缀,对于那些没有Swift本地等效项的对象。(参见Swift进化提案#86)。

  2. 在Linux上,Swift还需要为RegularExpression初始化和matches方法指定选项参数。

  3. 由于某种原因,在Linux的Swift中,将String强制转换为NSString不起作用,但使用String作为源初始化一个新的NSString却可以。

这个版本也适用于macOS/Xcode上的Swift 3,唯一的例外是你必须使用名称NSRegularExpression而不是RegularExpression。


11

iOS 16 更新: RegexRegexBuilder ‍♀️

Xcode 以前使用 Apple 的 NSRegularExpression 支持正则表达式。Swift API 冗长且难以正确使用,因此 Apple 在今年发布了 Regex 字面量支持和RegexBuilder。由Regex类型使用的正则表达式语法与NSRegularExpression相同,即 ICU Unicode 规范。

API 简化的方向是整理 iOS 16 / macOS 13 中复杂的基于字符串范围的解析逻辑,并提高性能。

使用字面量的另一个优点是,在使用无效的 RegEx 语法时我们会在编译时获取错误提示:Cannot parse regular expression...,并附带有关RegEx错误的清晰描述。享受吧!

在 Swift 5.7 中使用 RegEx 字面量

func parseLine(_ line: Substring) throws -> MailmapEntry {

    let regex = /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/

    guard let match = line.prefixMatch(of: regex) else {
        throw MailmapError.badLine
    }

    return MailmapEntry(name: match.1, email: match.2)
}

我们可以使用以下方式进行匹配:

  1. firstMatch(of:):返回此集合中正则表达式的第一个匹配项,其中正则表达式由给定闭包(RegEx文本)创建。

  2. prefixMatch(of:):如果此字符串以给定正则表达式开头,则返回匹配项。

  3. wholeMatch(of:):完全匹配正则表达式,其中正则表达式由给定闭包(RegEx文本)创建。

  4. matches(of:):返回包含所有非重叠正则表达式匹配项的集合,该正则表达式由给定闭包(RegEx文本)创建。

我已经在上面链接了文档。新的RegEx字面量语法有多个新的API,例如trimmingPrefix()contains()等等,因此我鼓励进一步探索文档以获取更细致的用例。

以上方法也有相应的语法,我们可以在Regex字面量本身上调用prefixMatch(in:)并传入要搜索的字符串。我更喜欢上面的语法,但您可以选择自己喜欢的语法。

示例代码:

let aOrB = /[ab]+/

if let stringMatch = try aOrB.firstMatch(in: "The year is 2022; last year was 2021.") {
    print(stringMatch.0)
} else {
    print("No match.")
}
// prints "a"

Swift 5.7中的RegexBuilder

RegexBuilder是苹果发布的一个新API,旨在使Swift中的正则表达式代码更易于编写。如果我们想要更好的可读性,我们可以使用RegexBuilder将上面的正则表达式文字/\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/转换为更具声明性的形式。

请注意,我们可以在RegexBuilder中使用原始字符串,并且如果我们想要平衡可读性和简洁性,我们还可以在构建器中插入Regex Literals。

import RegexBuilder

let regex = Regex {
    ZeroOrMore(.horizontalWhitespace)
    Optionally {
        Capture(OneOrMore(.noneOf("<#")))
    }
        .repetitionBehavior(.reluctant)
    ZeroOrMore(.horizontalWhitespace)
    "<"
    Capture(OneOrMore(.noneOf(">#")))
    ">"
    ZeroOrMore(.horizontalWhitespace)
    /#|\Z/
}

正则表达式字面量/#|\Z/等同于:

ChoiceOf {
   "#"
   Anchor.endOfSubjectBeforeNewline
}

可组合的RegexComponent

RegexBuilder语法在可组合性方面与SwiftUI类似,因为我们可以在其他RegexComponent中重复使用RegexComponent

struct MailmapLine: RegexComponent {
    @RegexComponentBuilder
    var regex: Regex<(Substring, Substring?, Substring)> {
        ZeroOrMore(.horizontalWhitespace)
        Optionally {
            Capture(OneOrMore(.noneOf("<#")))
        }
            .repetitionBehavior(.reluctant)
        ZeroOrMore(.horizontalWhitespace)
        "<"
        Capture(OneOrMore(.noneOf(">#")))
        ">"
        ZeroOrMore(.horizontalWhitespace)
        ChoiceOf {
           "#"
            Anchor.endOfSubjectBeforeNewline
        }
    }
}

来源:部分代码来自WWDC 2022视频《Swift的新特性》。


这段代码直接摘自 WWDC 2022 视频《Swift 的新特性》,你应该在回答中明确说明,参见 https://stackoverflow.com/help/referencing。 - Joakim Danielson

7

使用Swift 4而不是NSString。

extension String {
    func matches(regex: String) -> [String] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: [.caseInsensitive]) else { return [] }
        let matches  = regex.matches(in: self, options: [], range: NSMakeRange(0, self.count))
        return matches.map { match in
            return String(self[Range(match.range, in: self)!])
        }
    }
}

7
请注意上述解决方案:NSMakeRange(0, self.count)是不正确的,因为self是一个String(=UTF8)而不是一个NSString(=UTF16)。因此,self.count不一定与其他解决方案中使用的nsString.length相同。您可以将范围计算替换为NSRange(self.startIndex..., in: self) - pd95

5

@p4bloch如果你想要捕获一系列捕获括号中的结果,那么你需要使用NSTextCheckingResultrangeAtIndex(index)方法,而不是range。这里是@MartinR针对Swift2的方法(从以上内容中适应了捕获括号)。在返回的数组中,第一个结果[0]是整个捕获,然后单独的捕获组从[1]开始。我注释掉了map操作(这样更容易看到我所做的更改),并用嵌套循环替换它。

func matches(for regex: String!, in text: String!) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex, options: [])
        let nsString = text as NSString
        let results = regex.matchesInString(text, options: [], range: NSMakeRange(0, nsString.length))
        var match = [String]()
        for result in results {
            for i in 0..<result.numberOfRanges {
                match.append(nsString.substringWithRange( result.rangeAtIndex(i) ))
            }
        }
        return match
        //return results.map { nsString.substringWithRange( $0.range )} //rangeAtIndex(0)
    } catch let error as NSError {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

一个示例用例可能是,比如你想要分割一个字符串的title year,例如 "Finding Dory 2016",你可以这样做:
print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]

这个答案让我非常开心。我花了两个小时搜索一个可以满足正则表达式并额外捕获组的解决方案。 - Ahmad
这段代码可以工作,但如果没有找到任何范围,它将会崩溃。我修改了这段代码,使得函数返回 [String?],在 for i in 0..<result.numberOfRanges 块中,你需要添加一个测试,只有当范围不等于 NSNotFound 时才追加匹配,否则应该追加 nil。参见:https://stackoverflow.com/a/31892241/2805570 - stef

4

以上大多数解决方案只返回完整匹配的结果,忽略了捕获组,例如:^\d+\s+(\d+)

要如预期地获取捕获组匹配,你需要像这样使用(Swift4):

public extension String {
    public func capturedGroups(withRegex pattern: String) -> [String] {
        var results = [String]()

        var regex: NSRegularExpression
        do {
            regex = try NSRegularExpression(pattern: pattern, options: [])
        } catch {
            return results
        }
        let matches = regex.matches(in: self, options: [], range: NSRange(location:0, length: self.count))

        guard let match = matches.first else { return results }

        let lastRangeIndex = match.numberOfRanges - 1
        guard lastRangeIndex >= 1 else { return results }

        for i in 1...lastRangeIndex {
            let capturedGroupIndex = match.range(at: i)
            let matchedString = (self as NSString).substring(with: capturedGroupIndex)
            results.append(matchedString)
        }

        return results
    }
}

如果你只想要第一个结果,那么这很棒。如果你想获取每个结果,需要在 let lastRange... results.append(matchedString) 周围加上 for index in 0..<matches.count { - Geoff
for循环应该像这样:`for i in 1...lastRangeIndex { let capturedGroupIndex = match.range(at: i) if capturedGroupIndex.location != NSNotFound { let matchedString = (self as NSString).substring(with: capturedGroupIndex) results.append(matchedString.trimmingCharacters(in: .whitespaces)) } }` - DonBaron

2

以下是我所做的,希望它能为您提供有关Swift如何工作的新视角。

在下面的示例中,我将获取[]之间的任何字符串。

var sample = "this is an [hello] amazing [world]"

var regex = NSRegularExpression(pattern: "\\[.+?\\]"
, options: NSRegularExpressionOptions.CaseInsensitive 
, error: nil)

var matches = regex?.matchesInString(sample, options: nil
, range: NSMakeRange(0, countElements(sample))) as Array<NSTextCheckingResult>

for match in matches {
   let r = (sample as NSString).substringWithRange(match.range)//cast to NSString is required to match range format.
    println("found= \(r)")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接