s

sxw2k

V1

2022/07/30阅读:26主题:橙蓝风

Swift RegexBuilder Vs. Raku Grammar

Swift 5.7 新增了 RegexBuilder, 它能以一种声明式的方式来写正则表达式。然后, 我发现 RegexBuilder 和 Raku(即 Perl 6)中的 Grammar 在语义表达上有些类似, 这两种语言在构建正则表达式的时候, 都非常有表现力, 可读性也很不错, RegexBuilder 中的某些特性甚至比 Raku 的 Grammar 更好用, 所以花了一点点时间粗浅研究了其中的一部分内容。

匹配整个字符串

Swift 中使用 wholeMatch 匹配整个字符串:

import _StringProcessing
import RegexBuilder

let regex = Regex {
  OneOrMore("a")
  Capture {
    OneOrMore(.digit)
  }
}

let input = "aaa12"
if let match = input.wholeMatch(of: regex) {
  let (_, digit) = match.output
  print(digit)
}

其中 Regex {} 语法是使用 RegexBuilder DSL 构造一个正则表达式, OneOrMore 是一个结构体, 它接收一个或多个 CharacterClass, 相当于传统正则表达式中的量词 +。上面声明的名为 regex 的正则表达式的意思是: 匹配一个或多个字符 a, 然后是一个或多个数字, 并捕获这些数字。

Raku Grammar 的等价写法如下:

my $input = "aaa12";
my $match = (grammar :: { token TOP { a+ \d+ }}).parse($input);
say ~$match[0];

因为要解析的文本特别简单, 所以这里使用 grammar :: {} 结构声明了一个匿名的 Grammar。 这个匿名 Grammar 中声明一个一个名为 TOP 的 token, 它匹配一个或多个字符 a, 然后是一个或多个数字。

打印出所有的捕获

import _StringProcessing
import RegexBuilder

let word = OneOrMore(.word)
let space = ZeroOrMore(.whitespace)

let wordPattern = Regex {
  space
  Capture { word }
}

let input = "The quick brown fox jumps over the lazy dog"
for match in input.matches(of: wordPattern) {
  let (_, word) = match.output
  print(word)
}

和 Raku 一样, Swift 还可以把正则表达式保存在变量或常量中。上面的正则表达式捕获了所有的单词, 其中 word 常量的意思是匹配一个或多个字符, space 常量的意思是匹配零个或多个空格。wordPattern 也是一个正则表达式, 它由 spaceword 这两个子正则表达式组合而成, 这和 Raku 的 Grammar 的思想是一样的。

上面的代码会打印出每一个单词:

The
quick
brown
fox
jumps
over
the
lazy
dog

使用 Raku Grammar 的等价写法如下:

grammar WordPattern {
    token TOP  { <word>+ % \s+ }
    token word { (\w+) }
}

my $text = "The quick brown fox jumps over the lazy dog";
my $match = WordPattern.parse($text);
.say for $match<word>;

WordPattern 这个 Grammar 的意思是: 匹配一个或多个单词, 这些单词之间由一个或多个空格分隔。% 是 Raku 中的一个非常好用的正则表达式操作符, 对于匹配由一个或多个分隔符分隔的字符串特别方便:

'1,22,3,44' ~~ /[\d+]+ % ','/
'a,b;c.d;e' ~~ /\w+ % <[,;.]>/

解析行程数据

有一种行程数据, 其格式如下:

Russia
    Vladivostok : 43.131621,131.923828 : 4
    Ulan Ude : 51.841624,107.608101 : 2
    Saint Petersburg : 59.939977,30.315785 : 10
Norway
    Oslo : 59.914289,10.738739 : 2
    Bergen : 60.388533,5.331856 : 4
Ukraine
    Kiev : 50.456001,30.50384 : 3
Switzerland
    Wengen : 46.608265,7.922065 : 3
    Bern : 46.949076,7.448151 : 1

例如 Norway 表示国家, Oslo 和 Bergen 是目的地, 59.914289,10.738739 是逗号分割的经纬度, 最后的 2 和 4 是售票数。

