WWDC Quick Look 💓 By SwiftGGTeam
Swift Regex: Beyond the basics

Swift Regex: Beyond the basics

元の動画を見る

ハイライト

Swift 5.7 では文字列処理が導入されていますRegex型、通常のリテラル構文、およびRegexBuilderDSL を使用すると、開発者は文字列コンテンツをタイプセーフな方法で照合、抽出、変換し、Foundation の日付/数値パーサーを正規表現に直接埋め込むことができます。

主要内容

文字列の操作は、プログラミングで最も一般的なタスクの 1 つです。ログの解析からデータ検証に至るまで、開発者は毎日、さまざまな文字列一致コードを作成します。

Swift 5.7 より前では、正規表現の処理は主に次のものに依存していました。NSRegularExpression。この API は Objective-C 時代のものであり、使用するのが面倒です。一致する結果は次のとおりです。NSTextCheckingResult、キャプチャ グループには数値インデックスを介してアクセスする必要があり、タイプはすべてStringまたはRange、間違いを犯しやすいです。

Swift 5.7 は、新しい通常のソリューションをもたらします。核となるのは次の 3 つです。

  1. RegexType: 標準ライブラリの新しい通常の型。厳密に型指定され、構成可能です。
  2. 通常のリテラル: コンパイラーは構文をチェックし、Xcode は構文の強調表示をサポートします。
  3. RegexBuilderDSL: Swift コードを使用して規則的なパターンを記述し、自然言語に近い可読性を実現します。

これら 3 つを組み合わせると、文字列処理が「黒魔術」から通常の Swift コードに変わります。

文字列からタイプセーフなマッチングへ

テキストからユーザー ID を抽出したいとします。

name:  John Appleseed,  user_id:  100

以前に使用したNSRegularExpression大量の定型コードを記述する必要があります。 Swift Regex を使用するようになったので、3 つの記述方法から選択できます。

文字列から作成 (実行時解析):

let regex = try Regex(#"user_id:\s*(\d+)"#)

通常のリテラル (コンパイル時チェック):

let regex = /user_id:\s*(\d+)/

RegexBuilder DSL (最高の可読性):

import RegexBuilder

let regex = Regex {
    "user_id:"
    OneOrMore(.whitespace)
    Capture(.localizedInteger)
}

3つの書き方で作成regex全てRegex統合マッチング アルゴリズムに渡すことができるタイプ。

マッチングアルゴリズムのリスト

Swiftの標準ライブラリは、StringそしてSubstring規則性主導のアルゴリズムのバッチを拡張しました (03:49):

let input = "name:  John Appleseed,  user_id:  100"
let regex = /user_id:\s*(\d+)/

input.firstMatch(of: regex)           // 找第一个匹配
input.wholeMatch(of: regex)           // 整个字符串必须匹配
input.prefixMatch(of: regex)          // 前缀匹配
input.starts(with: regex)             // 是否以模式开头
input.replacing(regex, with: "456")   // 替换匹配内容
input.trimmingPrefix(regex)           // 去掉前缀匹配部分
input.split(separator: /\s*,\s*/)     // 按正则分割

規則性は次のような場合にも使用できます。switch声明の中で:

switch "abc" {
case /\w+/:
    print("It's a word!")
}

銀行取引明細書を解析する

Foundation のパーサーを直接埋め込むことが可能RegexBuilder05:14):

let statement = """
    DSLIP    04/06/20 Paypal  $3,020.85
    CREDIT   04/03/20 Payroll $69.73
    """

let regex = Regex {
    Capture(.date(format: "\(month: .twoDigits)/\(day: .twoDigits)/\(year: .twoDigits)"))
    OneOrMore(.whitespace)
    OneOrMore(.word)
    OneOrMore(.whitespace)
    Capture(.currency(code: "USD").sign(strategy: .accounting))
}

ここ.date()そして.currency()Foundation が提供するパーサーで、ローカリゼーションをサポートし、さまざまなエッジ ケースを処理します。日付解析ロジックを自分で記述する必要はありません。

詳細

Capture を使用してデータを抽出する

マッチングは最初のステップにすぎず、データの抽出が鍵となります。Capture一致した部分文字列を保存し、出力タプルに追加できます (10:28)。

