正则表达式教程: 开始

正则表达式教程: 开始原文:RegularExpressionsTutorial:GettingStarted作者:TomElliott译者:kmyhy在这篇教程中,你将学习如何在iOSapp中使用Swift4.2实现正则表达式。更新说明:TomElliott将本教程升级至Swift4.2。原文作者是JamesFrost。正则表达式基础如果你没有听过正则表达式——也…

大家好,欢迎来到IT知识分享网。

原文:Regular Expressions Tutorial: Getting Started
作者:Tom Elliott
译者:kmyhy

在这篇教程中,你将学习如何在 iOS app 中使用 Swift 4.2 实现正则表达式。

更新说明:Tom Elliott 将本教程升级至 Swift 4.2。原文作者是 James Frost。

正则表达式基础

如果你没有听过正则表达式——也叫正则式,那么在继续阅读本教程之前,你应该看一下正则表达式的介绍。幸好,我们已经为你准备好了。请阅读这篇《正则表达式介绍》教程。

在 iOS 中实现正则表达式

了解了基本概念之后,来看看如何在 app 中使用正则表达式。

在本文顶部或底部有一个 Download Materials 按钮,可以下载开始项目。打开开始项目,运行它。

你准备给你的老板 —— 一个大坏蛋——编写一个日记 app。每个人都知道,那些超级大坏蛋为了统治世界,准备将他们的邪恶计划都记录下来。有很多计划需要执行,而你,作为小喽啰,也是这些计划的一部分,你的计划就是为其他计划编写这个 app!

app 的 UI 基本上就绪,但主要的功能需要使用正则表达式,它还没有完成!

本教程中,你的任务是将正则表达式添加到 app 中,为它添加亮点(从而避免被扔到炽热的岩浆中去)。

这里是几张最终完成的产品效果图:

正则表达式教程: 开始

最终 app 会有两个常见的使用正则式的场景:

  1. 文本搜索:高亮、搜索和替换。
  2. 校验用户输入。

首先实现最简单的正则表达式的用法:文本搜索。

实现搜索和替换

这个 app 的搜索替换功能大概是这样的:

  • 在 SearchViewController 中有一个只读的 UITextView,其中包含了你老板的秘密计划的日记。
  • 导航栏中有一个 Search 按钮,会弹出模态窗口 SearchOptionsViewController。
  • 这会允许你那邪恶的老板输入一些信息并点击“搜索”。
  • 然后搜索页面隐藏,在 Text View 的摘要中高亮显示所有匹配结果。
  • 如果你的老板在 SearchOptionsViewController 中选择了“替换”选项,app 会进行替换,而不是高亮显示。

注:app 使用了 UITextView 中的 NSAttributedString 属性来高亮搜索结果。
你也可以用 Text Kit 来实现高亮。更多内容你可以参考这篇 Text Kit Swift 教程

还有一个阅读模式按钮,允许你高亮所有日期、时间,以及日记中的分隔线。为了简单起见,你不需要了解文本中日期时间的所有可能格式。在本文最后,你将实现这个高亮功能。

首先让搜索功能起作用,将普通字符串表示的正则表达式转换成 NSRegularExpression 对象。

打开 SearchOptionsViewController.swift。SearchViewController 会模态地弹出这个 view controller,允许用户输入他/她要搜索的东西(或者替换),并指定大小写敏感或进匹配整词等选项。

看一下文件头部的 SearchOptions 结构。SearchOptions 是一个简单的结构体,封装了用户的搜索选项。代码会将一个 SearchOptions 实例返回给 SearchViewController。最好是能只用它构造一个对应的 NSRegularExpression。你可以写一个 NSRegularExpression 扩展,通过一个定制构造函数来实现。

选择 File ▸ New ▸ File… 然后选择 Swift File。名字就叫做 RegexHelpers.swift。打开文件,添加代码:

extension NSRegularExpression {
  
