学习路径
/
R
R
/
教学大纲
/
正则表达式
正则

正则表达式 属于 R

1 个练习

关于 正则表达式

正则表达式是一种非常灵活的字符串处理方式,可以_模式匹配_字符串,它使用专门为此设计的领域特定语言(DSL)。

Advanced

和许多其他编程语言一样,R 并不打算自己实现正则表达式库。 它只是包装了 ICU 正则表达式引擎。 ICU 与大多数较新语言所使用的流行库 PCRE2 类似,不过某些边界情况的处理方式有所不同。

许多正则表达式功能都内置在 Base R 中,你在较旧的代码里可能会看到相关示例。

对于新代码,_强烈_建议使用stringr库。

正如字符串概念中所提到的,stringr有完善的文档,通常由包的作者撰写。

Note

本 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  
Note

R for Data science 中提到:

str_match() 返回的是一个矩阵,所以用起来并不是特别方便。

并加了一条脚注:

主要是因为我们在本书中从不讨论矩阵!

这本书的作者之一同时也是 stringr库的作者,这实际上是在批评自己的作品。

这个函数在未来的版本中会如何演变,值得关注。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 正则表达式