メモ:tidyr::extract()の正規表現でマッチしたりしなかったりするグループがあるときは、文字列全体にマッチさせつつnon-greedyに

タイトルは何言ってるかわかりませんが…

こういうデータがあるときに、

name_with_note
name1
name2(note1)
name2(note2)

(...)の部分とそれ以外で分けて、こういう結果がほしい、というときの話。

name note
name1 NA
name2 (note1)
name2 (note2)

簡単に見えてちょっとてこずったのでメモ。1つ目の行には(...)の部分がないのが問題です。

データはこんな感じ。

d <- data.frame(
  name_with_note = c(
    "name1",
    "name2(note1)",
    "name2(note2)"
  ),
  stringsAsFactors = FALSE
)

いろいろ正規表現を変えて試すので、そのための関数を定義しておきます。

library(magrittr)

extract_by_regex <- function(regex) {
  tidyr::extract(d,
                 col = name_with_note,
                 into = c("name", "note"),
                 regex = regex) %>%
  knitr::kable()
}

まずは単純に、(...)で囲まれている部分とそれ以外の部分を別々の正規表現にするとこんな感じでしょう。

extract_by_regex("(.*)(\\(.*\\))")
name note
NA NA
name2 (note1)
name2 (note2)

これだと、1行目が引っかかりません。(...)の部分がないのでまあ当然ですよね。

では次、(...)の部分は存在したりしなかったりするので?を付けます。

extract_by_regex("(.*)(\\(.*\\))?")
name note
name1 NA
name2(note1) NA
name2(note2) NA

今度は、すべての行がヒットしましたが、noteNAになってしまいました。これは、.*の部分がgreedyにマッチしてしまっているからです。

では、non-greedyにするために.*?にしてみましょう。

extract_by_regex("(.*?)(\\(.*\\))?")
name note
NA
NA
NA

今度はnameが空になってしまいました。.*?の最小マッチは何にもマッチしないことなので、まあそうなりますよね(これが理解できずに30分ほど悩んだ…)。

正解は、^$を付けて文字列全体にマッチさせることです。

extract_by_regex("^(.*?)(\\(.*\\))?$")
name note
name1 NA
name2 (note1)
name2 (note2)

正規表現むずかしい。もっと簡単にできるよ、というのがあれば教えてください。