  convenience init?(options: SearchOptions) throws {
    let searchString = options.searchString
    let isCaseSensitive = options.matchCase
    let isWholeWords = options.wholeWords
    
    let regexOption: NSRegularExpression.Options = 
      isCaseSensitive ? [] : .caseInsensitive
    
    let pattern = isWholeWords ? "\\b\(searchString)\\b" : searchString
    
    try self.init(pattern: pattern, options: regexOption)
  }
}

代码为 NSRegularExpression 增加了一个便利构造函数。它使用传入的 SearchOption 参数进行配置。

需要注意的是:

  • 当用户进行大小写不敏感的查找时,正则表达式的 NSRegularExpressionOptions 使用 .caseInsensitive。NSRegularExpression 默认会进行的大小写敏感查找,但是,在这里,为了用户体验更好,使用大小写不敏感查找。
  • 如果用户进行整词查找,app 会将正则式模板包裹在 \b 字符内。\b 是边界字符,因此在搜索模板前后加上 \b 进行的就是整词查找(例如,”\bcat\b” 匹配的是 cat 这个词,而不会匹配到 catch)。

如果因为某种原因,无法创建 NSRegularExpression,这个构造函数会出错,返回 nil。现在,你有了 NSRegularExpression 对象,就可以用它来匹配文本了。

打开 SearchViewController.swift,找到 searchForText(_:replaceWith:inTextView:),在这个空方法中添加:

if let beforeText = textView.text, let searchOptions = self.searchOptions {
  let range = NSRange(beforeText.startIndex..., in: beforeText)
      
  if let regex = try? NSRegularExpression(options: searchOptions) {
    let afterText = regex?.stringByReplacingMatches(
      in: beforeText,
      options: [], 
      range: range, 
      withTemplate: replacementText
    )
    textView.text = afterText
  }
}

首先,这个方法从 UITextView 中获得当前文本,算出整个字符串的 NSRange。可以只将正则表达式应用在文本的某个范围,这也是为什么要在这里计算 NSRange 的原因。这里,我们使用了整个字符串,因此这个正则表达式会应用到整个文本。

真正关键的语句是调用 stringByReplacingMatches。这个方法返回一个新的字符串,而不是修改原来的字符串。然后这个方法将新字符串设置到 UITextView 上,这样用户就看到了结果。

仍然在 SearchViewController 中,找到 highlightText(_:inTextView:) 方法添加:

// 1
let attributedText = textView.attributedText.mutableCopy() as! NSMutableAttributedString
// 2
let attributedTextRange = NSMakeRange(0, attributedText.length)
attributedText.removeAttribute(
  NSAttributedString.Key.backgroundColor, 
  range: attributedTextRange)
// 3
if let searchOptions = self.searchOptions, 
   let regex = try? NSRegularExpression(options: searchOptions) {
  let range = NSRange(textView.text.startIndex..., in: textView.text)
  if let matches = regex?.matches(in: textView.text, options: [], range: range) {
    // 4
    for match in matches {
      let matchRange = match.range
      attributedText.addAttribute(
        NSAttributedString.Key.backgroundColor, 
        value: UIColor.yellow, 
        range: matchRange
      )
    }
  }
}

// 5
textView.attributedText = (attributedText.copy() as! NSAttributedString)

以上代码解释如下:

  1. 首先,获得 textview 的 attributedText 的可变拷贝。
  2. 然后,创建一个包含整个文本的 NSRange,移除已经存在的背景色。
  3. 在查找替换时,用之前创建的构造函数创建一个正则表达式对象,获取匹配结果数组。
  4. 遍历每个匹配结果,添加黄色的背景色。
  5. 最后,用高亮后的文本替换 UITextView 中的内容。

Build & run。尝试搜索各种单词和词组!你会看到全文中的搜索词都高亮了,如下图所示:

正则表达式教程: 开始

试图搜索 the 这个词,查看加上各种选项的效果。注意,例如整词搜索,在 then 中的 the 字就不会高亮。

同样测试搜索替换功能,看看你的文本有没有如期望的一样被替换。同样,再试试 match case (大小写敏感)和 whole words (整词匹配)选项。

