蘇揚電氣 發表於 2023-4-7 10:47:43

Swift 中的 RegexBuilder学习指南

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>前言</li><li>Regex 字面量</li><ul class="second_class_ul"><li>RegexBuilder - 像写代码一样写正则</li><li>RegexRepetitionBehavior</li></ul><li>Foundation 的支持</li><ul class="second_class_ul"></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>前言</h2>
<div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t3229/39/2924280771/392779/d242f52a/57e8d68eN57129bbf.jpg" data-name="精通Swift设计模式(图灵出品)" data-owner="京东自营" data-price="77.4" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&amp;p=JF8BAMoJK1olXwUFXVxUDU8eC18IG1MTWgIAVm4ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYCXFhaDEkVHDZNRwYlO3wcAlw8dU1yeyd2RTBMGW0AKwoGaEcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD0IWCm0MEloRWQELZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGWwQCUVleZhZBBycBRBBRMwULUllaAU4XB18KGloXXzYy"></div></div>
<p>在我们日常的项目开发中,经常会碰到和正则表达式打交道的时候。比如用户密码,通常会要求同时包含小写字母、大写字母、数字,并且长度不少于 8 位,以此来提高密码的安全性。</p>
<p>在 Swift 中,我们可以用正则表达式的字面量方式来进行实现。</p>
<p class="maodian"></p><h2>Regex 字面量</h2>
<p>Regex 字面量实现代码:</p>
<div class="jb51code"><pre class="brush:py;">let regex = /^(?=.*)(?=.*)(?=.*\d){8,}$/
let text = "Aa11111111"
print(text.matches(of: regex).first?.output) // Optional("Aa11111111")
</pre></div>
<p>通过上述代码可以看到,<code>//</code>通过两个斜线就可以来生成正则的字面量。用字面量的方式确实可以使代码很简洁,但简洁的代价就是很难看懂,对后面的代码维护也造成了很大的困难。</p>
<p>就像网上盛传的一句梗一样:&ldquo;我有一个问题,所以我写了一个正则表达式。现在,我有了两个问题。&rdquo;😂</p>
<p>对于 Regex 难懂且难维护的问题,Swift 的开发团队给出的方案就是:RegexBuilder。</p>
<p class="maodian"></p><h3>RegexBuilder - 像写代码一样写正则</h3>
<p>假设我们有一个字符串&quot;name: John Appleseed, user_id: 100&quot;,想要提取其中user_id的值。 首先第一步,先导入 RegexBuilder:</p>
<div class="jb51code"><pre class="brush:py;">import RegexBuilder
</pre></div>
<p>接着,通过结构体 <code>Regex</code> 来构建正则语句:</p>
<div class="jb51code"><pre class="brush:py;">let regex = Regex {
    "user_id:" // 1
    OneOrMore(.whitespace) // 2
    Capture(.localizedInteger(locale: Locale(identifier: "zh-CN"))) // 3
}
</pre></div>
<p>第一行代码匹配的是固定字符串:&quot;user_id&quot;,第二行代码匹配的是一个或者多个空格,第三行代码则是匹配的整型数字。</p>
<p><code>localizedInteger</code> 会将匹配到的数字自动转为整型,比如下面的例子:</p>
<div class="jb51code"><pre class="brush:py;">let input = "user_id:100.11"
let regex = Regex {
    Capture(.localizedInteger(locale: Locale(identifier: "zh-CN")))
}
if let match = input.firstMatch(of: regex) {
    print("Matched: \(match.0)") // Matched:100.11
    print("User ID: \(match.1)") // User ID: 100
}
</pre></div>
<p>虽然匹配的是 100.11,但输出的仍然是 100。</p>
<p>最后,就可以通过 macth 的相关函数来进行数据提取了:</p>
<div class="jb51code"><pre class="brush:py;">if let match = input.firstMatch(of: regex) {
    print("Matched: \(match.0)")
    print("User ID: \(match.1)")
}
</pre></div>
<p class="maodian"></p><h3>RegexRepetitionBehavior</h3>
<p>该结构体是用来定义匹配的重复行为的,它有三个值:</p>
<ul><li>edger:会尽可能多的去匹配输入的字符,必要的时候会回溯。默认为edger</li><li>reluctant:会尽可能少的去匹配输入的字符,它会根据你的需求来一点点增大匹配区域,以完成匹配。</li><li>possessive:会尽可能多的去匹配输入的字符,不会回溯。</li></ul>
<p>比如下面这个例子:</p>
<div class="jb51code"><pre class="brush:py;">let testSuiteTestInputs = [    "2022-06-06 09:41:00.001",    "2022-06-06 09:41:00.001.",    "2022-06-06 09:41:00.001."]
let regex = Regex {
    Capture(OneOrMore(.any))
    Optionally(".")
}
for line in testSuiteTestInputs {
    if let (dateTime) = line.wholeMatch(of: regex)?.output {
      print("Matched: \(dateTime)\"")
    }
}
</pre></div>
<p>因为这三条数据最后的<code>.</code>是不一定有的,所以我们的正则有一个 <code>Optionally(&quot;.&quot;)</code>。但匹配出来的 dateTime 还是会带 <code>.</code>。因为 edger 会匹配所有的字符包含最后的点在内,这样 <code>Optionally(&quot;.&quot;)</code> 根本不会起作用。</p>
<p>改成 <code>Capture(OneOrMore(.any, .reluctant))</code>则会修复这个问题。因为 <code>reluctant</code> 它是匹配尽可能少的输入,所以最后的<code>Optionally(&quot;.&quot;)</code>会执行。</p>
<p>在 Swift 5.7 中,Foundation 框架也对 RegexBuilder 进行适配。所以对于 Date、URL等类型,我们可以借助 Foundation 的强大功能来进行解析。</p>
<p class="maodian"></p><h2>Foundation 的支持</h2>
<p>假如,我们在做一个金融相关的 APP,为了兼容一些老数据,需要将一些字符串类型的数据转为结构体。</p>
<p>这是我们的字符串数据:</p>
<div class="jb51code"><pre class="brush:py;">let statement = """
CREDIT    2022/03/03    张三   ¥2,000,000.00
DEBIT   03/03/2022    Tom      $2,000,000.00
DEBIT
</pre></div>
<p>这是我们需要转的结构体:</p>
<div class="jb51code"><pre class="brush:py;">struct Trade {
    let type: String
    let date: Date
    let name: String
    let count: Decimal
}
</pre></div>
<p>下面这个就是我们需要编写的 Regex:</p>
<div class="jb51code"><pre class="brush:py;">let regex = Regex {
    Capture {
      /CREDIT|DEBIT/
    }
    OneOrMore(.whitespace)
    Capture {
      One(.date(.numeric, locale: Locale(identifier: "zh_CN"), timeZone: .gmt))
    }
    OneOrMore(.whitespace)
    Capture {
      OneOrMore(.word)
    }
    OneOrMore(.whitespace)
    Capture {
      One(.localizedCurrency(code: "CNY", locale: Locale(identifier: "zh_CN")))
    }
}
</pre></div>
<p>首先,我们需要匹配固定的字符串:CREDIT/DEBIT,接着是匹配一个或者多个空格。</p>
<p>接下来就是 Foundation 的重头戏了,对于日期类型的字符串,我们并不需要写一些匹配年月日规则的正则,只需要借助 Foundation 内嵌的功能即可。这样做不仅省去了我们自己编写的时间,更重要的是:官方写的要比我们自己写的更能保证代码的正确性。</p>
<p>需要注意的是,Apple 推荐我们显式的写出 locale 属性,而不是下面这种跟随系统写法 :</p>
<p>❌</p>
<div class="jb51code"><pre class="brush:py;">One(.date(.numeric, locale: Locale.current, timeZone: TimeZone.current))
</pre></div>
<p>因为这种写法会带来多种预期,并不能保证数据的确定性。</p>
<p>匹配完日期,接着就是对空格和用户名的匹配。最后,是对交易金额的匹配,金额也是 Foundation 提供的函数来进行的匹配。</p>
<p>测试代码:</p>
<div class="jb51code"><pre class="brush:py;">let result = statement.matches(of: regex)
var trades = ()
result.forEach { match in
    let (_, type, date, name, count) = match.output
    trades.append(Trade(type: String(type), date: date, name: String(name), count: count))
}
print(trades)
//
</pre></div>
<p>通过打印可以得知,输出的结果并不符合预期,漏掉了 Tom 那条数据。漏掉的原因可以通过代码一眼得知:因为对日期和金额我们显式的指定了是中国的格式,显然<code>03/03/2022</code> 这种格式是不符合年月日的格式的。这也体现了显式指定格式的好处:方便排查问题。</p>
<p>我们只要将日期格式转为年月日格式,再将 $ 转为 ¥ 即可让正则正确匹配。</p>
<p>首先,我们需要根据 currency 来来返回正确的 Date 类型:</p>
<div class="jb51code"><pre class="brush:py;">func pickStrategy(_ currency: Substring) -&gt; Date.ParseStrategy {
switch currency {
case "$": return .date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)
case "¥": return .date(.numeric, locale: Locale(identifier: "zh_CN"), timeZone: .gmt)
default: fatalError("We found another one!")
}
}
</pre></div>
<p>接着,编写正则表达式来获取相应的字符串字段:</p>
<div class="jb51code"><pre class="brush:py;">let regex1 = #/
(?&lt;date&gt;   \d{2} / \d{2} / \d{4})
(?&lt;name&gt;   \P{currencySymbol}+)
(?&lt;currency&gt; \p{currencySymbol})
/#
</pre></div>
<p>注:<code>#//#</code>格式为 Swift 中运行时正则表达式的格式。</p>
<p>最后,再调用 replace 函数来进行符合正则的字符替换:</p>
<div class="jb51code"><pre class="brush:py;">statement.replace(regex1) { match -&gt; String in
    print(match.currency)
    let date = try! Date(String(match.date), strategy: pickStrategy(match.currency))
    // ISO 8601, it's the only way to be sure
    let newDate = date.formatted(.iso8601.year().month().day())
    return newDate + match.name + "¥"
}
statement = statement.replacingOccurrences(of: "-", with: "/")
</pre></div>
<p>这样,我们就能解析出符合我们需求的 Trade 类型的数据了。</p>
<p class="maodian"></p><h2>总结</h2>
<ul><li>RegexBuilder 会使代码更加易读易维护</li><li>RegexRepetitionBehavior 的三个值的区别</li><li>尽可能多的使用 Foundation 提供的函数来解析数据</li><li>使用 Foundation 时要指定格式解析数据,这样可以保证数据的唯一性</li></ul>
<p>参考链接</p>
<ul><li>用户密码正则表达式来源</li><li>RegexBuilder</li></ul>
<p>以上就是Swift 中的 RegexBuilder学习指南的详细内容,更多关于Swift RegexBuilder的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>Swift 并发修改Sendable 闭包实例详解</li><li>Swift之for循环的基础使用学习</li><li>Swift简单快速的动态更换app图标AppIcon方法示例</li><li>Swift自动调整视图布局AutoLayout和AutoresizingMask功能详解</li><li>Swift 中 Opaque Types学习指南</li><li>Swift中的高阶函数功能作用示例详解</li><li>Swift并发系统并行运行多个任务使用详解</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Swift 中的 RegexBuilder学习指南