Ekstrak pencocokan regex Swift

175

Saya ingin mengekstrak substring dari string yang cocok dengan pola regex.

Jadi saya mencari sesuatu seperti ini:

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

Jadi inilah yang saya miliki:

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 ...
}

Masalahnya adalah, itu matchesInStringmemberikan saya sebuah array NSTextCheckingResult, di mana NSTextCheckingResult.rangetipe NSRange.

NSRangetidak kompatibel dengan Range<String.Index>, jadi itu mencegah saya menggunakantext.substringWithRange(...)

Adakah cara untuk mencapai hal sederhana ini dengan cepat tanpa terlalu banyak baris kode?

mitchkman
sumber

Jawaban:

313

Sekalipun matchesInString()metode tersebut menggunakan Stringargumen pertama, ia bekerja secara internal NSString, dan parameter rentang harus diberikan menggunakan NSStringpanjang dan bukan sebagai panjang string Swift. Kalau tidak, itu akan gagal untuk "cluster grapheme diperpanjang" seperti "bendera".

Pada Swift 4 (Xcode 9), perpustakaan standar Swift menyediakan fungsi untuk mengkonversi antara Range<String.Index> dan 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 []
    }
}

Contoh:

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

Catatan: Bukaan paksa Range($0.range, in: text)!aman karena NSRangemengacu pada substring dari string yang diberikan text. Namun, jika Anda ingin menghindarinya maka gunakan

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

sebagai gantinya.


(Jawaban yang lebih lama untuk Swift 3 dan sebelumnya :)

Jadi, Anda harus mengubah string Swift yang diberikan ke NSStringdan kemudian mengekstrak rentang. Hasilnya akan dikonversi ke array string Swift secara otomatis.

(Kode untuk Swift 1.2 dapat ditemukan di riwayat edit.)

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 []
    }
}

Contoh:

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 []
    }
}

Contoh:

let string = "πŸ‡©πŸ‡ͺ€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R
sumber
9
Anda menyelamatkan saya dari menjadi gila. Tidak bercanda. Terima kasih banyak!
mitchkman
1
@MathijsSegers: Saya telah memperbarui kode untuk Swift 1.2 / Xcode 6.3. Terima kasih telah memberi tahu saya!
Martin R
1
tetapi bagaimana jika saya ingin mencari string di antara tag? Saya membutuhkan hasil yang sama (informasi pertandingan) seperti: regex101.com/r/cU6jX8/2 . pola regex mana yang akan Anda sarankan?
Peter Kreinz
Pembaruan untuk Swift 1.2, bukan Swift 2. Kode tidak dikompilasi dengan Swift 2.
PatrickNLT
1
Terima kasih! Bagaimana jika Anda hanya ingin mengekstrak apa yang sebenarnya antara () di regex? Misalnya, dalam "[0-9] {3} ([0-9] {6})" Saya hanya ingin mendapatkan 6 angka terakhir.
p4bloch
64

Jawaban saya dibangun di atas jawaban yang diberikan tetapi membuat pencocokan regex lebih kuat dengan menambahkan dukungan tambahan:

  • Mengembalikan tidak hanya kecocokan tetapi mengembalikan juga semua grup penangkap untuk setiap kecocokan (lihat contoh di bawah)
  • Alih-alih mengembalikan array kosong, solusi ini mendukung kecocokan opsional
  • Hindari do/catchdengan tidak mencetak ke konsol dan memanfaatkan guardkonstruk
  • Tambahkan matchingStringssebagai ekstensi keString

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

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

Cepat 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))
                    : ""
            }
        }
    }
}
Lars Blumberg
sumber
1
Ide bagus tentang kelompok penangkap. Tapi mengapa "penjaga" lebih cepat daripada "lakukan / tangkap" ??
Martin R
Saya setuju dengan orang-orang seperti nshipster.com/guard-and-defer yang mengatakan Swift 2.0 tampaknya mendorong gaya pengembalian awal [...] daripada pernyataan bersarang jika . Hal yang sama berlaku untuk pernyataan do / catch bersarang IMHO.
Lars Blumberg
try / catch adalah penanganan kesalahan asli di Swift. try?dapat digunakan jika Anda hanya tertarik pada hasil panggilan, bukan pada pesan kesalahan yang mungkin. Jadi ya, guard try? ..baik-baik saja, tetapi jika Anda ingin mencetak kesalahan maka Anda perlu melakukan blokir. Kedua cara itu Swifty.
Martin R
3
Saya telah menambahkan unittests ke cuplikan Anda yang bagus, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
Akan menulis sendiri berdasarkan jawaban @MartinR sampai saya melihat ini. Terima kasih!
Oritm
13

Jika Anda ingin mengekstraksi substring dari sebuah String, bukan hanya posisinya, (tetapi String yang sebenarnya termasuk emoji). Lalu, berikut ini mungkin solusi yang lebih sederhana.

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

Contoh penggunaan:

"someText πŸ‘ΏπŸ…πŸ‘Ώβš½οΈ pig".regex("πŸ‘Ώβš½οΈ")

Akan mengembalikan yang berikut:

["πŸ‘Ώβš½οΈ"]

Catatan menggunakan "\ w +" dapat menghasilkan yang tidak terduga ""

