สารสกัดจาก Swift ตรงกับการแข่งขัน


175

ฉันต้องการแยกสตริงออกจากสตริงที่ตรงกับรูปแบบ regex

ดังนั้นฉันกำลังมองหาสิ่งนี้:

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

NSRangeไม่เข้ากันกับRange<String.Index>ดังนั้นจึงป้องกันไม่ให้ฉันใช้text.substringWithRange(...)

มีความคิดว่าจะบรรลุสิ่งง่าย ๆ นี้ได้อย่างไรโดยไม่ต้องใช้โค้ดมากเกินไป?

คำตอบ:


313

แม้ว่าmatchesInString()วิธีการที่ใช้Stringเป็นอาร์กิวเมนต์แรกมันทำงานภายในด้วย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"]

หมายเหตุ:แกะบังคับให้Range($0.range, in: text)!มีความปลอดภัยเพราะหมายถึงการย่อยของสตริงที่กำหนดNSRange textอย่างไรก็ตามหากคุณต้องการหลีกเลี่ยงก็ให้ใช้

        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

1
@MathijsSegers: ฉันได้อัปเดตรหัสสำหรับ Swift 1.2 / Xcode 6.3 แล้ว ขอบคุณสำหรับการให้ฉันรู้ว่า!
Martin R

1
แต่ถ้าฉันต้องการค้นหาสตริงระหว่างแท็กล่ะ ฉันต้องการผลเดียวกัน (ข้อมูลการแข่งขัน) ที่ชอบ: regex101.com/r/cU6jX8/2 คุณต้องการแนะนำรูปแบบ regex ใด
Peter Kreinz

การอัปเดตสำหรับ Swift 1.2 ไม่ใช่ Swift 2 โค้ดไม่ได้รวบรวมกับ Swift 2
PatrickNLT

1
ขอบคุณ! เกิดอะไรขึ้นถ้าคุณต้องการแยกสิ่งที่จริงระหว่าง () ใน regex? ตัวอย่างเช่นใน "[0-9] {3} ([0-9] {6})" ฉันต้องการแค่รับตัวเลข 6 ตัวหลังเท่านั้น
p4bloch

64

คำตอบของฉันสร้างขึ้นจากคำตอบที่ได้รับ แต่ทำให้การจับคู่ regex มีประสิทธิภาพมากขึ้นโดยการเพิ่มการสนับสนุนเพิ่มเติม:

  • ส่งกลับไม่เพียง แต่การจับคู่ แต่ยังส่งคืนกลุ่มการจับภาพทั้งหมดสำหรับแต่ละการแข่งขัน (ดูตัวอย่างด้านล่าง)
  • แทนที่จะส่งกลับอาร์เรย์ที่ว่างเปล่าโซลูชันนี้สนับสนุนการจับคู่แบบเลือกได้
  • หลีกเลี่ยงdo/catchโดยไม่พิมพ์ไปยังคอนโซลและใช้ประโยชน์จากการguardสร้าง
  • เพิ่มmatchingStringsเป็นส่วนขยายString

สวิฟท์ 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
ความคิดที่ดีเกี่ยวกับกลุ่มการจับกุม แต่ทำไม "การ์ด" ถึงรวดเร็วกว่า "ทำ / จับ"?
Martin R

ผมเห็นด้วยกับคนเช่นnshipster.com/guard-and-deferที่บอกว่าสวิฟท์ 2.0 แน่นอนดูเหมือนว่าจะได้รับการส่งเสริมให้รูปแบบของผลตอบแทนในช่วงต้น [ ... ] มากกว่าซ้อนกันถ้างบ คำสั่ง do / catch ที่ซ้อนกัน IMHO
Lars Blumberg

try / catch คือการจัดการข้อผิดพลาดดั้งเดิมใน Swift try?สามารถใช้ได้หากคุณสนใจเฉพาะผลลัพธ์ของการโทรเท่านั้นไม่ใช่ในข้อความแสดงข้อผิดพลาดที่เป็นไปได้ ใช่แล้วใช้ได้guard try? ..แต่ถ้าคุณต้องการพิมพ์ข้อผิดพลาดคุณต้องมีการบล็อก ทั้งสองวิธีเป็น Swifty
Martin R

3
ฉันได้เพิ่ม unittests ลงในข้อมูลโค้ดที่ดีของคุณแล้วgist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
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
นี่คือสิ่งที่ฉันต้องการ
Kyle KIM

1
ดี! มันต้องการการปรับแต่งเล็กน้อยสำหรับ Swift 3 แต่มันยอดเยี่ยม
Jelle

@ เจลคือสิ่งที่จำเป็นต้องมีการปรับ? ฉันกำลังใช้ swift 5.1.3
Peter Schorn

9

ฉันพบว่าคำตอบที่ได้รับการยอมรับน่าเสียดายที่ไม่ได้รวบรวมใน Swift 3 สำหรับ 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. Swift บน Linux ดูเหมือนว่าต้องการวางNSคำนำหน้าบนวัตถุ Foundation ซึ่งไม่มีค่าเทียบเท่า Swift-native (ดูข้อเสนอวิวัฒนาการของ Swift # 86 )

  2. Swift บน Linux ยังต้องการการระบุoptionsอาร์กิวเมนต์สำหรับทั้งการRegularExpressionเริ่มต้นและmatchesวิธีการ

  3. ด้วยเหตุผลบางอย่างบีบบังคับStringเป็นNSStringไม่ทำงานในสวิฟท์บน Linux แต่การเริ่มต้นใหม่NSStringด้วยการStringเป็นแหล่งที่มาไม่ทำงาน

รุ่นนี้ยังทำงานร่วมกับสวิฟท์ 3 MacOS / Xcode ยกเว้นอย่างเดียวที่คุณต้องใช้ชื่อแทนNSRegularExpressionRegularExpression


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"การค้นหา Dory 2016" ที่คุณสามารถทำได้:

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

คำตอบนี้ทำให้วันของฉัน ฉันใช้เวลา 2 ชั่วโมงในการค้นหาวิธีแก้ปัญหาที่สามารถตอบสนองการแสดงออก regualr ด้วยการจับกลุ่มเพิ่มเติม
Ahmad

ใช้งานได้ แต่จะทำงานล้มเหลวหากไม่พบช่วงใด ๆ ฉันแก้ไขโค้ดนี้เพื่อให้ฟังก์ชันส่งคืน[String?]และในfor i in 0..<result.numberOfRangesบล็อกคุณต้องเพิ่มการทดสอบที่ต่อท้ายการแข่งขันหากช่วง! = NSNotFoundมิฉะนั้นจะต้องผนวกท้าย ดู: stackoverflow.com/a/31892241/2805570
stef

4

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

ระวังด้วยวิธีแก้ปัญหาข้างต้น: 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)) }
        } ?? []
    }
}

ส่งคืนอาร์เรย์แบบสองมิติของสตริง:

"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 response เมื่อ regex ของพวกเขาไม่ถูกต้อง:

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

สำหรับฉันที่มีการแปลเป็นภาษาท้องถิ่นข้อผิดพลาดช่วยให้เข้าใจสิ่งที่ผิดพลาดกับการหลบหนีเพราะมันแสดงให้เห็นว่า regex สุดท้ายรวดเร็วพยายามที่จะใช้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.