高亮和替换文字都好了。但 app 中的另一个正则表达式功能怎么做呢?

校验数据

许多 app 都会有用户输入功能,比如用户输入 email 地址或电话号码。你想对用户输入进行某些校验,以确保数据完整性并通知用户输入错误。

正则表达式能完美胜任各种数据校验,因为它在模式匹配方面非常擅长。

需要在你的 app 中添加两个地方:校验模式自身,以及用这些模式进行用户校验的机制。

作为练习,请用正则表达式校验下列字符串(忽略大小写敏感的问题):

  • first name(名):应当包含标准英文字符,长度在 1-10 之间。
  • middle initial (中名首字母):包含单个英文字符。
  • last name(姓):包含标准英文字符+撇号(比如 O’Brian)、连字号(比如 Randell-Nash) ,长度在 2-20 之间。
  • 超级大坏蛋的名字:包含标准的英文字符、撇号、点号、连字号、数字、空格,长度 2-10 字符。 例如:Ra’s al Ghul, Two-Face 和 Mr. Freeze.
  • 密码:至少 8 个字符,包括 1 个大写字符、1 个小写字符、1 个数字和一个非数字字母字符。这才是重点!

当然,在开发时,你可以用 materials 文件夹下的 iRegex playground。

你要怎样使用这些正则表达式?如果你感觉困难,只要回到教程顶部的清单,在上面寻找对你有帮助的部分。

下面是答案。但是,在进一步阅读之前,请先自己进行尝试,并对比答案:

  "^[a-z]{1,10}$",    // First name
  "^[a-z]$",          // Middle Initial
  "^[a-z'\\-]{2,20}$",  // Last Name
  "^[a-z0-9'.\\-\\s]{2,20}$"  // Super Villain name
  "^(?=\\P{Ll}*\\p{Ll})(?=\\P{Lu}*\\p{Lu})(?=\\P{N}*\\p{N})(?=[\\p{L}\\p{N}]*[^\\p{L}\\p{N}])[\\s\\S]{8,}$" // Password validator

打开 AccountViewController.swift 在 viewDidLoad() 中添加代码:

textFields = [
  firstNameField, 
  middleInitialField, 
  lastNameField, 
  superVillianNameField, 
  passwordField 
]

let patterns = [ "^[a-z]{1,10}$",
                 "^[a-z]$",
                 "^[a-z'\\-]{2,20}$",
                 "^[a-z0-9'.\\-\\s]{2,20}$",
                 "^(?=\\P{Ll}*\\p{Ll})(?=\\P{Lu}*\\p{Lu})(?=\\P{N}*\\p{N})(?=[\\p{L}\\p{N}]*[^\\p{L}\\p{N}])[\\s\\S]{8,}$" ]

  regexes = patterns.map {
    do {
      let regex = try NSRegularExpression(pattern: $0, options: .caseInsensitive)
      return regex
    } catch {
      #if targetEnvironment(simulator)
      fatalError("Error initializing regular expressions. Exiting.")
      #else
      return nil
      #endif
    }
  }

将 view controller 中的 texst field 添加到一个数组中,并创建另一个数组用于正则式模板。然后用 Swift 的 map 函数创建一个 NSRegularExpression 数组,每个模板创建一个正则表达式对象。如果创建正则表达式对象失败,对于开发环境,可以在模拟器中抛出一个 fatalError,但是对于生产环境则忽略,因为你不想让用户的 app 崩了!

为了创建用于校验 first name 的正则表达式,首先要匹配字符串的开始标记。然后匹配 A-Z 字符,然后匹配字符串结束标记并确保长度为 1-10 。

接下来两个模板 —— 中名首字母 和 last name —— 遵循同样的逻辑。对于中名,不需要用 {1} 指明长度,因为 1$ 默认只会匹配一个字符。超级大坏蛋名字的模板也类似,但更复杂一些,因为需要支持特殊字符:撇号、连号和点号。

注意,你无需关心大小写问题,这里 —— 你要在实例化正则表达式时才需要关心。

