← レッスン一覧に戻る レッスン 42 / 48
中級 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サポートと組み合わせれば、国際的なテキスト処理がそのまま動作します。