Swift trích xuất regex phù hợp


175

Tôi muốn trích xuất các chuỗi con từ một chuỗi khớp với mẫu regex.

Vì vậy, tôi đang tìm kiếm một cái gì đó như thế này:

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

Vì vậy, đây là những gì tôi có:

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

Vấn đề là, điều đó matchesInStringmang lại cho tôi một mảng NSTextCheckingResult, NSTextCheckingResult.rangethuộc loại nào NSRange.

NSRangekhông tương thích với Range<String.Index>, vì vậy nó ngăn tôi sử dụngtext.substringWithRange(...)

Bất kỳ ý tưởng làm thế nào để đạt được điều đơn giản này trong swift mà không cần quá nhiều dòng mã?

Câu trả lời:


313

Ngay cả khi matchesInString()phương thức lấy tham Stringsố làm đối số đầu tiên, nó vẫn hoạt động bên trong NSStringvà tham số phạm vi phải được cung cấp bằng cách sử dụng NSStringđộ dài và không phải là độ dài chuỗi Swift. Nếu không, nó sẽ thất bại đối với "cụm grapheme mở rộng" như "cờ".

Kể từ Swift 4 (Xcode 9), thư viện chuẩn Swift cung cấp các chức năng để chuyển đổi giữa 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 []
    }
}

Thí dụ:

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

Lưu ý: Unrap cưỡng bức Range($0.range, in: text)!là an toàn vì tham chiếu NSRangeđến một chuỗi con của chuỗi đã cho text. Tuy nhiên, nếu bạn muốn tránh nó thì hãy sử dụng

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

thay thế.


(Câu trả lời cũ hơn cho Swift 3 trở về trước :)

Vì vậy, bạn nên chuyển đổi chuỗi Swift đã cho thành một NSStringvà sau đó trích xuất các phạm vi. Kết quả sẽ được tự động chuyển thành mảng chuỗi Swift.

(Mã cho Swift 1.2 có thể được tìm thấy trong lịch sử chỉnh sửa.)

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

Thí dụ:

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

Thí dụ:

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

9
Bạn đã cứu tôi khỏi trở nên điên loạn. Không đùa. Cảm ơn bạn rất nhiều!
mitchkman

1
@MathijsSegers: Tôi đã cập nhật mã cho Swift 1.2 / Xcode 6.3. Cảm ơn vì đã cho tôi biết!
Martin R

1
Nhưng nếu tôi muốn tìm kiếm các chuỗi giữa một thẻ thì sao? Tôi cần kết quả tương tự (thông tin trận đấu) như: regex101.com/r/cU6jX8/2 . mô hình regex nào bạn muốn đề xuất?
Peter Kreinz

Bản cập nhật dành cho Swift 1.2, không phải Swift 2. Mã không được biên dịch với Swift 2.
PatrickNLT

1
Cảm ơn! Điều gì sẽ xảy ra nếu bạn chỉ muốn trích xuất những gì thực sự giữa () trong regex? Ví dụ: trong "[0-9] {3} ([0-9] {6})" Tôi chỉ muốn nhận 6 số cuối.
p4bloch

64

Câu trả lời của tôi được xây dựng dựa trên các câu trả lời đã cho nhưng làm cho kết hợp regex mạnh mẽ hơn bằng cách thêm hỗ trợ bổ sung:

  • Trả về không chỉ các trận đấu mà còn trả về tất cả các nhóm bắt giữ cho mỗi trận đấu (xem ví dụ bên dưới)
  • Thay vì trả về một mảng trống, giải pháp này hỗ trợ các kết quả khớp tùy chọn
  • Tránh do/catchbằng cách không in ra bàn điều khiển và sử dụng guardcấu trúc
  • Thêm matchingStringsdưới dạng tiện ích mở rộngString

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
Ý tưởng tốt về các nhóm chụp. Nhưng tại sao "bảo vệ" Swiftier hơn "làm / bắt" ??
Martin R

Tôi đồng ý với những người như nshipster.com/guard-and-defer , người nói Swift 2.0 chắc chắn dường như đang khuyến khích một phong cách quay trở lại sớm [...] thay vì lồng nhau nếu tuyên bố . Điều tương tự cũng đúng với các câu lệnh do / Catch lồng nhau IMHO.
Lars Blumberg

thử / bắt là xử lý lỗi gốc trong Swift. try?có thể được sử dụng nếu bạn chỉ quan tâm đến kết quả của cuộc gọi, không phải trong một thông báo lỗi có thể. Vì vậy, có, guard try? ..là tốt, nhưng nếu bạn muốn in lỗi thì bạn cần một khối do. Cả hai cách đều là Swifty.
Martin R

3
Tôi đã thêm unittests vào đoạn trích hay của bạn, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye

1
Đã chuẩn bị tự viết dựa trên câu trả lời @MartinR cho đến khi tôi thấy điều này. Cảm ơn!
Oritm

13

Nếu bạn muốn trích xuất các chuỗi con từ một Chuỗi, không chỉ vị trí, (mà cả Chuỗi thực tế bao gồm cả biểu tượng cảm xúc). Sau đó, sau đây có thể là một giải pháp đơn giản hơn.

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

Cách sử dụng ví dụ:

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

Sẽ trả lại như sau:

["👿⚽️"]

