正規表現は、そのために設計されたドメイン固有言語(DSL)を使って文字列を_パターンマッチ_する、非常に汎用性の高い方法です。
正規表現の機能の多くはBase Rに組み込まれているので、古いコードでその例を見かけることもあるでしょう。
新しいコードでは、stringrライブラリを使うことを_強く_おすすめします。
文字列の概念で触れたように、stringrには優れたドキュメントがあり、たいていはパッケージの作者によって書かれています。
このRのシラバスでは、すでに基本的な正規表現の構文に親しんでいることを前提としています。ここでは、R固有の機能だけに焦点を当てます。
正規表現の知識を復習するためのリソースを以下に挙げます。
Stringsの概念ですでに説明した関数の多くは、標準で正規表現検索に対応するように設計されています。
文字列の概念は、文字列リテラルにのみマッチさせることで比較的シンプルに保っていましたが、代わりに任意の正規表現パターンを使うこともできます。
形式的には、Rで正規表現はregex()を呼び出すことで作成されます。利便性のために、stringrの関数は任意のパターン文字列を正規表現として扱います。以下の例では、最初のstr_replace_all()は2番目のバージョンの糖衣構文です。
input <- "Some input string"
# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"
input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"
チートシートは役に立つガイドです。pattern引数を持つすべての関数(赤色で印刷されています)は正規表現を受け付け、2ページ目には正規表現の構文がまとめられています。
大文字と小文字の区別やロケールといったデフォルトを上書きする必要がある場合には、regex()関数がいまだに重要になることに注意してください。
# default is case sensitive
"A Mixed Assortment" |>
str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"
多くの正規表現コードには、上で使った\sのようなバックスラッシュが含まれます。通常のR文字列では、\は2つ重ねてエスケープする必要があるため、"\\s"となります。
バックスラッシュは増えすぎて混乱しがちなので、複雑な場合には生の文字列を使うとよいでしょう。
デフォルトでは、生の文字列を定義する構文はr"( )"ですが、他の区切り文字も使えます。丸括弧( )と角括弧[ ]はどちらも正規表現の構文(キャプチャグループやオプショングループ)でよく使われるため、波括弧{ }がよい選択かもしれません。
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
ここではいくつかの問いと、その答えを出すのに役立つ関数を紹介します。
いくつかの例では、練習用にstringrに含まれている80個のフルーツ名のベクトル、fruitを使います。
文字列にマッチが含まれているか?
^や$のアンカーを使わずに済ませるには、str_detect()、あるいはstr_starts()やstr_ends()を使います。
# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
マッチを含む文字列だけを返すには?
文字列を取得するにはstr_subset()を、インデックスを取得するにはstr_which()を使います。
fruit |> str_subset("a...e")
[1] "apple" "blackberry" "mandarine" "nectarine" "pineapple" "pomegranate" "raspberry" "salal berry"
> fruit |> str_which("a...e")
[1] 1 7 48 51 62 64 70 73
各文字列にマッチはいくつあるか?
str_count()
# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3
マッチはどこにあるか?
対話的に使うならstr_view()、より一般的にはstr_locate()やstr_locate_all()を使います。
# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
[1] │ <apple>
[7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry
# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
start end
[1,] 1 5
[2,] NA NA
[3,] NA NA
[4,] NA NA
[5,] NA NA
[6,] NA NA
[7,] 3 7
[8,] NA NA
[9,] NA NA
[10,] NA NA
多くの場合、_何が_マッチしたのかを知りたいはずです。これは、正規表現の中に括弧(())でキャプチャグループを含めることで実現できます。
同じ正規表現内で\1、\2などを使ってキャプチャを後方参照することができます。たとえば、繰り返される文字のペアを見つけるには次のようにします。
fruit |> str_subset("(..)\\1")
[1] "banana" "coconut" "cucumber" "jujube" "papaya" "salal berry"
同様の構文で、キャプチャを置換文字列の中でも使えます。
# double to triple
fruit |>
str_subset("(..)\\1") |>
str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana" "cococonut" "cucucumber" "jujujube" "papapaya" "salalal berry"
後の処理のためにマッチを集めるには、最初のマッチだけならstr_match()関数を、すべてのマッチならstr_match_all()を使います。
キャプチャグループは、どの言語でも複雑な出力を生みがちです。Rのアプローチでは(さらにベクトル化もされていて、追加の難しさになっています)、str_match()から行列を生成します。入力文字列ごとに1行、1列目にはマッチした文字列全体、残りの列にはそれぞれのキャプチャグループが入ります。
名前付きグループを使うと、結果の行列の列名になるので便利です。
recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
[,1] [,2] [,3]
[1,] "25g sugar in 200ml" "25g" "200ml"
# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
wt vol
[1,] "25g sugar in 200ml" "25g" "200ml"
このシラバスでは行列についてあまり触れていませんが、参照できる概念のドキュメントがあります。行列は、長い間Base Rの一部でありながら、Tidyverseのエコシステムとはあまり相性がよくありません。
手短にまとめると、行列は2つのインデックスを持つベクトルのように扱い、順序は[rows, cols]です。どちらかのインデックスを空にすると、行または列全体を取得できます。
m[r,]で単一の行rを、あるいはm[, c]で単一の列cを取得すると、デフォルトでは返される値がベクトルに平坦化されます。
> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
wt vol
"25g sugar in 200ml" "25g" "200ml"
# get a vector element
amt1["wt"] |> unname()
[1] "25g"
必然的に、str_match_all()はさらに複雑な構造の出力を生成します。
[[1]]インデックスで出力をunlistします。match()と同じです。mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"
# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"
もちろん、マッチは失敗することもあります。その場合、結果は特別な値NAになります。何もないことの概念で説明されているように、これに備えてテストできるようにしておきましょう。
# failed match
recipe |> str_match("(not here)")
[,1] [,2]
[1,] NA NA
R for Data Scienceは次のように述べています:
str_match()は行列を返すので、特に扱いやすいわけではありません。
脚注付きで:
主な理由は、この本では行列をまったく扱わないからです!
この本の著者の1人はstringrライブラリも書いており、自分の作品を暗に批判しているのです。
この関数の実装が今後のリリースでどのように進化していくのか、興味深いところです。