Rust に入門したばかりで,公式ガイド「プログラミング言語Rust」(和訳)もまだ一部しか読んでない素人だけど,文字列の検索・置換はどうやるのか調べたのを記事にします。
(動作確認は Rust 1.12.0;改訂時は Rust 1.20.0 + regex 0.2.2 を使用)
[2017-09-02 追記]regex のバージョンが 0.2 系に上がって一部互換性がなくなった。0.2 で大きな問題がなければ 1.0 をリリースするとのことなので,今回のような大きな変更はしばらく無いのかもしれない。
変更の詳細については公式の CHANGELOG を参照のこと。
0.2 系で違いがあるところに加筆していきます。
予備知識
文字列について
文字列には &str と String の2種類があるらしい。
公式ガイドの「文字列」が分かりやすい。
(2016-10-07 追記)「Rustの文字列操作 - Qiita 」も役に立つ。
&str は「文字列のスライス」と呼ばれ,サイズ固定で変更不可。文字列リテラルはこれになる。
String は伸張可能。&str から to_string() で作れる。String に & を付ければ &str になる。
いずれも,文字コードは UTF-8 固定で,エンコードが正しいことが保証されているそう。
文字列リテラルにバックスラッシュとかが含まれるとき,
let s = "The sign \\ is called backslash.";
とエスケープして書くのは面倒だけど,
let s = r"The sign \ is called backslash.";
という書き方もできる。
正規表現について
正規表現は言語には組み込まれておらず,regex というクレートを使う。
まず Cargo.toml に
[dependencies]
regex = "0.1"
とかって書いておいて,
extern crate regex;
use regex::Regex;
としておく。
すると,以下のように書ける。
let re = Regex::new(r"\d+").unwrap();
言語組込みではないので正規表現リテラルは無いのだろう。
正規表現が正しくない場合,unwrap() でパニクる。
参考:regex::Regex 構造体(英文)
検索
検索してみる。
検索して位置情報を得るには find や find_iter を使う(前者は最初の一つだけ,後者は連続して)。
[2017-09-02 加筆]
find の返り値も regex 0.2 系で Option<Match> に変更になった。Match については「名前付きキャプチャー」の加筆箇所を参照。
[加筆おわり]
検索で見出された部分文字列を得るには,captures や captures_iter を使う。
まず captures から。
extern crate regex;
use regex::Regex;
fn main() {
let str = "MZ-80K2E";
// 最初にマッチした箇所全体を取り出す例
let re = Regex::new(r"\d+").unwrap();
let caps = re.captures(str).unwrap();
println!("{}", caps.at(0).unwrap()); // => "80" が表示される
// ( ) であらわにキャプチャーを書いて,それぞれを取り出す例
let re = Regex::new(r"([A-Z])(\d)").unwrap();
let caps = re.captures(str).unwrap();
println!("{} before {}", caps.at(1).unwrap(), caps.at(2).unwrap());
// => "K before 2" が表示される。
}
このように,at(0) だとマッチした部分全体が,at(1) だと 1 番目の ( ) でキャプチャーされた部分文字列が得られる。
ええと,caps.at(n).unwrap() の代わりに &caps[n] でもいいようだ。
[2017-09-02 加筆]
regex 0.2 系では at が無くなった。
0.1 系で
caps.at(0).unwrap()
と書いていたのは
caps.get(0).unwrap().as_str()
と書くことになった。
&caps[n] の形式は 0.2 系でも有効。
[加筆おわり]
次に,captures_iter を。これは結果を for で回せるということらしい。
extern crate regex;
use regex::Regex;
fn main() {
let str = "MZ-80K2E";
let re = Regex::new(r"\d").unwrap();
for caps in re.captures_iter(str) {
println!("{}", &caps[0]);
}
// => "8", "0", "2" が順に表示される
}
置換
置換には replace や replace_all を使う。前者は最初にマッチした箇所だけ,後者は検索して見出された箇所すべてを置換。
置換文字列を指定して置換
extern crate regex;
use regex::Regex;
fn main() {
let str = "MZ-80K2E";
let re = Regex::new(r"\d").unwrap();
// 数字をアスタリスクに
let result = re.replace_all(str, "*");
println!("{}", result); // => "MZ-**K*E" が表示される
// 数字を [ ] で囲む
let result = re.replace_all(str, "[$0]");
println!("{}", result); // => "MZ-[8][0]K[2]E" が表示される
}
置換文字列の指定では,マッチ部分が $0 で参照できる。(この「参照」は Rust 用語の参照ではなく正規表現用語の参照)
( ) でキャプチャーしたときは,例えば 2 番目のキャプチャー文字列が $2 で参照できる。
置換文字列をクロージャーで構成
複雑な置換では,置換文字列を構成するのにロジックが必要になる。
たとえば,文字列中の数字列部分を整数とみて,ゼロ埋め4桁にしたい,つまり,「MZ-80K2E」を「MZ-0080K0002E」にしたい,といった場合。
replace や replace_all の第 2 引数にクロージャーを与えればよいようだ。以下のように書ける。
extern crate regex;
use regex::{Regex, Captures};
fn main() {
let str = "MZ-80K2E";
let re = Regex::new(r"\d+").unwrap();
let result = re.replace_all(str,
|caps: &Captures| {
let num: u32 = (&caps[0]).parse().unwrap();
format!("{:04}", num)
}
);
println!("{}", result);
}
クロージャーの引数の型を ®ex::Captures と書くのが面倒なので,regex::Captures を use しておいた。
regex クレートの表現力
regex クレートはどの程度の表現が可能なのだろうか。
公式ドキュメントの「Syntax」によれば,RE2 という正規表現エンジンに概ね沿っているようだ。
後方参照・先読み・戻り読みはダメ
RE2 も Rust の regex も,入力サイズに対して線型時間で実行できるようにつくられており,それを実現するため後方参照は使えないとのこと。先読み・戻り読みも無いようだ。
文字プロパティー
Unicode の文字のプロパティーなんかは使えて,たとえば漢字を検索するのに
let re = Regex::new(r"\p{Han}").unwrap();
なんて書ける。
名前付きキャプチャー
名前付きキャプチャーは (?P<name>exp) でいける。
名前付きキャプチャーを使った場合,caps.name("name") みたいな感じで取り出せる。
[2017-09-02 加筆]
regex 0.1 系では
caps.name("name").unwrap()
で名前付きキャプチャーの文字列が取り出せたが,0.2 系の場合,これは regex::Match を返す。
regex::Match にはキャプチャー文字列の位置情報も含まれる。
ここから as_str() で文字列を取り出すこともできるし,単に文字列が欲しいだけなら
&caps["name"]
でよいようだ。
[加筆おわり]
オプション
大文字・小文字の同一視はどうか。
Ruby だと,正規表現リテラルで /foo/i みたいな i オプションで書けるし,Regexp.new("foo", Regexp::IGNORECASE) といった書き方もできる。
Rust では正規表現リテラルは無いようだし,Regex::new に与える引数は一つだけ。
どうするかというと,正規表現中にオプションを書くことができる。(あまり知られてないけど,実は Ruby の正規表現エンジン,鬼雲なんかでもこういう書き方ができる)
let re = Regex::new(r"(?i)foo").unwrap();
このように「(?i)」よりも右側で大文字・小文字が同一視される。「(?-i)」を置くとそこから右側では区別をする。
また,
let re = Regex::new(r"(?i:foo)").unwrap();
のような書き方もできて,(?i: ) で囲った箇所だけ大文字・小文字が同一視される。
この手のオプションには他に,m,s,U,u,x がある。いずれも - を頭に付ければ OFF になる。
u はデフォルトで ON になっているが,「Unicode サポート」というやつ。例えば \d が ASCII の数字だけでなく全角数字なんかにもマッチする。
x は鬼車の x オプションと同じで,空白文字が無視され,# でコメントが書けるようになる。つまり,次のような書き方ができるのだ。
let re = Regex::new(r"(?x)
\d{3} # 郵便番号前半
- # ハイフン
\d{4} # 郵便番号後半
").unwrap();
このオプションについては拙文「Ruby の正規表現を複数行で書く」を参考にしていただけると嬉しい。
m オプションについては次節で述べる。
マルチラインモード(2016-11-09 追記)
m オプションは「マルチラインモード」にするもので,デフォルトは OFF になっている。
これはアンカー ^ および $ の意味を変更するものだ。Ruby の正規表現の m オプションとはまるで違う1ので,Rubyist さんは要注意。
デフォルトでは,^,$ はそれぞれ文字列の先頭・末尾にマッチする。
マルチラインモードでは行頭・行末にマッチするようになる。
ところで,何をもって行頭・行末とするのかは実は正規表現エンジンによってバラツキがあるので要注意だ。文字列が改行で終わっているとき,^ や $ が文字列末尾にマッチするかどうかは,エンジンに依るのだ。
regex クレートのドキュメントには明記されていないので,実験してみよう。(参考としてシングルラインモードも併記)
extern crate regex;
use regex::Regex;
// Regex のインスタンスを簡潔に得る
fn regex(re_str: &str) -> Regex {
Regex::new(re_str).unwrap()
}
fn main() {
let s = "foo\nbar\n";
// シングルラインモード
let re_s = regex("^");
let re_e = regex("$");
println!("{:?}", re_s.replace_all(s, "^")); // "^foo\nbar\n"
println!("{:?}", re_e.replace_all(s, "$")); // "foo\nbar\n$"
// マルチラインモード
let re_s = regex("(?m)^");
let re_e = regex("(?m)$");
println!("{:?}", re_s.replace_all(s, "^")); // "^foo\n^bar\n^"
println!("{:?}", re_e.replace_all(s, "$")); // "foo$\nbar$\n$"
}
このように,$ だけでなく ^ も文字列末尾にマッチするが,これは Perl や Ruby(鬼雲)の動作とは異なる。
もう一つ注意したいことがある。改行コードが CR+LF,つまり "\r\n" だったとき,"\r" の直前ではなく "\n" の直前が行末になる。
改行コードの種類に頓着せず行末のアンカーを使うとエラい目に遭うだろう。
-
Ruby の場合は
.の意味を変える(mを付けると改行にもマッチするようになる)。 ↩