"someText πŸ‘ΏπŸ…πŸ‘Ώβš½οΈ pig".regex("\\w+")

Akan mengembalikan array String ini

["someText", "️", "pig"]
Mike Chirico
sumber
1
Inilah yang saya inginkan
Kyle KIM
1
Bagus! Perlu sedikit penyesuaian untuk Swift 3, tapi ini hebat.
Jelle
@ Jean apa penyesuaian yang dibutuhkan? Saya menggunakan cepat 5.1.3
Peter Schorn
9

Saya menemukan bahwa solusi jawaban yang diterima sayangnya tidak dapat dikompilasi pada Swift 3 untuk Linux. Inilah versi yang dimodifikasi, yang artinya:

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 []
    }
}

Perbedaan utama adalah:

  1. Swift di Linux tampaknya perlu menjatuhkan NSawalan pada objek Foundation yang tidak ada padanan asli Swift. (Lihat proposal evolusi Swift # 86. )

  2. Swift di Linux juga membutuhkan spesifikasi optionsargumen untuk RegularExpressioninisialisasi dan matchesmetode.

  3. Untuk beberapa alasan, memaksa Stringmenjadi NSStringtidak berfungsi di Swift di Linux tetapi menginisialisasi yang baru NSStringdengan Stringsebagai sumber tidak bekerja.

Versi ini juga berfungsi dengan Swift 3 di macOS / Xcode dengan satu-satunya pengecualian bahwa Anda harus menggunakan nama dan NSRegularExpressionbukan RegularExpression.

Rob Mecham
sumber
5

@ p4bloch jika Anda ingin mengambil hasil dari serangkaian tanda kurung, maka Anda perlu menggunakan rangeAtIndex(index)metode NSTextCheckingResult, alih-alih range. Inilah metode @MartinR untuk Swift2 dari atas, diadaptasi untuk menangkap tanda kurung. Dalam larik yang dikembalikan, hasil pertama [0]adalah seluruh tangkapan, dan kemudian masing-masing kelompok tangkapan dimulai [1]. Saya berkomentar mapoperasi (jadi lebih mudah untuk melihat apa yang saya ubah) dan menggantinya dengan loop bersarang.

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 []
    }
}

Contoh use case mungkin, katakan Anda ingin membagi string title yearmis. "Finding Dory 2016" Anda bisa melakukan ini:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
sumber
Jawaban ini membuat saya senang. Saya menghabiskan 2 jam mencari solusi yang dapat memuaskan ekspresi regualr dengan tambahan menangkap grup.
Ahmad
Ini berfungsi tetapi akan macet jika rentang tidak ditemukan. Saya memodifikasi kode ini sehingga fungsinya kembali [String?]dan di for i in 0..<result.numberOfRangesblok, Anda harus menambahkan tes yang hanya menambahkan kecocokan jika rentang! = NSNotFound, Jika tidak, ia harus menambahkan nol. Lihat: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 tanpa 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)!])
        }
    }
}
shiami
sumber
Hati-hati dengan solusi di atas: NSMakeRange(0, self.count)tidak benar, karena selfadalah String(= UTF8) dan bukan NSString(= UTF16). Jadi self.countbelum tentu sama dengan nsString.length(seperti yang digunakan dalam solusi lain). Anda dapat mengganti perhitungan rentang denganNSRange(self.startIndex..., in: self)
pd95
3

Sebagian besar solusi di atas hanya memberikan kecocokan penuh sebagai hasilnya mengabaikan kelompok tangkapan misalnya: ^ \ d + \ s + (\ d +)

Untuk mendapatkan pertandingan grup tangkapan seperti yang diharapkan, Anda memerlukan sesuatu seperti (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
    }
}
valexa
sumber
Ini bagus jika Anda ingin hanya hasil pertama, untuk mendapatkan setiap hasil yang dibutuhkan for index in 0..<matches.count {sekitarlet lastRange... results.append(matchedString)}
Geoff
untuk klausa akan terlihat seperti ini: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

Inilah yang saya lakukan, saya harap ini membawa perspektif baru bagaimana ini bekerja pada Swift.

Dalam contoh di bawah ini saya akan mendapatkan string apa pun di antaranya []

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)")
}
Dalorzo
sumber
2

Ini adalah solusi yang sangat sederhana yang mengembalikan array string dengan korek api

Cepat 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)
        }
    }
Jorge Osorio
sumber
2

Cara tercepat untuk mengembalikan semua pertandingan dan menangkap grup di 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)) }
        } ?? []
    }
}

Mengembalikan deretan string 2-dimensi:

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

mengembalikan ...

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

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

Terima kasih kepada Lars Blumberg nya jawaban untuk menangkap kelompok dan pertandingan penuh dengan Swift 4 , yang membantu saya keluar banyak. Saya juga membuat tambahan untuk orang-orang yang menginginkan respons error.localizedDescription ketika regex mereka tidak valid:

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 []
        }
    }
}

Bagi saya memiliki deskripsi localizedDescription sebagai kesalahan membantu memahami apa yang salah dengan melarikan diri, karena menampilkan yang regex terakhir yang coba diimplementasikan dengan cepat.

Vasco
sumber