使用 Swift 的 RegexBuilder 写出来大概是下面这样的(这里是从 Raku Gramamr 翻译成 Swift 的 RegexBuilder 语法):

import _StringProcessing
import RegexBuilder

let word = OneOrMore(.word)

let integer = Regex {
    Optionally { "-" }
    OneOrMore(.digit)
}

let num = Regex {
    Optionally { "-" }
    OneOrMore(.digit)
    Optionally {
        Regex {
          "."
          OneOrMore(.digit)
        }
    }
}

let name = Regex {
    OneOrMore(.word)
    ZeroOrMore {
        Regex {
            One(.whitespace)
            OneOrMore(.word)
        }
    }
}

let destination = Regex {
    OneOrMore(.whitespace)
    name
    OneOrMore(.whitespace)
    ":"
    OneOrMore(.whitespace)
    num
    ","
    num
    OneOrMore(.whitespace)
    ":"
    OneOrMore(.whitespace)
    integer
    .anchorsMatchLineEndings()
}

let country = Regex {
    name
    .anchorsMatchLineEndings()
    OneOrMore { destination }
}

let tripPattern = Regex {
    Capture {
      OneOrMore { country }
    }
}

let text = """
Russia
    Vladivostok : 43.131621,131.923828 : 4
    Ulan Ude : 51.841624,107.608101 : 2
    Saint Petersburg : 59.939977,30.315785 : 10
Norway
    Oslo : 59.914289,10.738739 : 2
    Bergen : 60.388533,5.331856 : 4
Ukraine
    Kiev : 50.456001,30.50384 : 3
Switzerland
    Wengen : 46.608265,7.922065 : 3
    Bern : 46.949076,7.448151 : 1
"""


for match in text.matches(of: tripPattern) {
  let (_, trip) = match.output
  print(trip)
}

几乎等价的 Raku Grammar 写法如下:

my $input = q:to/END/;
Russia
    Vladivostok : 43.131621,131.923828 : 4
    Ulan Ude : 51.841624,107.608101 : 2
    Saint Petersburg : 59.939977,30.315785 : 10
Norway
    Oslo : 59.914289,10.738739 : 2
    Bergen : 60.388533,5.331856 : 4
Ukraine
    Kiev : 50.456001,30.50384 : 3
Switzerland
    Wengen : 46.608265,7.922065 : 3
    Bern : 46.949076,7.448151 : 1
END

grammar SalesExport {
    token TOP { ^ <country>+ $ }
    token country {
        <name> \n
        <destination>+
    }
    token destination {
        \s+ <name> \s+ ':' \s+
        <lat=.num> ',' <long=.num> \s+ ':' \s+
        <sales=.integer> \n
    }
    token name    { \w+ [ \s \w+ ]*   }
    token num     { '-'? \d+ [\.\d+]? }
    token integer { '-'? \d+          }
}

my $match = SalesExport.parse($input);
.Str.say for $match;

这个 Grammar 在以前的公众号文章中出现过, 这里不再多说。

解析交易数据

到现在为止, 你可能会说 Swift 的 RegexBuilder 平平无奇, 但是让我感到意外的是它支持 Foundation 框架中的正则表达式解析器。

例如, 在解析字符串中的日期、货币、数字等结构的时候, 就可以使用 Foundation 中的解析器:

import RegexBuilder
import Foundation

let fieldSeparator = /\s{2,}|\t/

let regex = Regex {
  Capture { /CREDIT|DEBIT/ }
  fieldSeparator

  Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) }
  fieldSeparator

  Capture {
    OneOrMore {
      NegativeLookahead { fieldSeparator }
      CharacterClass.any
    }
  }
  fieldSeparator
    Capture { One(.localizedCurrency(code: "USD", locale: Locale(identifier: "en_US"))) }
}
// 这里的 Date 和 NSDecimal 不再是字符串类型了, 而是一个结构化的东西
// Regex<(Substring, Substring, Foundation.Date, Substring, __C.NSDecimal)>

let input = """
CREDIT    03/02/2022    Payroll from employer         $200.23
CREDIT    03/03/2022    Suspect A                     $2,000,000.00
DEBIT     03/03/2022    Ted's Pet Rock Sanctuary      $2,000,000.00
DEBIT     03/05/2022    Doug's Dugout Dogs            $33.27
"""


