Swift özü normal ifade eşleşmeleri


175

Normal dizgi ile eşleşen bir dizeden alt dizeleri ayıklamak istiyorum.

Yani böyle bir şey arıyorum:

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

Yani sahip olduğum şey bu:

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

Sorun şu ki matchesInString, bana bir dizi teslim ediyor NSTextCheckingResult, nerede NSTextCheckingResult.rangetür NSRange.

NSRangeile uyumsuz Range<String.Index>, bu yüzden kullanmamı engelliyortext.substringWithRange(...)

Çok fazla kod satırı olmadan bu basit şeyi hızlı bir şekilde nasıl başaracağınız hakkında bir fikriniz var mı?

Yanıtlar:


313

matchesInString()Yöntem Stringilk argüman olarak a alsa bile , dahili olarak çalışır NSStringve range parametresi NSStringSwift dize uzunluğu olarak değil length kullanılarak verilmelidir . Aksi takdirde, "bayraklar" gibi "genişletilmiş grafik kümeleri" için başarısız olur.

İtibariyle Swift 4 (Xcode 9), Swift standart kitaplığı arasında dönüştürme işlevleri sağlar Range<String.Index> ve 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 []
    }
}

Misal:

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

Not: Zorunlu paketin açılması Range($0.range, in: text)!güvenlidir, çünkü NSRangeverilen dizenin bir alt dizesini ifade eder text. Ancak, bundan kaçınmak istiyorsanız,

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

yerine.


(Swift 3 ve öncesi için eski cevap :)

Bu nedenle, verilen Swift dizesini bir biçime dönüştürmeli NSStringve sonra aralıkları çıkarmalısınız . Sonuç otomatik olarak bir Swift dize dizisine dönüştürülecektir.

(Swift 1.2 kodu düzenleme geçmişinde bulunabilir.)

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

Misal:

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

Misal:

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

9
Beni delirmekten kurtardın. Şaka yapmıyorum. Çok teşekkür ederim!
mitchkman

1
@MathijsSegers: Swift 1.2 / Xcode 6.3 kodunu güncelledim. Bilmeme izin verdiğin için teşekkürler!
Martin R

1
ancak bir etiket arasındaki dizeleri aramak istersem ne olur? Aynı sonuca ihtiyacım var (maç bilgileri): regex101.com/r/cU6jX8/2 . hangi regex modelini önerirsiniz?
Peter Kreinz

Güncelleme Swift 2 için değil, Swift 1.2 içindir. Kod Swift 2 ile derlenmez
PatrickNLT

1
Teşekkürler! Ya sadece normal ifadedeki () arasında gerçekten olanı çıkarmak istiyorsanız? Örneğin, "[0-9] {3} ([0-9] {6})" içinde sadece son 6 rakamı almak istiyorum.
p4bloch

64

Cevabım verilen cevapların üzerine kuruludur, ancak ek destek ekleyerek normal ifade eşleşmesini daha sağlam hale getirir:

  • Yalnızca eşleşmeleri değil , aynı zamanda her bir eşleşme için tüm yakalama gruplarını döndürür (aşağıdaki örneklere bakın)
  • Boş bir dizi döndürmek yerine, bu çözüm isteğe bağlı eşleşmeleri destekler
  • do/catchKonsola yazdırmamaktan kaçınır ve guardyapıyı kullanır
  • EklentimatchingStrings olarak eklerString

Hızlı 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")

Hızlı 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")

Hızlı 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
Yakalama grupları hakkında iyi fikir. Ama neden "bekçi" "do / catch" den daha hızlı?
Martin R

Nshipster.com/guard-and-defer gibi Swift 2.0'ın iç içe if ifadeleri yerine iç içe geçmiş bir [...] tarzı erken dönüşü cesaretlendirdiğini söyleyen insanlarla aynı fikirdeyim . Aynısı iç içe geçmiş do / catch deyimleri IMHO için de geçerlidir.
Lars Blumberg

try / catch, Swift'teki yerel hata işlemedir. try?olası bir hata mesajıyla değil, yalnızca aramanın sonucuyla ilgileniyorsanız kullanılabilir. Yani evet, guard try? ..iyi, ama hatayı yazdırmak istiyorsanız bir do-block'a ihtiyacınız var. Her iki yol da Swifty.
Martin R

3
Güzel pasajınıza birim
testler ekledim

1
Ben bunu görene kadar kendi @MartinR cevabına göre yazmak üzereydi. Teşekkürler!
Oritm

13

Bir dizeden alt dizeleri ayıklamak istiyorsanız, yalnızca konumdan değil, emoji içeren gerçek Dizeden de alabilirsiniz. Sonra, aşağıdaki basit bir çözüm olabilir.

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

Örnek Kullanım:

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

Aşağıdakileri döndürür:

["👿⚽️"]

