Rust で正規表現を扱う (regex)

Rust の regex クレート を使うと、正規表現を使った様々な文字列処理を行うことができます。 Rust (Cargo) プロジェクト内で以下のように実行して Cargo.toml に依存関係を追加すれば regex の使用準備は完了です。

$ cargo add regex
Cargo.toml
[dependencies]
regex = "1.7.0"

パターンに一致するか調べる (is_match)

Regex#is_match メソッドを使うと、引数で渡した文字列に、Regex のパターンに一致する部分文字列が含まれているかを調べることができます。

use regex::Regex;

let re = Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();
let input = "Today's date is 2023-01-07.";
if re.is_match(input) {
    println!("日付らしき文字列が見つかりました");
}

Regex#is_match メソッドは、パターンに一致する文字列が部分的にでも見つかれば true を返します。 文字列全体がパターンに一致するかどうかを調べたい場合は、パターンに ^(行頭)と $(行末)を含めて、r"^\d{4}-\d{2}-\d{2}$" のようにします。

パターンに一致した位置を調べる (find, find_iter)

Regex#find メソッドは、パターンに一致する部分文字列が見つかったときに regex::Match オブジェクトを返します。 見つからない場合は Option::None を返します。 Match のメソッドを使って、実際に一致した部分文字列や、その位置を取得できます。

連続する数値を探す
let re = Regex::new(r"\d+").unwrap();
let input = "server: ok=100 changed=50 unreachable=0 failed=3";
match re.find(input) {
    Some(m) => println!("Found `{}` at {}-{}", m.as_str(), m.start(), m.end()),
    None => println!("Not found"),
}
実行結果
Found `100` at 11-14

Regex#find は最初に見つかった部分文字列だけを返しますが、複数回マッチさせたい場合は、代わりに Regex#find_iter メソッドで regex::Matches オブジェクトを取得します。 MatchesIterator を実装しているので、ループ処理が可能です。

let re = Regex::new(r"\d+").unwrap();
let input = "server: ok=100 changed=50 unreachable=0 failed=3";
for m in re.find_iter(input) {
    println!("Found `{}` at {}-{}", m.as_str(), m.start(), m.end());
}
実行結果
Found `100` at 11-14
Found `50` at 23-25
Found `0` at 38-39
Found `3` at 47-48

グルーピングして部分文字列を抽出する (captures, captures_iter)

Regex#captures メソッドは、パターン中の括弧 (()) でグルーピングされた部分を一度に抽出して、regex::Captures オブジェクトを返します。 CapturesVec と同様にアクセス([]get)することができ、インデックス 0 には、パターンに一致した文字列全体が格納されています。

let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let input = "Eiichi Shibusawa was born on 1840-02-13 and died on 1931-11-11.";
match re.captures(input) {
    Some(caps) => {
        println!("年月日: {}", &caps[0]);
        println!("年: {}", &caps[1]);
        println!("月: {}", &caps[2]);
        println!("日: {}", &caps[3]);
    }
    None => println!("Not found"),
}
実行結果
年月日: 1840-02-13
年: 1840
月: 02
日: 13

複数回マッチさせたいときは、captures の代わりに captures_iter を使用します。

for caps in re.captures_iter(input) {
    println!("年: {}, 月: {}, 日: {}", &caps[1], &caps[2], &caps[3]);
}
実行結果
年: 1840, 月: 02, 日: 13
年: 1931, 月: 11, 日: 11

正規表現パターンの中で、キャプチャグループに名前を付けるには (?P<name>exp) というシンタックスを使用します。

let re = Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap();
let input = "Eiichi Shibusawa was born on 1840-02-13 and died on 1931-11-11.";
for caps in re.captures_iter(input) {
    println!(
        "年: {}, 月: {}, 日: {}",
        &caps["year"], &caps["month"], &caps["day"]
    );
}

パターンに一致する部分を置換する (replace, replace_all)

Regex#replace メソッドを使用すると、正規表現パターンに一致した部分を任意の文字列に置換できます。 最初に一致した部分だけでなく、一致した部分をすべて置換したいときは、Regex#replace_all メソッドを使用します。

let re = Regex::new(r"[m|M]aku").unwrap();
let input = "I am maku. You are not Maku.";
let output = re.replace_all(input, "####");
println!("{}", output); //=> "I am ####. Your are not ####."

パターンの中で括弧 (()) を使ってキャプチャグループを構成しておけば、実際に一致した部分文字列を、置換文字列の中で $1$2 で参照できます。

ハイフン (-) で繋がった単語の前後を入れ替える
let re = Regex::new(r"\b(\w+)-(\w+)\b").unwrap();
let input = "AAA BBB-CCC DDD EEE-FFF GGG";
let output = re.replace_all(input, "$2-$1");
println!("{}", output); //=> "AAA CCC-BBB DDD FFF-EEE GGG"