中級 data-structures

Wasmでの正規表現とテキスト処理

なぜWasmでRustの正規表現を使うのか?

JavaScriptには組み込みの正規表現がありますが、なぜRustのregexクレートをWasmにコンパイルするのでしょうか?パフォーマンス、正確性、そして機能面で優れているからです:

  ベンチマーク: 10,000件のメールアドレスをマッチ

  JS RegExp          ████████████████████████  24ms
  Rust regex (Wasm)  ████████  8ms
  Rust regex (native)████  4ms

  ← 高速                         低速 →
機能 JavaScript RegExp Rustのregexクレート
エンジン種類 バックトラッキング(NFA/DFA) 保証されたDFA/NFAハイブリッド
壊滅的バックトラッキング 発生しうる(ReDoS) 不可能(線形時間)
Unicodeサポート 部分的(ES2018以降) 完全(Unicodeカテゴリ)
名前付きキャプチャ あり(ES2018) あり
先読み/後読み あり なし(O(n)を保証)
コンパイル速度 高速 やや遅い(キャッシュ可能)
マッチ速度 良好 優秀(2-5倍高速)

ReDoS防護

Rustのregexクレートはパターンに関係なく**O(n)**のマッチング時間を保証します。これにより正規表現によるサービス拒否攻撃(ReDoS)に対して安全です:

  パターン: (a+)+$     入力: "aaaaaaaaaaaaaaaaX"

  JavaScript RegExp:
    2^n通りの組み合わせを試行 → 数秒間ハング
    これはReDoS脆弱性です!

  Rustのregexクレート:
    線形スキャン → 即座に「マッチなし」を返す
    設計上安全(バックトラッキングなし)

Wasmでのregexクレートの使用

use regex::Regex;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct TextProcessor {
    email_re: Regex,
    url_re: Regex,
    phone_re: Regex,
}

#[wasm_bindgen]
impl TextProcessor {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        TextProcessor {
            email_re: Regex::new(r"[\w.+-]+@[\w-]+\.[\w.]+").unwrap(),
            url_re: Regex::new(r"https?://[\w\-._~:/?#\[\]@!$&'()*+,;=%]+").unwrap(),
            phone_re: Regex::new(r"\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}").unwrap(),
        }
    }

    pub fn find_emails(&self, text: &str) -> Vec<String> {
        self.email_re.find_iter(text)
            .map(|m| m.as_str().to_string())
            .collect()
    }

    pub fn validate_email(&self, email: &str) -> bool {
        self.email_re.is_match(email)
    }

    pub fn redact_phones(&self, text: &str) -> String {
        self.phone_re.replace_all(text, "[秘匿]").to_string()
    }
}

regexをWasmにコンパイル — サイズの考慮事項

regexクレートはWasmバイナリに約200-400 KBを追加します(Unicodeテーブルの包含状況による)。サイズを削減するには:

# Cargo.toml
[dependencies]
regex = { version = "1", default-features = false, features = ["std"] }
# Unicodeテーブルを削除: 約100 KB節約だが\p{...}パターンが使えなくなる

WasmアプリでよくあるRegexパターン

  パターン                      マッチ対象                  ユースケース
  ─────────────────────────── ───────────────────────── ──────────────────
  [\w.+-]+@[\w-]+\.[\w.]+     user@example.com          メール検証
  https?://[^\s]+             http://example.com/path   URL抽出
  \d{4}-\d{2}-\d{2}          2024-01-15                ISO日付
  \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}  192.168.1.1     IPアドレス
  #[0-9a-fA-F]{6}            #ff00aa                   16進カラー
  \b[A-Z]{2,}\b              NASA, FBI                 頭字語
  ^\s*$                       (空行/空白行)              空白行
  <[^>]+>                     <div class="x">           HTMLタグ

テキスト検索と置換

コンパイル済みregexによる効率的な検索

use regex::Regex;

// 一度コンパイルして何度も使用
lazy_static! {
    static ref LOG_PATTERN: Regex = Regex::new(
        r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (ERROR|WARN|INFO|DEBUG): (.+)"
    ).unwrap();
}

