Swift抽出正規表現一致


175

正規表現パターンに一致する文字列から部分文字列を抽出したい。

だから私はこのようなものを探しています:

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

NSRangeと互換性がないRange<String.Index>ため、使用できませんtext.substringWithRange(...)

コードの行が多すぎずに、この簡単なことを迅速に行う方法はありますか?

回答:


313

matchesInString()メソッドがString最初の引数としてa を取る場合でも、は内部的にで動作しNSString、範囲パラメーターはNSString、Swift文字列の長さではなく、長さを使用して指定する必要があります。そうしないと、「フラグ」などの「拡張書記素クラスタ」では失敗します。

以下のようスイフト4(Xcodeの9)、スウィフト標準ライブラリとの間で変換する機能を提供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"]

注:は指定されたstringのサブストリングを参照するRange($0.range, in: text)!ため、強制アンラップは安全です。ただし、それを避けたい場合は、NSRangetext

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

代わりに。


(Swift 3以前の古い回答:)

したがって、指定されたSwift文字列をに変換してからNSString、範囲を抽出する必要があります。結果は自動的にSwift文字列配列に変換されます。

(Swift 1.2のコードは編集履歴にあります。)

Swift 2(Xcode 7.3.1):

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"]

9
あなたは私を狂気から救いました。冗談じゃない。どうもありがとうございます!
mitchkman、2015年

1
@MathijsSegers:Swift 1.2 / Xcode 6.3のコードを更新しました。知らせてくれてありがとうございます!
マーティンR

1
しかし、タグ間の文字列を検索したい場合はどうなりますか?regex101.com/r/cU6jX8/2のような同じ結果(一致情報)が必要です。どの正規表現パターンを提案しますか?
Peter Kreinz、2015

更新がスイフト1.2である、ないスウィフト2.コードは、スイフト2でコンパイルしない
PatrickNLT

1
ありがとう!正規表現で実際に()の間にあるものだけを抽出したい場合はどうなりますか?たとえば、「[0-9] {3}([0-9] {6})」では、最後の6つの数値のみを取得します。
p4bloch 2015

64

私の答えは与えられた答えの上に構築されますが、追加のサポートを追加することで正規表現マッチングをより堅牢にします:

  • 一致だけでなく、各一致のすべてのキャプチャグループも返します(下の例を参照)
  • 空の配列を返す代わりに、このソリューションはオプションの一致をサポートします
  • do/catchコンソールに出力しないことで回避しguard構造を利用します
  • 拡張子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")

スウィフト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")

スウィフト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
キャプチャグループについての良い考え。しかし、なぜ「ガード」Swifterが「do / catch」ではないのですか?
マーティンR

nshipster.com/guard-and-deferのように、Swift 2.0は、ifステートメントをネストするのではなく、早期復帰のスタイル[...]を推奨しているようだと私は同意します。同じことがネストされたdo / catchステートメントのIMHOにも当てはまります。
Lars Blumberg 2016年

try / catchはSwiftのネイティブエラー処理です。try?エラーメッセージではなく、呼び出しの結果のみに関心がある場合に使用できます。はい、guard try? ..大丈夫ですが、エラーを印刷したい場合は、doブロックが必要です。どちらもSwiftです。
マーティンR

3
私はあなたの素敵なスニペットにユニットテスト
neoneye /

1
私がこれを見るまで、@ MartinRの回答に基づいて自分で書きます。ありがとう!
Oritm

13

位置だけでなく文字列から部分文字列を抽出する場合(絵文字を含む実際の文字列)。次に、おそらく次の簡単なソリューションです。

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
これが私が欲しかったものです
カイルキム

1
いいね!Swift 3では少し調整が必要ですが、すばらしいです。
Jelle

@Jelle必要な調整は何ですか?私は
Swift

9

残念ながら、受け入れられた回答のソリューションは、残念ながらLinux用のSwift 3ではコンパイルできません。次に、修正されたバージョンを示します。

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上NSのSwiftは、Swiftネイティブの同等のものがないFoundationオブジェクトのプレフィックスを削除する必要があるようです。(Swiftの進化の提案#86を参照してください。)

  2. Linux上のSwiftでもoptionsRegularExpression初期化とmatchesメソッドの両方の引数を指定する必要があります。

  3. 何らかの理由で、LinuxのSwiftではa Stringをに強制変換することNSStringはできませんが、ソースを使用して新しいNSStringを初期化することStringはできます。

このバージョンはmacOS / XcodeのSwift 3でも動作しますが、名前のNSRegularExpression代わりにを使用する必要がありますRegularExpression


5

@ p4bloch一連のキャプチャ括弧から結果をキャプチャするrangeAtIndex(index)場合はNSTextCheckingResult、ではなくのメソッドを使用する必要があります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"]

この答えは私の一日を作りました。私は2時間かけて、グループをさらにキャプチャして、通常の表現を満たすことができるソリューションを探しました。
アフマド

これは機能しますが、範囲が見つからない場合はクラッシュします。このコードを変更して、関数が戻り[String?]for i in 0..<result.numberOfRangesブロック内に範囲!=の場合にのみ一致を追加するテストを追加するNSNotFound必要があります。それ以外の場合は、nilを追加する必要があります。参照:stackoverflow.com/a/31892241/2805570
stef

4

NSStringなしのSwift 4。

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)!])
        }
    }
}

上記の溶液には注意してください:NSMakeRange(0, self.count)ので、正しくないselfですString(= UTF8)ではなくNSString(= UTF16)。したがって、(他のソリューションで使用される)self.countと必ずしも同じではありませんnsString.length。範囲の計算をNSRange(self.startIndex..., in: self)
pd95

3

上記のソリューションのほとんどは、キャプチャグループを無視する結果として完全一致のみを提供します。例:^ \ 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
    }
}

あなたはそれが必要とする各結果を得るために、ちょうど最初の結果を希望している場合、これは素晴らしいですfor index in 0..<matches.count {周りlet lastRange... results.append(matchedString)}
ジェフ・

句のために、次のようになります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)) } }
CRE8IT

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)")
}

2

これは、一致した文字列の配列を返す非常にシンプルなソリューションです

スウィフト3。

internal func stringsMatching(regularExpressionPattern: String, options: NSRegularExpression.Options = []) -> [String] {
        guard let regex = try? NSRegularExpression(pattern: regularExpressionPattern, options: options) else {
            return []
        }

        let nsString = self as NSString
        let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))

        return results.map {
            nsString.substring(with: $0.range)
        }
    }

2

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, count)).map { match in
            (0..<match.numberOfRanges).map { match.range(at: $0).location == NSNotFound ? "" : nsString.substring(with: match.range(at: $0)) }
        } ?? []
    }
}

文字列の2次元配列を返します。

"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

0

Lars BlumbergのグループのキャプチャとSwift 4との完全な一致に対する彼の回答に大きく感謝します。また、正規表現が無効な場合にerror.localizedDescription応答が必要な人のために追加しました。

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        do {
            let regex = try NSRegularExpression(pattern: regex)
            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))
                        : ""
                }
            }
        } catch let error {
            print("invalid regex: \(error.localizedDescription)")
            return []
        }
    }
}

私がローカライズされた説明をエラーとして持つことは、エスケープで何がうまくいかなかったかを理解するのに役立ちました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.