正規表達式是一種用途極為廣泛的方式,能對字串進行_模式比對_,它使用的是一套專為此目的設計的領域特定語言(DSL)。
許多正規表達式功能已內建於 Base R,你可能會在較舊的程式碼中看到相關範例。
若是新程式碼,_強烈_建議使用 stringr 函式庫。
如同字串概念所述,stringr 有完善的文件,通常由套件作者親自撰寫。
這份 R 課程大綱假設你已經熟悉基本的正規表達式語法。 我們只會專注在 R 特有的功能上。
以下列出一些能幫你複習正規表達式知識的資源。
在 Strings 概念中已經討論過的許多函式,本來就是為了正規表達式搜尋而設計的。
我們在字串概念中刻意保持簡單,只比對字串常值,但其實任何正規表達式模式都可以改用。
嚴格來說,在 R 中建立正規表達式的方式是呼叫 regex()。
為了方便起見,stringr 的函式會把任何模式字串都當成正規表達式處理。
在下面的範例中,第一個 str_replace_all() 是第二種寫法的語法糖:
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 字串中,\ 必須用兩個反斜線來跳脫,因此寫成 "\\s"。
反斜線很容易愈用愈多、變得混亂,所以在複雜的情況下改用原始字串可能會很有幫助。
預設用來定義原始字串的語法是 r"( )",但也允許使用其他分隔符號。
由於小括號 ( ) 和方括號 [ ] 在正規表達式語法中都很常見(用於擷取群組和選項群組),大括號 { } 可能是個不錯的選擇。
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
以下列出幾個問題,以及能幫助你回答它們的函式。
有些範例使用 fruit,這是 stringr 內含、供練習用的向量,包含 80 個水果名稱。
字串中是否含有相符的內容?
用 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 欄包含完整的相符字串,其餘各欄則包含各個擷取群組。
使用具名群組有好處,因為它們會成為結果矩陣的欄名。
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]] 索引把輸出從清單中取出,做法如清單概念所述。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
str_match()回傳的是矩陣,所以用起來不是特別方便。
並附上註腳:
主要是因為我們在這本書裡從來沒討論過矩陣!
這本書的作者之一也寫了 stringr 函式庫,等於是在含蓄地批評自己的作品。
未來版本中這個函式的實作會如何演變,值得關注。