密码校验写的是什么鬼?需要强调一点,这只是为了演示怎样使用正则式才这样用的,不要在真正的 app 中这么用!

然后再来看它是怎样子使用的。首先,讲几个正则表达式中的概念:

  • (小括号) 表示你的正则表达式中的一个捕获组。
  • 当捕获组以 ?= 开头时,表示这个组使用“正向先行断言”,只有自身出现的位置的后面能匹配某个表达式才表示匹配。例如,A(?=B)匹配 A,但只有它后面跟随有一个 B 才匹配 A。先行断言是一种断言,比如 ^ 或 $,自身不会消费任何字符。
  • \p{} 用于匹配某一类(Category) Unicode 字符,而 \P{} 则匹配不属于某一类的 Unicode 字符。所谓的某一类字符可以是所有字母 (\p{L}),所有大写字母 (\p{Lu}) 或者数字 (\p{N})。

可以将这个表达是分成几段:

  • ^ 和 $ 很简单,匹配一行的开头和结束。
  • (?=\P{Ll}*\p{Ll}) 匹配(但不会消费)任意数量的非小写的 unicode 字符,以及后面跟一个小写 unicode 字符,也就是匹配一个至少 1 个小写字符的字符串。
  • (?=\P{Lu}*\p{Lu}) 和上面类似,确认至少 1 个大写字符。
  • (?=\P{N}*\p{N}) 确认至少 1 位数字。
  • (?=[\p{L}\p{N}]*[^\p{L}\p{N}]) 使用了 ^ 取反模式,确认至少 1 位非数字字母字符。
  • 最后是 [\s\S]{8,} 匹配任意 8 个以上空白字符和非空白字符。

嘘,总算搞定!

你可以从正则表达式中获得许多灵感。解决上述问题还有其他方法,比如用 \d 替换 [0-0]。管它白猫黑猫,只要能抓住老鼠的猫就是好猫。

模式已经有了,你需要在 text field 中去使用它们了。

仍然是 AccountViewController.swift,找到validate(string:withRegex:) 方法并替换为:

let range = NSRange(string.startIndex..., in: string)
let matchRange = regex.rangeOfFirstMatch(
  in: string, 
  options: .reportProgress, 
  range: range
)
return matchRange.location != NSNotFound

在后面的 validateTextField(_? 方法添加下列代码:

let index = textFields.index(of: textField)
if let regex = regexes[index!] {
  if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
    let valid = validate(string: text, withRegex: regex)

    textField.textColor = (valid) ? .trueColor : .falseColor
  }
}

这和你在 SearchViewController.swift 所做的类似。在 validateTextField(_? 中,首先从 regexes 数组中找到对应的 regex 对象,将用户在 text field 中输入的空白字符删除。

然后,在 validate(string:withRegex:) 中,你创建了一个 NSRange,包含了整个文本,用 rangeOfFirstMatch(in:options:range:) 方法检查结果是否匹配。这可能是最高效的检查是否匹配的方法,因为如果它找到第一个匹配结果,方法就会返回。但是如果你想知道总的匹配数有多少的话,可以使用numberOfMatches(in:options:range:)。

最后,将 allTextFieldsAreValid() 的代码替换为:

for (index, textField) in textFields.enumerated() {
  if let regex = regexes[index] {
    if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
      let valid = text.isEmpty || validate(string: text, withRegex: regex)
      
      if !valid {
        return false
      }
    }
  }
}

return true

上面同样也调用了 validate(string:withRegex:) 方法,这个方法简单判断每个非空的 text field 是否校验通过。

运行项目,点击左上角的 Account 图标,在注册表单中输入信息。填完所有字段后,文本会根据是否有效变绿或变红。如下图所示:

正则表达式教程: 开始

保存你的账户。注意,你只能在所有字段校验通过时才能保存。重新启动 app。这次,当 app 打开,会显示一个登录表单,然后才能查看日记中的秘密计划。输入之前创建的密码,然后点击 login。