let regex = Regex {
   "a"
   Capture("b")
   "c"
   /d(e)f/
}

if let match = "abcdef".wholeMatch(of: regex) {
    let (wholeMatch, b, e) = match.output
    // wholeMatch = "abcdef"
    // b = "b"
    // e = "e"
}

キーポイント:

  • match.outputタプルです。最初の要素は一致全体で、その後に各要素が続きます。Capture
  • 通常のリテラルのかっこ()キャプチャーグループでもある
  • 出力タプルの型はコンパイル時に決定されます

実戦: XCTest ログの解析

テスト スイートのログを解析するとします。

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.

最初のステップは、基本的な規則ルールを記述することです (06:24):

import RegexBuilder

let regex = Regex {
    "Test Suite '"
    /[a-zA-Z][a-zA-Z0-9]*/
    "' "
    ChoiceOf {
        "started"
        "passed"
        "failed"
    }
    " at "
    OneOrMore(.any)
    Optionally(".")
}

キーポイント:

  • ChoiceOf複数のオプションの 1 つと一致する
  • Optionally0 回または 1 回一致
  • 通常のリテラル/[a-zA-Z][a-zA-Z0-9]*/DSLに埋め込み可能

プラスCaptureデータを抽出します (11:10):

let regex = Regex {
    "Test Suite '"
    Capture(/[a-zA-Z][a-zA-Z0-9]*/)
    "' "
    Capture {
        ChoiceOf {
            "started"
            "passed"
            "failed"
        }
    }
    " at "
    Capture(OneOrMore(.any))
    Optionally(".")
}

for line in testInputs {
    if let (whole, name, status, dateTime) = line.wholeMatch(of: regex)?.output {
        print("\(name), \(status), \(dateTime)")
    }
}

貪欲なマッチングの問題への対処

上記のコードを実行すると、次のことがわかりますdateTime最終ピリオドも含みます。その理由は、OneOrMore(.any)デフォルトは熱心で、できるだけ多くの文字に一致します (11:51)。

修正するには、消極モードに変更します。

Capture(OneOrMore(.any, .reluctant))

キーポイント:

  • .reluctant重複が一致する文字数をできるだけ少なくする
  • エンジンはまず次のパターンとの一致を試み、失敗した場合はバックトラックしてさらに多くの文字を消費します。
  • も使用できます.repetitionBehavior(.reluctant)正規表現全体のデフォルトの動作を変更する

キャプチャの変換: 部分文字列から特定の型へ

キャプチャされたコンテンツのデフォルトは、Substring、通常は、より意味のある型に変換する必要があります。Captureサポートtransform閉鎖 (15:20):

Regex {
    Capture {
        OneOrMore(.digit)
    } transform: {
        Int($0)  // 输出类型变成 Int?
    }
}

変換が失敗する可能性がある場合は、次を使用します。TryCapture、これはオプションのタイプを「フラット化」します (15:55):

Regex {
    TryCapture {
        OneOrMore(.digit)
    } transform: {
        Int($0)  // 输出类型是 Int,不是 Int?
    }
}

キーポイント:

  • TryCapture変換は戻りますOptional、ただし出力タイプはオプションではありません
  • 変換が失敗すると、エンジンは自動的にバックトラックし、他の一致するパスを試行します。
  • 失敗可能なイニシャライザでの使用に適しています

Foundation パーサーを使用した日付の処理

ステータスを列挙に変換し、タイムスタンプを次のように変換します。Date16:21):

enum TestStatus: String {
    case started, passed, failed
}

let regex = Regex {
    "Test Suite '"
    Capture(/[a-zA-Z][a-zA-Z0-9]*/)
    "' "
    TryCapture {
        ChoiceOf { "started"; "passed"; "failed" }
    } transform: {
        TestStatus(rawValue: String($0))
    }
    " at "
    Capture(.iso8601(
        timeZone: .current,
        includingFractionalSeconds: true,
        dateTimeSeparator: .space))
    Optionally(".")
}
// 输出类型: Regex<(Substring, Substring, TestStatus, Date)>

