WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift Regex

Meet Swift Regex

元の動画を見る

ハイライト

Swift 5.7 では言語レベルで Regex が導入され、サポートされています。/.../3 つの作成方法: リテラル、ランタイム構築、および Regex Builder、強力な型キャプチャ、Foundation パーサー埋め込み、Unicode 正しい処理、およびローカル トレースバック制御などの高度な機能。

主要内容

文字列解析の問題点

あなたは財務調査員と協力して取引分析ツールを開発しています。彼らがあなたに与えるデータは、プレーンテキスト文字列の束です:

DEBIT     03/05/2022    Doug's Dugout Dogs         $33.27

フィールドは「3 つ以上のスペース」または「タブ」で区切られていますが、その理由は誰も覚えていません。日付の形式があいまいなので、月/日/年であることを祈るしかありません。

(01:16)String設定されたアルゴリズムを試してみますか?split(whereSeparator: \.isWhitespace)「Doug’s Dugout Dogs」を 3 つのフィールドに分割します。インデックスレベルにドロップダウンして手動で解析しますか?コードは冗長でエラーが発生しやすくなります。

var slice = transaction[...]

func extractField() -> Substring {
    let endIdx = {
        var start = slice.startIndex
        while true {
            guard let spaceIdx = slice[start...].firstIndex(where: \.isWhitespace) else {
                return slice.endIndex
            }
            if slice[spaceIdx] == "\t" {
                return spaceIdx
            }
            let afterSpaceIdx = slice.index(after: spaceIdx)
            if afterSpaceIdx == slice.endIndex || slice[afterSpaceIdx].isWhitespace {
                return spaceIdx
            }
            start = afterSpaceIdx
        }
    }()
    defer { slice = slice[endIdx...].drop(while: \.isWhitespace) }
    return slice[..<endIdx]
}

let kind = extractField()
let date = try Date(String(extractField()), strategy: Date.FormatStyle(date: .numeric))
let account = extractField()
let amount = try Decimal(String(extractField()), format: .currency(code: "USD"))

このコードは 20 行を超えており、フィールド分割のみを処理します。より良いツールが必要です。

正規表現リテラル: 簡潔な構文

(02:47) Swift 5.7 の導入/.../正規表現リテラル構文は、Perl、Python、Ruby、Java などの主流の正規言語と互換性があります。

let digits = /\d+/
// digits: Regex<Substring>

キーポイント:

  • コンパイラは通常の構文を理解し、構文の強調表示とコンパイル時のエラー チェックを提供します。
  • 出力タイプは汎用ですRegex<Output>、キャプチャ タイプ情報が含まれます

まず正規表現を使用してフィールド分割の問題を解決します。

let transaction = "DEBIT     03/05/2022    Doug's Dugout Dogs         $33.27"

let fragments = transaction.split(separator: /\s{2,}|\t/)
// ["DEBIT", "03/05/2022", "Doug's Dugout Dogs", "$33.27"]

03:56\s{2,}2 つ以上の空白文字と一致します。|または、\tタブ文字と一致します。 1 行のコードで 20 行を超えるインデックス付けが置き換えられます。

まだ利用可能replacing区切り文字を統一します。

let normalized = transaction.replacing(/\s{2,}|\t/, with: "\t")
// DEBIT\t03/05/2022\tDoug's Dugout Dogs\t$33.27

Regex Builder: 宣言型文字列処理

(03:44) リテラルは簡潔ですが、複雑な正規表現は読みにくいです。 Regex Builder は宣言構文を提供します。

import RegexBuilder

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

let transactionMatcher = Regex {
    /CREDIT|DEBIT/
    fieldSeparator
    One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt))
    fieldSeparator
    OneOrMore {
        NegativeLookahead { fieldSeparator }
        CharacterClass.any
    }
    fieldSeparator
    One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))
}

キーポイント:

  • Regex { ... }SwiftUI の ViewBuilder に似た宣言型ビルダーです
  • リテラルを混在させることができます (/CREDIT|DEBIT/) とビルダーコンポーネント
  • One(.date(...))Foundation の日付パーサーに直接埋め込む
  • One(.localizedCurrency(...))通貨パーサーを埋め込む

06:55NegativeLookahead重要なコンポーネントです。次の入力を「覗き見」しますが、それを消費することはなく、区切り文字が見つかったときにフィールドの内容の一致が停止されるようにします。

OneOrMore {
    NegativeLookahead { fieldSeparator }  // 偷看:下一个不是分隔符才继续
    CharacterClass.any                    // 匹配任意字符
}

