Свитки регулярных выражений Swift
Я хочу извлечь подстроки из строки, соответствующей шаблону регулярного выражения.
Итак, я ищу что-то вроде этого:
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(...)
Любая идея, как добиться этой простой вещи в быстром без слишком много строк кода?
Ответы
Ответ 1
Даже если метод matchesInString()
принимает в качестве первого аргумента String
он работает внутри с NSString
, и параметр диапазона должен быть указан
используя длину NSString
, а не длину строки Swift. В противном случае
fail для "расширенных кластеров графемы", таких как "флаги".
Как и у Swift 4 (Xcode 9), стандарт Swift
библиотека предоставляет функции для преобразования между 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"]
Ответ 2
Мой ответ основывается на ответах на эти вопросы, но делает регулярное сопоставление более надежным, добавляя дополнительную поддержку:
- Возвращает не только совпадения, но также возвращает все группы захвата для каждого совпадения (см. Примеры ниже)
- Вместо того, чтобы возвращать пустой массив, это решение поддерживает дополнительные совпадения
- Avoids
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")
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))
: ""
}
}
}
}
Ответ 3
Если вы хотите извлечь подстроки из строки, а не только позиции (но фактическая строка, включая emojis). Тогда возможно более простое решение.
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"]
Ответ 4
Я обнаружил, что принятое решение для решения, к сожалению, не компилируется на 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 []
}
}
Основные отличия:
-
Swift в Linux, по-видимому, требует сбросить префикс NS
на объектах Foundation, для которого нет родственного эквивалента Swift. (См. Предложение по быстрой эволюции # 86.)
-
Swift в Linux также требует указания аргументов options
для инициализации RegularExpression
и matches
.
-
По какой-то причине принуждение a String
к NSString
не работает в Swift в Linux, но инициализирует новый NSString
с String
по мере того, как источник работает.
Эта версия также работает с Swift 3 на macOS/Xcode с единственным исключением, что вы должны использовать имя 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"]
Ответ 6
Большинство вышеперечисленных решений дают полное совпадение, в результате игнорируя группы захвата, например: ^\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
}
}
Ответ 7
Вот как я это сделал, я надеюсь, что это привнесет новую перспективу, как это работает на 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)")
}
Ответ 8
Это очень простое решение, которое возвращает массив строк со спичками
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)
}
}
Ответ 9
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)!])
}
}
}
Ответ 10
Огромное спасибо Ларсу Блумбергу за ответ на сбор групп и полные матчи с 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 []
}
}
}
Для меня наличие localizedDescription как ошибки помогло понять, что пошло не так с экранированием, так как оно показывает, какое окончательное регулярное выражение swift пытается реализовать.
Ответ 11
Самый быстрый способ вернуть все совпадения и захватить группы в 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