注:这是正则表达式教程而不是登录认证教程。不要将代码用于登录认证。再次强调,密码是以纯文本的形式存储在设备上的。LoginViewController 中的 loginAction 只检查密码是否和设备上保存的一致,而不会和服务器中存储的密码进行比对。无论如何这都是不安全的。

正则表达式教程: 开始

处理多个查找结果

你还没有用到导航栏上的 Reading Mode 按钮吧。当用户点击它的时候,app 会进入一种“聚焦”模式,高亮文本中的日期时间字段,并对日记中每个子项末尾进行高亮。

打开 SearchViewController.swift 找到关于 Reading Mode 按钮的下列代码:

//MARK: Underline dates, times, and splitters

@IBAction func toggleReadingMode(_ sender: AnyObject) {
  if !self.readingModeEnabled {
    readingModeEnabled = true
    decorateAllDatesWith(.underlining)
    decorateAllTimesWith(.underlining)
    decorateAllSplittersWith(.underlining)
  } else {
    readingModeEnabled = false
    decorateAllDatesWith(.noDecoration)
    decorateAllTimesWith(.noDecoration)
    decorateAllSplittersWith(.noDecoration)
  }
}

上面的方法用 3 个助手方法对日期、时间和分隔线进行修饰。每个方法会带一个 decoration 选项,用于表示是否高亮或者不修饰(移除高亮效果)。如果你查看每个助手方法,你会发现它们是空的!

在关心如何实现修饰效果之前,先来看看如何创建和定义 NSRegularExpression 对象吧。一个简单的方法是在 NSRegularExpression 上创建一个静态变量。回到 RegexHelpers.swift,在 NSRegularExpression 扩展中添加:

static var regularExpressionForDates: NSRegularExpression? {
  let pattern = ""
  return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
}

static var regularExpressionForTimes: NSRegularExpression? {
  let pattern = ""
  return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
}

static var regularExpressionForSplitter: NSRegularExpression? {
  let pattern = ""
  return try? NSRegularExpression(pattern: pattern, options: [])
}

现在,让你来写这些正则表达式模板!要求如下:

日期

  • xx/xx/xx 或 xx.xx.xx 或 xx-xx-xx 格式。日、月、年的字符无所谓,因为代码只是将它们高亮而已。例如:10-05-12.
  • 完整的、或者简写的月份名称(比如 Jan 或者 January,Feb 或者 February),跟随 1-2 位的数字(例如 x 或 xx)。月份日期可以是序数词(比如 1st、2nd、10th、21st 等),然后是逗号分隔,然后是 4 位年份(比如 xxxx)。在月份名称、日和年之间可以有 0 到多个空白字符。比如 March 13th, 2001

时间

查找简单时间比如 9am huozhe 11 pm:1 或 2 位数字 + 0 到多为空格 + 小写的 am 或 pm。

分隔线

一个波浪号(~),长度大于 10。

你可以用那个 playground 文件来测试。看看是否能够写出相应的正则表达式。

给你 3 个正则式模板作为例子。用下列模板替换 regularExpressionForDates 中的空白模板:

(\\d{1,2}[-/.]\\d{1,2}[-/.]\\d{1,2})|((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)((r)?uary|(tem|o|em)?ber|ch|il|e|y|)?)\\s*(\\d{1,2}(st|nd|rd|th)?+)?[,]\\s*\\d{4}

这个模板被 | (或)字符串分成两部分。这表示要么匹配第一部分,要么匹配第二部分。

第一部分是 (\d{1,2}[-/.]\d{1,2}[-/.]\d{1,2})。这意味着两位数字之后是一个 – 或 / 字符,然后是两位数字,然后又是 – 或 / 字符,然后是两位数字。

第二部分的第一部分是 ((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)(®?uary|(tem|o|em)?ber|ch|il|e|y|)?),匹配完整和缩写的月份名。

接下来是 \s*\d{1,2}(st|nd|rd|th)?,匹配 0 或多个空格,加 1-2 位数字,加上一个可选的序数词后缀。例如,1 和 1st。