for match in input.matches(of: regex) {
    let (_, kind, date, description, amount) = match.output
    print(kind, date, description, amount)
}

传统正则表达式语法和 RegexBuilder 语法可以混用(仅限于 Xcode, Docker 中这样用会报错)。上面的 .date 语法是 Foundation 中的日期解析器, .localizedCurrency 是 Foundation 中的货币解析器, 这里在 RegexBuilder 中引入 Foundation 框架中已经封装好的日期和货币解析器, 就很高级。 Raku 中还没有这样的封装, 但是我觉得应该也可以在核心 core 中实现。

几乎等价的 Raku Grammar 写法如下:

grammar TransactionGrammar {
    token TOP              { <transaction>+ %% \n*                             }
    rule  transaction      { <payment> <date> <description> <cost>             }
    token payment          { 'CREDIT' | 'DEBIT'                                }
    token date             { <digit-sequence>+ % '/'                           }
    token description      { [<-[\s]>+]+ % \s                                  }
    token cost             { <currency-sign> <currency-number>                 }
    token digit-sequence   { \d+                                               }
    token currency-sign    { '$'                                               }
    token currency-number  { <digit-sequence>+ % <[.,]>                        }
}

my $input = q:to/END/;
CREDIT  03/02/2022  Payroll from employer       $200.23
CREDIT  03/03/2022  Suspect A                   $2,000,000.00
DEBIT   03/03/2022  Ted's Pet Rock Sanctuary    $2,000,000.00
DEBIT   03/05/2022  Doug's Dugout Dogs          $33.27
END

my $match = TransactionGrammar.parse($input);
.say for $match;

transform vs. action

当我发现 Swift 的 RegexBuilder 还支持 transform 的时候, 我再次感到高级。

Swift 的 RegexBuilder 还支持对匹配到的字符串进行转换, 其结果就是把字符串转换为结构化的东西。例如, 下面的 transfrom 闭包把捕获到的字符串转换成了枚举:

import Foundation
import RegexBuilder

enum TestStatusString {
    case started = "started"
    case passed = "passed"
    case failed = "failed"
}

let regex = Regex {
    "Test Suite '"
    Capture(OneOrMore(.word))
    "' "
    TryCapture {
        ChoiceOf{
            "started"
            "passed"
            "failed"
        }
    } transform: {
       TestStatus(rawValue: String($0))
    }
    " at "
    Capture(.iso8601(
        timeZone: .current, includingFractionalSeconds: true, dateTimeSeparator: .space
    ))
    Optionally(".")
// Regex<(Substring, Substring, TestStatus, Foundation.Date)>

let testSuitTestInputs = [
    "Test Suite 'RegexDSLTests' started at 2022-06-06 09:41:00.001",
    "Test Suite 'RegexDSLTests' failed at 2022-06-06 09:41:00.001.",
    "Test Suite 'RegexDSLTests' passed at 2022-06-06 09:41:00.001.",
]

for line in testSuitTestInputs {
    if let match = line.wholeMatch(of: regex) {
        let (_, name, status, dateTime) = match.output
        print("Matched: \"\(name)\", \"\(status)\", \"\(dateTime)\"")
    }
}

Raku Grammar 的对应写法是使用 Action 对象, 咱们下个月再写。

环境

Swift 支持 Linux/Mac/iOS/iPadOS, RegexBuilder 是 Swfit 5.7 引进的新特性, 上面未使用 Foundation 框架的 Swift 代码是跑在 Docker 中的, 使用了 nightly 版本的 Swift 构建:

docker pull swiftlang/swift:nightly-main-centos7
docker run -d swiftlang/swift:nightly-main-centos7

swift --version
Swift version 5.8-dev (LLVM b2416e1165ab97c, Swift 965a54f037cfa76)
Target: x86_64-unknown-linux-gnu

使用了 Foundation 框架的代码是使用 Xcode 中的 Playground 来运行的。

总结

Raku ❤️ Swift

分类:

后端

标签:

后端

作者介绍

s
sxw2k
V1