いいえNegativeLookaheadCharacterClass.any行末まで貪欲に一致させてから、最後までバックトラックするため、非効率的です。

強い型のキャプチャ

(09:04)Capture関心のあるフィールドを抽出すると、タイプが直接反映されます。Regex汎用パラメータでは次のようになります。

let transactionMatcher = 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"))) }
}
// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>

キーポイント:

  • 0 番目のキャプチャは、一致したテキスト全体です (Substring
  • 1 番目のキャプチャはトランザクション タイプ (Substring
  • 2 番目のキャプチャは日付 (Date、Foundation パーサーによって直接出力されます)
  • 3 番目のキャプチャは施設名 (Substring
  • 4回目の捕獲量は(Decimal、Foundation パーサーによって直接出力されます)

以下を使用する場合は、厳密に型指定されたプロパティに直接アクセスします。

func parseLine(_ line: Substring) throws -> MailmapEntry {
    let regex = /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/

    guard let match = line.prefixMatch(of: regex) else {
        throw MailmapError.badLine
    }

    return MailmapEntry(name: match.1, email: match.2)
}

実行時の構築および拡張の区切り文字

(03:20) 一部の正規表現は、実行時にユーザー入力から構築する必要があります。

let runtimeString = #"\d+"#
let digits = try Regex(runtimeString)
// digits: Regex<AnyRegexOutput>

キーポイント:

  • ランタイム構造が構文エラーをスローする可能性がある
  • 出力タイプは次のとおりです。AnyRegexOutput、コンパイル時にはキャプチャ情報が不明であるため、

(10:53) スラッシュを含む正規表現の場合は、拡張区切り文字を使用してください#/.../#

let regex = #/
  (?<date>     \d{2} / \d{2} / \d{4})
  (?<middle>   \P{currencySymbol}+)
  (?<currency> \p{currencySymbol})
/#
// Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>

キーポイント:

  • #/.../#正規表現内でエスケープされていないスラッシュを許可する
  • 拡張構文モードでは空白は無視され、読みやすさを向上させるためにインデントを使用できます。
  • (?<name>...)これは名前付きキャプチャであり、出力タイプには次のラベルが付いています。

Unicode は正しく処理されます

(12:45) Swift の String は、デフォルトで Unicode 拡張書記素クラスター (文字レベル) を処理します。正規表現もこのモデルに従います。

let aZombieLoveStory = "🧟‍♀️💖🧠"
// Characters: 🧟‍♀️, 💖, 🧠

aZombieLoveStory.unicodeScalars
// U+1F9DF, U+200D, U+2640, U+FE0F, U+1F496, U+1F9E0

(14:12) Swift 文字列は Unicode の正規同等性に従います。"café"そして"cafe\u{301}"基になる Unicode スカラーが異なっていても、比較の結果は等しくなります。

正規表現はデフォルトで文字レベルでも一致します。

input.firstMatch(of: /.\N{SPARKLING HEART}./)
// 🧟‍♀️💖🧠  — 匹配三个字符

input.firstMatch(of: /.\N{SPARKLING HEART}./.matchingSemantics(.unicodeScalar))
// ️💖🧠  — 匹配三个 Unicode scalar,注意前面的 variation selector

キーポイント:

  • デフォルト.matchingSemantics(.graphemeCluster)Swift キャラクターと一致する
  • .matchingSemantics(.unicodeScalar)Unicode スカラー レベルの一致
  • ほとんどのシナリオではデフォルトの動作を使用し、正確な制御が必要な場合にのみ切り替えます。

ローカルトレースバック制御

(21:45) 通常のバックトラック メカニズムは諸刃の剣です。グローバル バックトラッキングは検索やあいまい一致に適していますが、完全一致ではパフォーマンスの問題が発生する可能性があります。

let fieldSeparator = /\s{2,}|\t/
// 8 个空格时,匹配全部 8 个;后续失败时回溯到 7 个、6 个...

Localビルダーはローカル ルックバック スコープを作成し、一致が成功した後に未試行の代替を破棄します。

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

let transactionMatcher = Regex {
    Capture { /CREDIT|DEBIT/ }
    fieldSeparator          // 匹配全部空白,后续失败也不回溯

    TryCapture(field) { timestamp ~= $0 ? $0 : nil }
    fieldSeparator

    TryCapture(field) { details ~= $0 ? $0 : nil }
    // ...
}

キーポイント:

  • Local { ... }原子非捕捉基に相当
  • フィールド区切り文字など、正確に指定されたトークンに適しています
  • 検索スペースを制限し、マッチング効率を向上させます

詳細

TryCapture: マッチングでカスタム ロジックを実行する

18:55TryCaptureクロージャを使用して照合プロセスに参加し、実行時検証を実装できるようにします。

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

    TryCapture(field) { timestamp ~= $0 ? $0 : nil }  // 运行时验证 timestamp 正则
    fieldSeparator

    TryCapture(field) { details ~= $0 ? $0 : nil }    // 运行时验证 details 正则
    fieldSeparator

    // ...
}

キーポイント:

  • TryCaptureクロージャが戻りますnil一致が失敗したことを示します
  • クロージャはマッチングに積極的に参加し、マッチングプロセス中にあらゆる検証を実行できます。
  • 既存のランタイム ルールまたはカスタム解析ロジックの統合に適しています

再利用可能な正規表現コンポーネント

(27:05) 一般的なパターンをカプセル化します。RegexComponent、さまざまな場所で再利用されます。

struct MailmapLine: RegexComponent {
    @RegexComponentBuilder
    var regex: Regex<(Substring, Substring?, Substring)> {
        ZeroOrMore(.horizontalWhitespace)

        Optionally {
            Capture(OneOrMore(.noneOf("<#")))
        }
        .repetitionBehavior(.reluctant)

        ZeroOrMore(.horizontalWhitespace)

        "<"
        Capture(OneOrMore(.noneOf(">#")))
        ">"

        ZeroOrMore(.horizontalWhitespace)
        /#|\Z/
    }
}

// 使用
let match = line.prefixMatch(of: MailmapLine())

キーポイント:

  • RegexComponentプロトコルにより、カスタム タイプを通常のコンポーネントとして使用できるようになります
  • @RegexComponentBuilderプロパティ ラッパーによりビルダー構文が有効になります
  • コンポーネント内でリテラルとビルダーを混在させることができます

Embed Foundation パーサー

struct DatedMailmapLine: RegexComponent {
    @RegexComponentBuilder
    var regex: Regex<(Substring, Substring?, Substring, Date)> {
        // ... 前面的邮件映射模式 ...

        Capture(.iso8601.year().month().day())  // 直接产出 Date

        ZeroOrMore(.horizontalWhitespace)
        /#|\Z/
    }
}

キーポイント:

  • .iso8601.year().month().day()財団の日付解析戦略ですか
  • キャプチャ タイプは直接です。Date、その後手動で変換する必要はありません。
  • 解析戦略に準拠する任意の型を Regex Builder に埋め込むことができます

重要ポイント

1.すべての文字列インデックス操作を Regex に置き換えます

使用アイテムをすべてチェックするString.indexSubstring範囲操作、.components(separatedBy:)文字列が解析される場所。特に、ログの解析、CSV/TSV 処理、設定ファイルの読み取り、ユーザー入力の検証です。 Swift Regex の強力な型キャプチャにより、解析結果を直接利用できるようになり、中間変換が削減されます。

入り口:split(separator:)prefixMatch(of:)firstMatch(of:)replacing(_:with:)

2. NS RegularExpression の代わりに Regex Builder を使用してください

今でもプロジェクトで使用されていますNSRegularExpression代わりに、Regex Builder に移行します。宣言構文がより読みやすくなり、強力な型のキャプチャが回避されます。NSTextCheckingResultインデックス作成操作は、デフォルトで Unicode によって正しく処理されます。シンプル モードから始めて、徐々に移行できます。

入り口:import RegexBuilderRegex { ... }

3.カプセル化ビジネス固有の RegexComponent

プロジェクトで再利用される通常のパターンをカプセル化します。RegexComponent。たとえば、電子メールの検証、携帯電話番号の形式、注文番号のルール、ログのタイムスタンプなどです。コンポーネントを再利用すると一貫性が確保され、変更する場合は 1 か所だけ変更するだけで済みます。

入り口:struct EmailPattern: RegexComponent { ... }

4. Local を使用してパフォーマンス重視の正規表現を制御します

大量のテキストが処理されるシナリオ (ログ分析、データ クリーニング、リアルタイム フィルタリング) の場合は、正規表現に不必要なグローバル バックトラッキングがないかどうかを確認してください。フィールド区切り文字、固定形式トークン、その他の正確なパターンが使用されますLocal { ... }パッケージでは、検索スペースが制限されます。

入り口:Local { /pattern/ }

関連セッション

コメント

GitHub Issues · utterances