fn parse_log(line: &str) -> Option<(String, String, String)> {
    LOG_PATTERN.captures(line).map(|caps| (
        caps[1].to_string(),  // タイムスタンプ
        caps[2].to_string(),  // レベル
        caps[3].to_string(),  // メッセージ
    ))
}

キャプチャを使った検索と置換

use regex::Regex;

fn anonymize_data(text: &str) -> String {
    let email_re = Regex::new(r"([\w.+-]+)@([\w-]+\.[\w.]+)").unwrap();
    let phone_re = Regex::new(r"\d{3}[-.]?\d{3}[-.]?\d{4}").unwrap();

    let result = email_re.replace_all(text, "***@$2");  // ドメインは保持
    let result = phone_re.replace_all(&result, "***-***-****");
    result.to_string()
}

WasmでのCSV処理

CSV解析はWasmがJavaScriptを大幅に上回る一般的なユースケースです:

  100,000行のCSVを処理

  Papa Parse (JS)     ████████████████████████████████  320ms
  Rust csvクレート     ████████████  120ms
  (Wasm)

  ← 高速                                    低速 →
use csv::ReaderBuilder;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn parse_csv(data: &str) -> JsValue {
    let mut reader = ReaderBuilder::new()
        .has_headers(true)
        .from_reader(data.as_bytes());

    let mut rows: Vec<Vec<String>> = Vec::new();
    for result in reader.records() {
        if let Ok(record) = result {
            rows.push(record.iter().map(|s| s.to_string()).collect());
        }
    }

    serde_wasm_bindgen::to_value(&rows).unwrap()
}

Unicode処理

Rustのregexクレートは優れたUnicodeサポートを備えており、国際的なテキスト処理において重要です:

  文字               UTF-8バイト数  Rust char    JS char
  ────────────────── ────────────── ──────────── ─────────
  'A'                1バイト        1 char       1コード単位
  アクセント付き'e'    2バイト        1 char       1コード単位
  漢字               3バイト        1 char       1コード単位
  絵文字             4バイト        1 char       2コード単位!
use regex::Regex;

// Unicode対応の単語境界
let re = Regex::new(r"\b\w+\b").unwrap();  // Unicodeで動作!

// Unicodeカテゴリマッチング
let re = Regex::new(r"\p{Han}+").unwrap();       // 漢字
let re = Regex::new(r"\p{Cyrillic}+").unwrap();  // ロシア語/キリル文字
let re = Regex::new(r"\p{Emoji}+").unwrap();     // 絵文字

// Unicode対応の大文字小文字無視
let re = Regex::new(r"(?i)strasse|straße").unwrap();  // ドイツ語にマッチ

パフォーマンス最適化のヒント

  最適化                              効果
  ──────────────────────────────────── ──────────────────────────
  regexを一度コンパイルして再利用       10-100倍高速化
  find()の代わりにis_match()を使用     より高速(キャプチャ割当てなし)
  パターンにアンカーを付ける(^...$)   テキスト全体のスキャンを回避
  複数パターンにはRegexSetを使用       N回スキャンの代わりに1回
  ASCII専用ならUnicodeを無効化          バイナリ縮小、高速化
  生バイトにはbytes::Regexを使用       UTF-8検証を回避

RegexSet: 1回のパスで複数パターンをマッチ

use regex::RegexSet;

let set = RegexSet::new(&[
    r"[\w.+-]+@[\w-]+\.[\w.]+",    // メール
    r"https?://[^\s]+",             // URL
    r"\d{3}[-.]?\d{3}[-.]?\d{4}",  // 電話番号
    r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",  // IP
]).unwrap();

let text = "Contact john@example.com or visit https://example.com";
let matches: Vec<usize> = set.matches(text).into_iter().collect();
// matches = [0, 1]  → メールとURLパターンを検出

まとめ

WasmにコンパイルしたRustのregexクレートは、保証された線形時間マッチング(ReDoSなし)でJavaScriptの2-5倍高速なテキスト処理を提供します。メール検証、ログ解析、CSV処理、検索と置換、その他テキスト集約型のワークロードに活用しましょう。Wasmバイナリには約200-400 KBが追加されますが、パフォーマンスと安全性でその価値を十分に発揮します。完全なUnicodeサポートと組み合わせれば、国際的なテキスト処理がそのまま動作します。

試してみる