Lưu ý sử dụng "\ w +" có thể tạo ra "" bất ngờ

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

Sẽ trả về mảng Chuỗi này

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

1
Đây là những gì tôi muốn
Kyle KIM

1
Đẹp! Nó cần một chút điều chỉnh cho Swift 3, nhưng thật tuyệt.
Jelle

@Jelle điều chỉnh nó cần là gì? Tôi đang sử dụng swift 5.1.3
Peter Schorn

9

Tôi thấy rằng giải pháp của câu trả lời được chấp nhận không may không biên dịch trên Swift 3 cho Linux. Đây là một phiên bản sửa đổi, sau đó, đó là:

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

Sự khác biệt chính là:

  1. Swift trên Linux dường như yêu cầu bỏ NStiền tố vào các đối tượng Foundation mà không có tương đương với Swift. (Xem đề xuất tiến hóa Swift # 86. )

  2. Swift trên Linux cũng yêu cầu chỉ định các optionsđối số cho cả RegularExpressionkhởi tạo và matchesphương thức.

  3. Vì một số lý do, việc ép buộc Stringvào một NSStringcông cụ không hoạt động trong Swift trên Linux nhưng khởi tạo một cái mới NSStringvới một Stringnguồn như hoạt động.

Phiên bản này cũng hoạt động với Swift 3 trên macOS / Xcode với ngoại lệ duy nhất là bạn phải sử dụng tên NSRegularExpressionthay vì RegularExpression.


5

@ p4bloch nếu bạn muốn chụp kết quả từ một loạt dấu ngoặc đơn, thì bạn cần sử dụng rangeAtIndex(index)phương pháp NSTextCheckingResultthay vì range. Đây là phương pháp của @MartinR cho Swift2 từ trên xuống, được điều chỉnh để bắt giữ dấu ngoặc đơn. Trong mảng được trả về, kết quả đầu tiên [0]là toàn bộ quá trình chụp và sau đó các nhóm chụp riêng lẻ bắt đầu từ đó [1]. Tôi đã nhận xét mapthao tác (để dễ dàng hơn khi thấy những gì tôi đã thay đổi) và thay thế nó bằng các vòng lặp lồng nhau.

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

Một trường hợp sử dụng ví dụ có thể là, giả sử bạn muốn tách một chuỗi title yearví dụ: "Tìm Dory 2016" bạn có thể làm điều này:

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

Câu trả lời này làm cho ngày của tôi. Tôi đã dành 2 giờ để tìm kiếm một giải pháp có thể đáp ứng biểu thức regualr với việc bắt giữ thêm các nhóm.
Ahmad

Điều này hoạt động nhưng nó sẽ sụp đổ nếu không tìm thấy bất kỳ phạm vi. Tôi đã sửa đổi mã này để hàm trả về [String?]và trong for i in 0..<result.numberOfRangeskhối, bạn phải thêm một bài kiểm tra chỉ nối thêm khớp nếu phạm vi! = NSNotFound, Nếu không nó sẽ nối thêm nil. Xem: stackoverflow.com/a/31892241/2805570
stef

4

Swift 4 không có 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)!])
        }
    }
}

Hãy cẩn thận với giải pháp trên: NSMakeRange(0, self.count)không đúng, vì selfString(= UTF8) chứ không phải NSString(= UTF16). Vì vậy, self.countkhông nhất thiết phải giống như nsString.length(như được sử dụng trong các giải pháp khác). Bạn có thể thay thế phép tính phạm vi bằngNSRange(self.startIndex..., in: self)
pd95

3

Hầu hết các giải pháp ở trên chỉ cung cấp kết quả khớp hoàn toàn do kết quả bỏ qua các nhóm chụp, ví dụ: ^ \ d + \ s + (\ d +)

Để có được các nhóm chụp phù hợp như mong đợi, bạn cần một cái gì đó như (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
    }
}

Điều này thật tuyệt nếu bạn chỉ muốn kết quả đầu tiên, để có được mỗi kết quả mà nó cần for index in 0..<matches.count {xung quanhlet lastRange... results.append(matchedString)}
Geoff

mệnh đề for sẽ giống như thế này: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

Đây là cách tôi đã làm, tôi hy vọng nó mang lại một viễn cảnh mới về cách thức hoạt động của Swift trên Swift.

Trong ví dụ dưới đây tôi sẽ nhận được bất kỳ chuỗi nào giữa []

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

Đây là một giải pháp rất đơn giản trả về một chuỗi các chuỗi có khớp

Swift 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

Cách nhanh nhất để trả lại tất cả các trận đấu và các nhóm bắt trong 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)) }
        } ?? []
    }
}

Trả về một chuỗi 2 chiều:

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

trả lại ...

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

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

0

Xin chân thành cảm ơn Lars Blumberg câu trả lời của anh ấy về việc bắt các nhóm và các trận đấu đầy đủ với Swift 4 , điều này đã giúp tôi rất nhiều. Tôi cũng đã bổ sung nó cho những người muốn có phản hồi error.localizedDes mô tả khi regex của họ không hợp lệ:

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

Đối với tôi, việc mô tả cục bộ là lỗi đã giúp hiểu được những gì đã xảy ra với việc thoát, vì nó hiển thị mà regex swift cuối cùng cố gắng thực hiện.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.