キーポイント:

  • .iso8601()Foundation が提供する日付パーサーです
  • 出力タプルの最後の要素は直接Dateタイプ
  • 手動で二次分析を行う必要はありません

カスタム パーサー: C ライブラリへのアクセス

既存の C パーサーにプラグインしたい場合は、次のようにすることができます。CustomConsumingRegexComponentプロトコル (19:16):

import Darwin

struct CDoubleParser: CustomConsumingRegexComponent {
    typealias RegexOutput = Double

    func consuming(
        _ input: String,
        startingAt index: String.Index,
        in bounds: Range<String.Index>
    ) throws -> (upperBound: String.Index, output: Double)? {
        input[index...].withCString { startAddress in
            var endAddress: UnsafeMutablePointer<CChar>!
            let output = strtod(startAddress, &endAddress)
            guard endAddress > startAddress else { return nil }
            let parsedLength = startAddress.distance(to: endAddress)
            let upperBound = input.utf8.index(index, offsetBy: parsedLength)
            return (upperBound, output)
        }
    }
}

カスタム パーサーの使用 (20:13):

let regex = Regex {
    "Test Case "
    OneOrMore(.any, .reluctant)
    "("
    Capture {
        CDoubleParser()
    }
    " seconds)."
}

if let match = input.wholeMatch(of: regex) {
    print("Time: \(match.1)")  // Double 类型
}

キーポイント:

  • consumingこのメソッドは、一致の上限インデックスと解析結果を返します。
  • 戻るnil一致が失敗し、エンジンがバックトラックすることを示します
  • これは、既存の解析ライブラリ (JSON、YAML パーサーなど) へのブリッジです。

重要ポイント

1.ログパーサー

何をすべきか: コンパイル エラーと警告を抽出するプロジェクト用の Xcode ビルド ログ パーサーを作成します。

実行する価値がある理由: 使用するRegexBuilderログ形式を記述するには、次を使用します。TryCaptureエラー情報を構造化データに変換します。これは、通常の文字列よりも保守が容易です。

開始方法: を定義します。BuildLogEntry構造、用途RegexBuilderマッチerror: / warning:接頭辞、使用Captureファイル名、行番号、エラーの説明を抽出します。

2.スマート クリップボード ツール

何をすべきか: クリップボードの内容を自動的に認識し、重要な情報 (配送番号、携帯電話番号、URL など) を抽出する macOS ツールを作成します。

実行する価値がある理由: Swift RegexfirstMatchそしてwholeMatchクリップボードの文字列に直接使用できます。TryCapture一致した結果を特定の型に変換します。

開始方法: 使用するNSPasteboardクリップボードの変更を監視し、テキスト コンテンツで複数の規則的なパターンを試し、一致が成功した後に抽出結果を表示します。

3.設定ファイルバリデータ

やるべきこと: チームの内部構成ファイル形式の検証ツールを作成し、構文エラーをチェックし、わかりやすいプロンプトを表示します。

実行する価値がある理由:RegexBuilder読みやすいため、構成ルールが一目で明確になり、新しいメンバーも検証ロジックをすぐに理解できます。

開始方法: 構成ファイルの各行を次のように定義します。Regex、使用wholeMatch検証。失敗した場合は、特定のエラー位置と予期される形式を返します。

4.データ移行スクリプト

対処方法: 古いシステムのテキスト ログを新しいデータベースに移行するスクリプトを作成し、正規表現を使用してフィールドを抽出し、型を変換します。

実行する価値がある理由:TryCapture協力するDate / Int / Doubleイニシャライザ、正規表現 1 行で抽出と変換を完了でき、二次解析は必要ありません。

開始方法: Foundation を使用する.date()そして.currency()日付および金額フィールドを処理するパーサー、カスタムCustomConsumingRegexComponent特殊な形式を処理します。

関連セッション

  • Swift Regex について — Swift Regex の概要、基本的な構文と中心的な概念を紹介
  • Foundation の新機能 — Foundation のフォーマッタとパーサーが RegexBuilder と連携する仕組み
  • Swift 同時実行性 — Swift 同時実行性モデルを理解し、文字列処理を備えた高性能解析ツールを作成します。

コメント

GitHub Issues · utterances