MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift正则表达式高效处理技巧

2024-03-245.7k 阅读

Swift 正则表达式基础

在 Swift 编程中,正则表达式是一种强大的工具,用于在文本中进行模式匹配。正则表达式使用特定的语法来描述字符模式,通过这些模式可以高效地查找、替换或提取文本中的信息。

在 Swift 中,处理正则表达式主要依赖于 NSRegularExpression 类,它是 Foundation 框架的一部分。虽然 Swift 有自己的字符串处理功能,但正则表达式提供了更灵活和强大的方式来处理复杂的文本模式。

首先,我们来看如何创建一个 NSRegularExpression 实例。假设我们要匹配电子邮件地址,电子邮件地址的一般模式是由字母、数字、下划线、点、连字符组成,以 @ 符号分隔,后面跟着域名,域名由字母、数字、点组成。以下是创建匹配电子邮件地址的正则表达式实例的代码:

let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
do {
    let regex = try NSRegularExpression(pattern: emailPattern, options: [])
    // 这里可以进行后续的匹配操作
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,我们定义了一个字符串 emailPattern 作为正则表达式的模式。然后使用 try NSRegularExpression(pattern:options:) 方法来创建 NSRegularExpression 实例。如果模式无效,会抛出异常,我们在 catch 块中处理异常。

基本匹配操作

查找匹配项

一旦创建了 NSRegularExpression 实例,就可以使用它来对文本进行匹配操作。最常见的操作之一是查找文本中所有匹配正则表达式的子字符串。以下是查找字符串中所有电子邮件地址的示例:

let text = "请联系我 at john.doe123@example.com 或 jane_smith@another-example.co.uk"
let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
do {
    let regex = try NSRegularExpression(pattern: emailPattern, options: [])
    let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
    for match in matches {
        let matchRange = Range(match.range, in: text)!
        let matchedString = String(text[matchRange])
        print("找到匹配的电子邮件地址: \(matchedString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在这段代码中,我们使用 matches(in:range:) 方法在 text 字符串中查找所有匹配 emailPattern 的子字符串。range 参数指定了要搜索的文本范围,这里我们使用 NSRange(text.startIndex..., in: text) 表示搜索整个字符串。matches 是一个数组,包含了所有找到的匹配项。通过遍历这个数组,我们可以获取每个匹配项的范围,并从原始字符串中提取出匹配的子字符串。

替换匹配项

另一个常见的操作是替换文本中匹配正则表达式的子字符串。例如,我们可能想将文本中的电子邮件地址替换为一个通用的字符串 "EMAIL_ADDRESS"。以下是实现这个功能的代码:

let text = "请联系我 at john.doe123@example.com 或 jane_smith@another-example.co.uk"
let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
do {
    let regex = try NSRegularExpression(pattern: emailPattern, options: [])
    let replacedText = regex.stringByReplacingMatches(in: text, range: NSRange(text.startIndex..., in: text), withTemplate: "EMAIL_ADDRESS")
    print("替换后的文本: \(replacedText)")
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,我们使用 stringByReplacingMatches(in:range:withTemplate:) 方法进行替换操作。withTemplate 参数指定了用于替换匹配项的字符串。这里我们简单地使用 "EMAIL_ADDRESS" 作为替换模板,执行该方法后,返回的 replacedText 就是替换后的文本。

正则表达式语法深入

字符类

字符类是正则表达式中用于匹配一组字符中的任意一个的结构。例如,[abc] 表示匹配字符 abc 中的任意一个。字符类也可以使用连字符表示范围,比如 [a-z] 表示匹配任意小写字母,[0-9] 表示匹配任意数字。

在 Swift 正则表达式中,有些字符在字符类中有特殊含义。例如,^ 在字符类的开头表示取反,即匹配不在字符类中的字符。比如 [^a-z] 表示匹配任何非小写字母的字符。

以下是一个示例,匹配字符串中所有非数字字符:

let text = "abc123def456"
let nonDigitPattern = "[^0-9]"
do {
    let regex = try NSRegularExpression(pattern: nonDigitPattern, options: [])
    let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
    for match in matches {
        let matchRange = Range(match.range, in: text)!
        let matchedString = String(text[matchRange])
        print("找到非数字字符: \(matchedString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

量词

量词用于指定前面的字符或字符组出现的次数。常见的量词有:

  • *:表示前面的字符或字符组可以出现 0 次或多次。例如,a* 表示匹配 0 个或多个 a 字符。
  • +:表示前面的字符或字符组可以出现 1 次或多次。例如,a+ 表示匹配 1 个或多个 a 字符。
  • ?:表示前面的字符或字符组可以出现 0 次或 1 次。例如,a? 表示匹配 0 个或 1 个 a 字符。

此外,还可以使用花括号 {n}{n,}{n,m} 来精确指定出现的次数。{n} 表示前面的字符或字符组必须出现 n 次;{n,} 表示前面的字符或字符组至少出现 n 次;{n,m} 表示前面的字符或字符组出现的次数在 nm 之间(包括 nm)。

以下是一些示例:

  • a{3}:匹配连续出现 3 次的 a,即 aaa
  • a{3,}:匹配连续出现至少 3 次的 a,如 aaaaaaa 等。
  • a{3,5}:匹配连续出现 3 到 5 次的 a,如 aaaaaaaaaaaa

以下代码示例匹配字符串中连续出现 3 次及以上的数字:

let text = "12 1234 567 89"
let digitPattern = "[0-9]{3,}"
do {
    let regex = try NSRegularExpression(pattern: digitPattern, options: [])
    let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
    for match in matches {
        let matchRange = Range(match.range, in: text)!
        let matchedString = String(text[matchRange])
        print("找到连续 3 次及以上的数字: \(matchedString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

分组

分组是正则表达式中非常重要的概念,通过使用圆括号 () 来实现。分组有两个主要作用:一是将多个字符组合成一个单元,以便可以对这个单元应用量词;二是可以捕获分组内的匹配内容,方便后续引用。

例如,(ab)+ 表示匹配一个或多个连续的 ab 字符串。这里 (ab) 就是一个分组,+ 量词应用于这个分组。

捕获分组的内容可以在匹配结果中通过索引来访问。在 Swift 中,NSRegularExpression.Matchrange(at:) 方法可以获取指定分组的范围。以下是一个示例,匹配日期格式为 YYYY-MM-DD 的字符串,并分别提取年、月、日:

let text = "今天的日期是 2023-10-05"
let datePattern = "(\\d{4})-(\\d{2})-(\\d{2})"
do {
    let regex = try NSRegularExpression(pattern: datePattern, options: [])
    let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
    for match in matches {
        let yearRange = Range(match.range(at: 1), in: text)!
        let monthRange = Range(match.range(at: 2), in: text)!
        let dayRange = Range(match.range(at: 3), in: text)!
        let year = String(text[yearRange])
        let month = String(text[monthRange])
        let day = String(text[dayRange])
        print("年: \(year), 月: \(month), 日: \(day)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,(\\d{4})(\\d{2})(\\d{2}) 分别是三个捕获分组。range(at: 1) 获取第一个分组(年)的范围,range(at: 2) 获取第二个分组(月)的范围,以此类推。

边界匹配

边界匹配用于指定匹配应该发生在字符串的开头、结尾或单词边界处。常见的边界匹配符有:

  • ^:表示匹配字符串的开头。例如,^Hello 表示匹配以 Hello 开头的字符串。
  • $:表示匹配字符串的结尾。例如,World$ 表示匹配以 World 结尾的字符串。
  • \b:表示匹配单词边界。单词边界是指单词和非单词字符之间的位置,或者字符串的开头和结尾如果是单词的开头或结尾的话。例如,\bcat\b 表示匹配独立的单词 cat,而不会匹配 category 中的 cat

以下是一些示例代码:

let text1 = "Hello, World!"
let startPattern = "^Hello"
do {
    let regex = try NSRegularExpression(pattern: startPattern, options: [])
    let matches = regex.matches(in: text1, range: NSRange(text1.startIndex..., in: text1))
    if!matches.isEmpty {
        print("字符串以 Hello 开头")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

let text2 = "Goodbye, World!"
let endPattern = "World$"
do {
    let regex = try NSRegularExpression(pattern: endPattern, options: [])
    let matches = regex.matches(in: text2, range: NSRange(text2.startIndex..., in: text2))
    if!matches.isEmpty {
        print("字符串以 World 结尾")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

let text3 = "I have a cat, but not a category"
let wordPattern = "\\bcat\\b"
do {
    let regex = try NSRegularExpression(pattern: wordPattern, options: [])
    let matches = regex.matches(in: text3, range: NSRange(text3.startIndex..., in: text3))
    for match in matches {
        let matchRange = Range(match.range, in: text3)!
        let matchedString = String(text3[matchRange])
        print("找到独立的单词 cat: \(matchedString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

提高匹配效率的技巧

避免过度复杂的正则表达式

虽然正则表达式非常强大,但过度复杂的正则表达式可能会导致性能问题。复杂的正则表达式可能需要更多的计算资源来进行匹配,特别是在处理大量文本时。

例如,一个包含多个嵌套分组、复杂的量词组合以及大量字符类的正则表达式可能会使匹配过程变得缓慢。在编写正则表达式时,尽量将复杂的模式分解为多个简单的模式,然后通过多次匹配或其他逻辑来实现相同的功能。

以下是一个示例,假设我们要匹配 HTML 标签内的文本。一个非常复杂的正则表达式可能是这样:

let complexHtmlPattern = "<([^>]+)>(.*?)</\\1>"

这个正则表达式使用了反向引用 \\1 来匹配开始标签和结束标签必须相同。虽然它可以实现功能,但在匹配复杂的 HTML 结构时,性能可能不佳。一个更简单的方法可能是先匹配所有的开始标签和结束标签,然后再进行进一步的处理。

let startTagPattern = "<([^>]+)>"
let endTagPattern = "</([^>]+)>"
do {
    let startRegex = try NSRegularExpression(pattern: startTagPattern, options: [])
    let endRegex = try NSRegularExpression(pattern: endTagPattern, options: [])
    let htmlText = "<div>Hello, World!</div>"
    let startMatches = startRegex.matches(in: htmlText, range: NSRange(htmlText.startIndex..., in: htmlText))
    let endMatches = endRegex.matches(in: htmlText, range: NSRange(htmlText.startIndex..., in: htmlText))
    // 这里可以根据开始标签和结束标签的匹配结果进一步处理,获取标签内的文本
} catch {
    print("创建正则表达式失败: \(error)")
}

预编译正则表达式

在需要多次使用同一个正则表达式进行匹配的场景下,预编译正则表达式可以显著提高性能。每次调用 try NSRegularExpression(pattern:options:) 方法都会创建一个新的正则表达式实例,这涉及到解析和编译正则表达式的过程,是比较耗时的。

通过将正则表达式实例化一次并保存起来,后续直接使用这个实例进行匹配操作,可以避免重复的编译过程。以下是一个示例,在一个循环中多次匹配电子邮件地址:

let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
do {
    let regex = try NSRegularExpression(pattern: emailPattern, options: [])
    let texts = ["john.doe@example.com", "jane.smith@another-example.co.uk", "tom@test.com"]
    for text in texts {
        let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
        for match in matches {
            let matchRange = Range(match.range, in: text)!
            let matchedString = String(text[matchRange])
            print("找到匹配的电子邮件地址: \(matchedString)")
        }
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,我们只创建了一次 NSRegularExpression 实例 regex,然后在循环中多次使用它来匹配不同的字符串。这样可以避免在每次循环时都重新编译正则表达式,提高了整体的匹配效率。

使用合适的匹配选项

NSRegularExpressionoptions 参数可以影响匹配的行为和效率。一些常用的选项包括:

  • .caseInsensitive:表示匹配时忽略大小写。如果需要匹配不区分大小写的文本,使用这个选项可以简化正则表达式的编写,同时提高匹配效率,因为不需要在字符类中同时包含大写和小写字母。例如,匹配单词 "Swift" 不区分大小写,可以使用 [sS][wW][iI][fF][tT],而使用 .caseInsensitive 选项后,只需要 Swift 即可。
let text = "I love Swift, swift is great"
let pattern = "Swift"
do {
    let regex = try NSRegularExpression(pattern: pattern, options:.caseInsensitive)
    let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
    for match in matches {
        let matchRange = Range(match.range, in: text)!
        let matchedString = String(text[matchRange])
        print("找到匹配的字符串: \(matchedString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}
  • .anchored:表示匹配必须从字符串的开头开始。如果明确知道要匹配的内容总是出现在字符串的开头,使用这个选项可以提高匹配效率,因为正则表达式引擎不需要在字符串的每个位置都进行尝试匹配。

以下是一个示例,匹配以 "http://" 开头的 URL:

let urlText = "http://www.example.com"
let urlPattern = "http://"
do {
    let regex = try NSRegularExpression(pattern: urlPattern, options:.anchored)
    let matches = regex.matches(in: urlText, range: NSRange(urlText.startIndex..., in: urlText))
    if!matches.isEmpty {
        print("是一个以 http:// 开头的 URL")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

处理复杂文本场景

匹配嵌套结构

在处理一些具有嵌套结构的文本,如 XML 或 JSON 时,正则表达式的编写会比较复杂。因为嵌套结构需要处理层次关系,简单的正则表达式可能无法准确匹配。

以匹配简单的 XML 标签为例,假设 XML 结构如下:

<root>
    <element>content</element>
</root>

一个简单的匹配标签内文本的正则表达式可能是 <([^>]+)>(.*?)</\\1>,但这个表达式在处理复杂嵌套结构时会有问题。例如,对于以下 XML 内容:

<root>
    <element>
        <sub - element>sub - content</sub - element>
    </element>
</root>

上述简单的正则表达式可能无法正确匹配每个标签内的内容。一种解决方法是使用递归正则表达式,但在 Swift 的 NSRegularExpression 中不直接支持递归。一种替代方案是结合字符串解析和多次匹配来处理。

首先,我们可以先匹配所有的开始标签和结束标签,然后通过栈来维护标签的层次关系。以下是一个简化的示例代码:

let xmlText = "<root><element><sub - element>sub - content</sub - element></element></root>"
let startTagPattern = "<([^>]+)>"
let endTagPattern = "</([^>]+)>"
do {
    let startRegex = try NSRegularExpression(pattern: startTagPattern, options: [])
    let endRegex = try NSRegularExpression(pattern: endTagPattern, options: [])
    let startMatches = startRegex.matches(in: xmlText, range: NSRange(xmlText.startIndex..., in: xmlText))
    let endMatches = endRegex.matches(in: xmlText, range: NSRange(xmlText.startIndex..., in: xmlText))
    var tagStack: [String] = []
    var startIndex = xmlText.startIndex
    for (startIndex, startMatch) in startMatches.enumerated() {
        let startRange = Range(startMatch.range, in: xmlText)!
        let tagName = String(xmlText[startRange].dropFirst().dropLast())
        tagStack.append(tagName)
        if let endMatchIndex = endMatches.firstIndex(where: {
            let endRange = Range($0.range, in: xmlText)!
            let endTagName = String(xmlText[endRange].dropFirst(2).dropLast())
            return endTagName == tagStack.last
        }) {
            let endRange = Range(endMatches[endMatchIndex].range, in: xmlText)!
            let contentRange = startRange.upperBound..<endRange.lowerBound
            let content = String(xmlText[contentRange])
            print("标签 \(tagStack.last!) 内的内容: \(content)")
            tagStack.removeLast()
        }
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,我们首先分别匹配开始标签和结束标签。然后使用一个栈 tagStack 来记录当前打开的标签。当找到一个结束标签时,检查它是否与栈顶的标签匹配,如果匹配,则提取两个标签之间的内容。

处理多行文本

在处理多行文本时,默认情况下,NSRegularExpression^$ 只匹配字符串的开头和结尾,而不是每行的开头和结尾。如果需要匹配每行的开头和结尾,可以使用 .dotMatchesLineSeparators 选项。

例如,假设我们有一个多行文本,要匹配每行以数字开头的行:

let multiLineText = "1. First line\n2. Second line\nThird line"
let pattern = "^[0-9].*"
do {
    let regex = try NSRegularExpression(pattern: pattern, options:.dotMatchesLineSeparators)
    let matches = regex.matches(in: multiLineText, range: NSRange(multiLineText.startIndex..., in: multiLineText))
    for match in matches {
        let matchRange = Range(match.range, in: multiLineText)!
        let matchedLine = String(multiLineText[matchRange])
        print("找到以数字开头的行: \(matchedLine)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,通过设置 .dotMatchesLineSeparators 选项,^ 就可以匹配每行的开头,从而找到以数字开头的行。

与其他字符串处理方法结合

虽然正则表达式功能强大,但在某些情况下,结合 Swift 原生的字符串处理方法可以更高效地完成任务。

例如,在处理简单的字符串查找和替换时,Swift 的 String 类提供了一些方便的方法,如 containsreplacingOccurrences(of:with:) 等。如果不需要复杂的模式匹配,使用这些方法可能会比正则表达式更高效。

let text = "I like apples, apples are delicious"
let simpleReplaceText = text.replacingOccurrences(of: "apples", with: "oranges")
print("简单替换后的文本: \(simpleReplaceText)")

另一方面,在一些需要复杂模式匹配但又涉及到字符串位置计算等操作时,可以先使用正则表达式进行初步匹配,然后利用 Swift 字符串的索引和切片功能进行进一步处理。

例如,我们匹配一个包含特定子字符串的句子,并获取句子的前后部分:

let longText = "The dog is running in the park. The cat is sleeping on the mat. The bird is flying in the sky."
let subStringPattern = "cat"
let sentencePattern = "[^.]*\\b\(subStringPattern)\\b[^.]*\\."
do {
    let sentenceRegex = try NSRegularExpression(pattern: sentencePattern, options: [])
    let sentenceMatches = sentenceRegex.matches(in: longText, range: NSRange(longText.startIndex..., in: longText))
    for sentenceMatch in sentenceMatches {
        let sentenceRange = Range(sentenceMatch.range, in: longText)!
        let sentence = String(longText[sentenceRange])
        let subStringRange = sentence.range(of: subStringPattern)!
        let preSubString = String(sentence[..<subStringRange.lowerBound])
        let postSubString = String(sentence[subStringRange.upperBound...])
        print("包含 cat 的句子: \(sentence)")
        print("cat 前的部分: \(preSubString)")
        print("cat 后的部分: \(postSubString)")
    }
} catch {
    print("创建正则表达式失败: \(error)")
}

在上述代码中,我们先用正则表达式匹配包含 "cat" 的句子,然后使用 Swift 字符串的 range(of:) 方法找到 "cat" 在句子中的位置,进而获取句子中 "cat" 前后的部分。

通过合理结合正则表达式和 Swift 原生的字符串处理方法,可以在不同的场景下实现高效的文本处理。