最后是 [,]\s*\d{4},匹配一个逗号,加 0 或多个空格,加一个 4 位数字年份。

这个正则表达式好恐怖呀!但是,你会发现正则表达式太简洁了,能将大量信息压缩成一个类似于加密的字符串。

接下来的模板是 regularExpressionForTimes 和 regularExpressionForSplitters。将空白模板用下列表达式替换:

// Times
\\d{1,2}\\s*(pm|am)

// Splitters
~{10,}

作为练习,看看你能否根据上面的规范解释这写正则表达式模式。

最后,打开 SearchViewController.swift,实现其中的修饰方法:

func decorateAllDatesWith(_ decoration: Decoration) {
  if let regex = NSRegularExpression.regularExpressionForDates {
    let matches = matchesForRegularExpression(regex, inTextView: textView)
    switch decoration {
    case .underlining:
      highlightMatches(matches)
    case .noDecoration:
      removeHighlightedMatches(matches)
    }
  }
}

func decorateAllTimesWith(_ decoration: Decoration) {
  if let regex = NSRegularExpression.regularExpressionForTimes {
    let matches = matchesForRegularExpression(regex, inTextView: textView)
    switch decoration {
    case .underlining:
      highlightMatches(matches)
    case .noDecoration:
      removeHighlightedMatches(matches)
    }
  }
}

func decorateAllSplittersWith(_ decoration: Decoration) {
  if let regex = NSRegularExpression.regularExpressionForSplitter {
    let matches = matchesForRegularExpression(regex, inTextView: textView)
    switch decoration  {
    case .underlining:
      highlightMatches(matches)
    case .noDecoration:
      removeHighlightedMatches(matches)
    }
  }
}

每个方法都使用了 NSRegularExpression 的静态变量来创建对应的正则表达式。然后搜索匹配的字符串并调用 highlightMatches(_? 方法彩色高亮每个字符串,或者调用 removeHighlightedMatches(_? 去反转样式。如果有兴趣,你可以读一下它们的源码。

Build & run。现在,点击 Reading Mode 图标。你会看到日期、时间和分隔线使用了链接样式进行高亮:

正则表达式教程: 开始

再次点击 Reading Mode,将文本转回默认样式。

这个 app 作为示例已经差不多了,你可以看看为什么时间的正则表达式无法匹配一些很常见的搜索模式?比如,它无法匹配 3:15pm,而只会匹配 28pm。

这是一个课后作业!请重写时间的正则表达式,让它能够匹配更多的时间格式。

特别是,你的答案应该匹配标准 12 小时制的时间格式 ab:cd am/pm。比如:11:45 am,10:33pm,04:12am,但不匹配 2pm,0:00 am 18:44am 9:63pm 或 7:4 am。在 am/pm 前至少有 1 个空格。另外,允许匹配 14:33am 中的 4:33am。

下面是答案之一,但在查看答案之前请先自己尝试写出。用 accompanying.playground 去检验结果。

答案:

"(1[0-2]|0?[1-9]):([0-5][0-9]\\s?(am|pm))"

接下来去哪里?

恭喜!你已经学会了正则表达式的使用。

你可以用下面的 Download Materials 按钮下载本教程完整版的项目代码。

正则表达式使用起来强大而有趣——就像是解决某种数学问题。它的灵活性体现在你能够根据需要创建各种模板,比如过滤输入中的空格,在解析 HTML/XML 时删除标签,或者查找特殊的 XML/HTML 标签 —— 以及更多功能!

再来一个练习……

在真实环境中有许多使用正则表达式进行校验的例子。作为最后的练习,请解释一下这个用于 email 地址校验的正则表达式:

[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?

初一看它就是一堆乱七八糟的字符,但利用你学过的知识(以及参考下面的连接)你会一步一步地理解它,使自己成为一个正则表达式高手!

其它资源

以下列出一些有用的正则表达式资源:

希望你喜欢这篇 NSRegularExpression 教程,有任何问题和建议,请到论坛中留言。

Download Materials


  1. a-z ↩︎

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/20697.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信