"\ W +" kullanıldığında not beklenmedik bir ""

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

Bu String dizisini döndürecek

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

1
İstediğim bu
Kyle KIM

1
Güzel! Swift 3 için küçük bir ayarlamaya ihtiyacı var, ama harika.
Jelle

@Jelle gereken ayarlama nedir? Hızlı 5.1.3 kullanıyorum
Peter Schorn

9

Kabul edilen cevap çözümünün maalesef Linux için Swift 3'te derlenmediğini buldum. İşte değiştirilmiş bir sürüm, o zaman bunu yapar:

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

Temel farklar:

  1. Linux'ta Swift, NSSwift-yerel eşdeğeri olmayan Foundation nesnelerine önek bırakılmasını gerektiriyor gibi görünüyor . (Bkz. Swift evrim önerisi # 86. )

  2. Linux'ta Swift ayrıca optionshem RegularExpressionbaşlatma hem de matchesyöntem için argümanların belirtilmesini gerektirir .

  3. Nedense, bir halka şiddet Stringbir içine NSStringyeni bir Linux üzerinde Swift çalışmıyor ama başlatılıyor NSStringbir ile Stringkaynak olarak çalışır.

Bu sürüm ayrıca ismini kullanması gerektiğini tek istisna MacOS / Xcode üzerinde Swift 3 ile çalışır NSRegularExpressionyerine RegularExpression.


5

@ p4bloch, bir dizi yakalama parantezinin sonuçlarını yakalamak istiyorsanız , yerine rangeAtIndex(index)yöntemini kullanmanız gerekir . İşte @MartinR'in Swift2 için yakalama parantezleri için uyarlanmış yöntemi. Döndürülen dizide, ilk sonuç tüm yakalama olur ve ardından tek tek yakalama grupları başlar . İşlemi yorumladım (böylece ne değiştirdiğimi görmek daha kolay) ve iç içe döngülerle değiştirdim.NSTextCheckingResultrange[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 []
    }
}

Örnek bir kullanım örneği, title yearörneğin "Dory 2016'yı Bulma" dizesini bölmek istediğinizi düşünebilirsiniz:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]

Bu cevap benim günümü yaptı. Grupların ek yakalanmasıyla regülasyon ifadesini tatmin edebilecek bir çözüm aramak için 2 saat harcadım.
Ahmad

Bu çalışır, ancak herhangi bir aralık bulunmazsa kilitlenir. Bu kodu işlev döndürecek şekilde değiştirdim [String?]ve for i in 0..<result.numberOfRangesblokta, sadece aralık! = İse eşleşmeyi ekleyen bir test eklemeniz NSNotFoundgerekir, aksi takdirde nil eklemelidir. Bakınız: stackoverflow.com/a/31892241/2805570
stef

4

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

Yukarıdaki çözüme dikkat edin: NSMakeRange(0, self.count)doğru değil, çünkü selfbir String(= UTF8) ve bir NSString(= UTF16) değil. Dolayısıyla, (diğer çözümlerde kullanıldığı self.countgibi) ile aynı olmak zorunda değildir nsString.length. Aralık hesaplamasınıNSRange(self.startIndex..., in: self)
pd95

3

Yukarıdaki çözümlerin çoğu yalnızca yakalama gruplarını yok sayarak tam eşleşmeyi verir, örneğin: ^ \ d + \ s + (\ d +)

Yakalama grubu eşleşmelerini beklendiği gibi elde etmek için (Swift4) gibi bir şeye ihtiyacınız var:

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

Eğer ihtiyacı olan her sonuç almak için, sadece ilk sonucu isteyen konum bu harika for index in 0..<matches.count {etrafındalet lastRange... results.append(matchedString)}
Geoff

for cümlesi şöyle görünmelidir: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

İşte böyle yaptım, umarım Swift üzerinde nasıl çalıştığını yeni bir bakış açısı getirir.

Aşağıdaki bu örnekte ben arasında herhangi bir dize alacak []

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

Bu, eşleşmelerle bir dize dizisi döndüren çok basit bir çözümdür

Hızlı 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'teki tüm karşılaşmaları döndürmenin ve grupları yakalamanın en hızlı yolu

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 boyutlu dizelerden oluşan bir dizi döndürür:

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

İadeler...

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

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

0

Lars Blumberg'e , Swift 4 ile grup yakalama ve tam maçlar için verdiği cevap için çok teşekkürler , bu da bana çok yardımcı oldu. Ben de onların regex geçersiz olduğunda bir error.localizedDescription yanıtı isteyen insanlar için bir ek yaptım:

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

Benim için localizedDescription öğesinin hata olarak alınması, kaçan neyin yanlış gittiğini anlamaya yardımcı oldu, çünkü son regex swift'in uygulamaya çalıştığını gösteriyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.