10  字符串和正则表达式

本章中,我们主要使用string包来实现字符串的高效处理。

10.1 创建字符串

10.1.1 转义字符

这里我们主要介绍转义字符.

有时在创建字符串时,我们需要输入引号,这时我们需要使用转义字符\来转义它,即告诉计算机这个是字符串中的引号,而不是程序中的引号.

值得注意的是,当字符串中需要输入反斜杠\时,同样需要转义符号\
library(tidyverse)
library(stringr)
library(babynames)
double_quote <- "\""
single_quote <- "'"
backslash <- "\\"

x <- c(double_quote, single_quote, backslash)
x # 输出的结果包含转义字符,
[1] "\"" "'"  "\\"
str_view(x) # 输出的结果为转义后的结果
[1] │ "
[2] │ '
[3] │ \

10.1.2 原始字符串-raw strings

创建一个包含多个引号或反斜杠的字符串会令人感到十分困惑.

library(stringr)
tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
[1] │ double_quote <- "\"" # or '"'
    │ single_quote <- '\'' # or "'"

如上代码中有非常多的反斜杠,要消除其中的过多的转义字符(主要是转义字符的转义字符),可以使用原始字符串.

原始字符串通常的形式为r"(strings)". 如果字符串中已经包含了)",则可以使用r[]r{},如果还不够,还可以插入任意数量的破折号,使开头和结尾的字符串具有唯一性.例如r"--()--"r"---()---.那么以上tricky字符串可以怎么改写呢?

原始字符串可以处理任何文本,同时精简过多的转义字符,使用非常灵活.
tricky_amendment <- r"(double_quote <- "\"" # or'"'
single_quote <- '\'' # or "'")"
str_view(tricky_amendment)
[1] │ double_quote <- "\"" # or'"'
    │ single_quote <- '\'' # or "'"

10.1.3 其他特殊字符

最常见的特殊字符有:

  • \n: 新行
  • \t: 制表符
  • \u& \U: Unicode转义字符的字符串(非英语字符)

更多的特殊字符可以通过?Quotes查看.

x <- c("one\ntow", "one\ttwo", "\u00b5", "\U0001f604")
str_view(x)
[1] │ one
    │ tow
[2] │ one{\t}two
[3] │ µ
[4] │ 😄

10.1.3.1 练习

创建包含以下值的字符串:

  • He said “That’s amazing!”
  • \a\b\c\d
  • \\\\\\
a <- "He said \"That's amazing!\"" # or a <- r"(He said "That's amazing!")"
b <- r"(\a\b\c\d)"
c <- r"(\\\\\\)"

str_view(c(a, b, c))
[1] │ He said "That's amazing!"
[2] │ \a\b\c\d
[3] │ \\\\\\

10.2 从数据创建多个字符串

本部分我们将讨论: - str_c(),str_glue(),str_flatten()三个函数的用法. - 以上函数与mutate(),summarise()等函数连用的方法.

手动创建字符串只是最基础的内容,更多时候我们希望将一些文本与已经保存在数据中的文本连接起来,例如,将”hello”与一个保存有姓名的数据连接起来,创建一些列问候语.

10.2.1 str_c()函数

10.2.1.1 简单的结合

names <- c("John", "Susan")
str_c("Hello ", names)
[1] "Hello John"  "Hello Susan"

10.2.1.2mutate()连用

df <- tibble(name = c("Flora", "David", "Terra", NA))
df  |> 
  mutate(greeting1 = str_c("Hi ", name, "!"))  |> 
  # coalesce()函数用于处理NA值,,用一个指定值代替缺失值,,有两种形式。
  mutate(greeting2 = str_c("Hi ", coalesce(name, "you"), "!"))  |> 
  mutate(greeting3 = coalesce(str_c("Hi ", name, "!"), "Hi!"))
# A tibble: 4 × 4
  name  greeting1 greeting2 greeting3
  <chr> <chr>     <chr>     <chr>    
1 Flora Hi Flora! Hi Flora! Hi Flora!
2 David Hi David! Hi David! Hi David!
3 Terra Hi Terra! Hi Terra! Hi Terra!
4 <NA>  <NA>      Hi you!   Hi!      

10.2.2 str_glue()函数

使用str_c()将字符串结合会存在一个问题,我们会输入大量的引号",在字符串较长时我们很难一眼看出代码的总体目标.str_glue()函数提供了另外一种结合的方式.

df  |> 
  mutate(greeting1 = str_glue("Hi {name}!"))  |> 
  # 在字符串中加入大括号等特殊字符,不是用\转义,而是加倍输入特殊字符
  mutate(greeting2 = str_glue("{{Hi {name}!}}"))
# A tibble: 4 × 3
  name  greeting1 greeting2  
  <chr> <glue>    <glue>     
1 Flora Hi Flora! {Hi Flora!}
2 David Hi David! {Hi David!}
3 Terra Hi Terra! {Hi Terra!}
4 <NA>  Hi NA!    {Hi NA!}   

10.2.3 str_flatten()函数

str_c()str_glue()可以很好地与mutate()配合使用,,因为它们的输出与输入长度相同。如果您想要一个能与summarize() 完美配合的函数,,即总是返回单个字符串的函数,,该怎么办呢?这就是str_flatten()的工作:它接收字符向量,并将向量中的每个元素合并为一个字符串:

str_flatten(c("x", "y", "z")) # 返回单个字符串
[1] "xyz"
# 与summarise()函数连用
df <- tribble(
  ~name,
  ~fruit,
  "Carmen",
  "banana",
  "Carmen",
  "apple",
  "Marvin",
  "nectarine",
  "Terence",
  "cantaloupe",
  "Terence",
  "papaya",
  "Terence",
  "mandarin"
)
df  |> 
  group_by(name)  |> 
  summarise(
    str_flatten(fruit, ", ")
  )
# A tibble: 3 × 2
  name    `str_flatten(fruit, ", ")`  
  <chr>   <chr>                       
1 Carmen  banana, apple               
2 Marvin  nectarine                   
3 Terence cantaloupe, papaya, mandarin

10.2.3.1 练习

将下列表达式从str_c()转换为str_glue(),反之亦然.

  1. str_c(“The price of”, food, ” is “, price)

  2. str_glue(“I’m {age} years old and live in {country}”)

  3. str_c(“\section{”, title, “}”)

food <- c("apple", "rice")
price <- c(10, 20)
age <- c(35, 45)
country <- c("China", "US")
title <- c("doctor", "teacher")

str_glue("The price of {food} is {price}")
The price of apple is 10
The price of rice is 20
str_c("I'm ", age, "years old and live in ", country)
[1] "I'm 35years old and live in China" "I'm 45years old and live in US"   
str_glue("\\\\section{{{title}}}")
\\section{doctor}
\\section{teacher}
注意

在处理字符串问题时,最好先将字符串的各部分分解出来再进行分析.如语句str_c("\\section{", title, "}")中的字符串,包括了三个部分:

\(N = 1\)

  1. “\section{”
  2. 变量title
  3. “}”

10.3 从字符串中提取数据

本部分我们将讨论: 如何提取一个字符串中的多个变量,主要有四个形式类似的函数:

separate_longer_delim(col, delim)separate_longer_position(col, width)separate_wider_delim(col, delim, names)separate_wider_position(col, widths)

  • pivot_longer()pivot_wider()函数类似,,对新生成的数据进行长宽的变换。
  • delim使用分隔符分割字符串,,position按指定宽度分割字符串。
  • 还有一个separate_wider_regex()函数,,可以根据正则表达式分割字符串,,在使用这个函数之前,,需要对正则表达式有所了解。

10.3.1 分行

df1 <- tibble(x = c("a,b,c", "d,e", "f"))
df1  |> 
  separate_longer_delim(x, delim = ",")
# A tibble: 6 × 1
  x    
  <chr>
1 a    
2 b    
3 c    
4 d    
5 e    
6 f    

10.3.2 分列

10.3.2.1 按分隔符分割

df3 <- tibble(x = c("a10.1.2022", "b10.2.2011", "e15.1.2015"))
df3  |> 
  separate_wider_delim(x, delim = ".", names = c("code", "edition", "year"))
# A tibble: 3 × 3
  code  edition year 
  <chr> <chr>   <chr>
1 a10   1       2022 
2 b10   2       2011 
3 e15   1       2015 

如果想在分割的过程汇中删除某列,,可以再names参数中将对应的列名设置为NA

df3  |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", NA, "edition")
  )
# A tibble: 3 × 2
  code  edition
  <chr> <chr>  
1 a10   2022   
2 b10   2011   
3 e15   2015   

10.3.2.2 按宽度分割

df4 <- tibble(x = c("202215TX", "202122LA", "202325CA"))
df4  |> 
  separate_wider_position(
    x,
    widths = c(year = 4, age = 2, state = 2)
  )
# A tibble: 3 × 3
  year  age   state
  <chr> <chr> <chr>
1 2022  15    TX   
2 2021  22    LA   
3 2023  25    CA   

10.3.2.3 wider问题延伸

10.3.2.3.1 分列数不够

我们先看一个例子

df <- tibble(x = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))
df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z")
  )
Error in `separate_wider_delim()`:
! Expected 3 pieces in each element of `x`.
! 2 values were too short.
ℹ Use `too_few = "debug"` to diagnose the problem.
ℹ Use `too_few = "align_start"/"align_end"` to silence this message.

报错了!报错的原因在于,separate_wider_delim()函数在分列时,,需要每个字符串的分裂数量一致,,显然在上面代码中,,“1-3”和”1”的分裂数不同。在实际工作中,,这个分裂数的不同可能是多或少,,需要在separate_wider_delim()函数中加入too_few参数。

df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("a", "x", "y", "z"),
    too_few = "debug" # 使用调试模式输出结果
  )
# A tibble: 5 × 7
  a     x     y     z     x_ok  x_pieces x_remainder
  <chr> <chr> <chr> <chr> <lgl>    <int> <chr>      
1 1     1-1-1 1     <NA>  FALSE        3 ""         
2 1     1-1-2 2     <NA>  FALSE        3 ""         
3 1     1-3   <NA>  <NA>  FALSE        2 ""         
4 1     1-3-2 2     <NA>  FALSE        3 ""         
5 1     1     <NA>  <NA>  FALSE        1 ""         

使用调试模式时,,输出中会多出三列:x_okx_piecesx_remainder(如果用不同的名称分隔变量,,会得到不同的前缀):

  • x_ok可以让你快速找到失败的输入.
  • x_pieces列包含了各字符串分裂后的有多少个元素.
  • x_remainder列包含了剩余的元素,通常在处理过多元素时使用.

在得知问题后,,我们有两种处理的选择:

  1. 解决分裂数不一致的问题,,即在分割前进一步预处理,,确保每个字符串的分裂数量一致。
  2. 也可以使用too_few参数,,用NA填补缺失的部分。
df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "align_start" # align_end和align_start控制填补NA的位置
  )
# A tibble: 5 × 3
  x     y     z    
  <chr> <chr> <chr>
1 1     1     1    
2 1     1     2    
3 1     3     <NA> 
4 1     3     2    
5 1     <NA>  <NA> 
10.3.2.3.2 分列数过多
df <- tibble(x = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))
df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "debug"
  )
# A tibble: 5 × 6
  x         y     z     x_ok  x_pieces x_remainder
  <chr>     <chr> <chr> <lgl>    <int> <chr>      
1 1-1-1     1     1     TRUE         3 ""         
2 1-1-2     1     2     TRUE         3 ""         
3 1-3-5-6   3     5     FALSE        4 "-6"       
4 1-3-2     3     2     TRUE         3 ""         
5 1-3-5-7-9 3     5     FALSE        5 "-7-9"     

我们同样有两种选择:

  1. 直接舍弃多的元素
  2. 将多的元素全部合并至最后一列
df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "drop"
  )
# A tibble: 5 × 3
  x     y     z    
  <chr> <chr> <chr>
1 1     1     1    
2 1     1     2    
3 1     3     5    
4 1     3     2    
5 1     3     5    
df  |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "merge"
  )
# A tibble: 5 × 3
  x     y     z    
  <chr> <chr> <chr>
1 1     1     1    
2 1     1     2    
3 1     3     5-6  
4 1     3     2    
5 1     3     5-7-9

10.4 处理字符串中的字母

如何查询字符串长度,提取子字符串以及在图形和表中处理字符串.

10.4.1 查询字符串长度-str_length()

str_length(c("a", "R for data science", NA))
[1]  1 18 NA

str_length()函数可以和dplyr系列函数连用.

babynames  |> 
  count(length = str_length(name), wt = n)
# A tibble: 14 × 2
   length        n
    <int>    <int>
 1      2   338150
 2      3  8589596
 3      4 48506739
 4      5 87011607
 5      6 90749404
 6      7 72120767
 7      8 25404066
 8      9 11926551
 9     10  1306159
10     11  2135827
11     12    16295
12     13    10845
13     14     3681
14     15      830
babynames  |> 
  filter(str_length(name) == 15)  |> 
  count(name, wt = n, sort = TRUE)
# A tibble: 34 × 2
   name                n
   <chr>           <int>
 1 Franciscojavier   123
 2 Christopherjohn   118
 3 Johnchristopher   118
 4 Christopherjame   108
 5 Christophermich    52
 6 Ryanchristopher    45
 7 Mariadelosangel    28
 8 Jonathanmichael    25
 9 Christianjoseph    22
10 Christopherjose    22
# ℹ 24 more rows

10.4.2 提取子字符串-str_sub()

str_sub(字符串, start, end)

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
[1] "App" "Ban" "Pea"
str_sub(x, -3, -1)
[1] "ple" "ana" "ear"

str_sub()函数同样可以和dplyr系列函数连用.

babynames  |> 
  mutate(
    first = str_sub(name, 1, 1), # 提取每个名字的第一个字母
    last = str_sub(name, -1, -1) # 提取每个名字的最后一个字母
  )
# A tibble: 1,924,665 × 7
    year sex   name          n   prop first last 
   <dbl> <chr> <chr>     <int>  <dbl> <chr> <chr>
 1  1880 F     Mary       7065 0.0724 M     y    
 2  1880 F     Anna       2604 0.0267 A     a    
 3  1880 F     Emma       2003 0.0205 E     a    
 4  1880 F     Elizabeth  1939 0.0199 E     h    
 5  1880 F     Minnie     1746 0.0179 M     e    
 6  1880 F     Margaret   1578 0.0162 M     t    
 7  1880 F     Ida        1472 0.0151 I     a    
 8  1880 F     Alice      1414 0.0145 A     e    
 9  1880 F     Bertha     1320 0.0135 B     a    
10  1880 F     Sarah      1288 0.0132 S     h    
# ℹ 1,924,655 more rows

10.4.2.1 练习

提取每个婴儿姓名中中间的字母.

babynames  |> 
  mutate(
    middle_letter = if_else(
      str_length(name) %% 2 == 1,
      str_sub(name, (str_length(name) / 2 + 1), (str_length(name) / 2 + 1)),
      str_sub(name, (str_length(name) / 2 - 1), (str_length(name) / 2))
    )
  )
# A tibble: 1,924,665 × 6
    year sex   name          n   prop middle_letter
   <dbl> <chr> <chr>     <int>  <dbl> <chr>        
 1  1880 F     Mary       7065 0.0724 Ma           
 2  1880 F     Anna       2604 0.0267 An           
 3  1880 F     Emma       2003 0.0205 Em           
 4  1880 F     Elizabeth  1939 0.0199 a            
 5  1880 F     Minnie     1746 0.0179 in           
 6  1880 F     Margaret   1578 0.0162 rg           
 7  1880 F     Ida        1472 0.0151 d            
 8  1880 F     Alice      1414 0.0145 i            
 9  1880 F     Bertha     1320 0.0135 er           
10  1880 F     Sarah      1288 0.0132 r            
# ℹ 1,924,655 more rows

10.5 非英语文本

处理非英语文本难免遇到意料之外的难题,包括字符编码问题、带变音符的字母、地区敏感的字符串排序与大小写转换。

10.5.1 字符编码

  • UTF-8: 目前最常用的字符编码,可以表示几乎所有语言的字符。
  • 使用guess_encoding()函数猜测字符串的编码。
  • 字符编码是丰富且复杂的,深入的探索请参看:https://kunststube.net/encoding/。

10.5.2 字母变体

在处理带重音符号的语言时,确定字母位置(例如使用 str_length()str_sub() )会面临重大挑战,因为带重音符号的字母可能被编码为单个字符(如ü),也可能通过组合无重音字母(如 u)和变音符号(如¨)形成两个字符。例如,以下代码展示了两种看起来完全相同的ü表示方式:

u <- c("\u00fc", "u\u0308")
str_length(u) # 字符串u中的两个字符输出相同
[1] 1 2
str_sub(u, 1, 1) # 但提取相同位置的字符不同
[1] "ü" "u"
u[[1]] == u[[2]] # 两者也不完全相同
[1] FALSE
str_equal(u[[1]], u[[2]]) # 识别两个字符串的输出外观是否相同
[1] TRUE

10.5.3 地区敏感性

10.6 正则表达式

正则表达式是一种简洁而强大的语言,用于描述字符串中的模式。本节我们将通过几个简单的示例了解正则表达式的基本概念。

10.6.1 模式基础

str_view()( 小节 10.1.2 有介绍)可以显示字符串向量所匹配的元素,并用<>包围,并用蓝色高亮显示匹配的元素,这个函数是学习正则表达式的利器。

  • 数字和字母可以完全匹配,称为文字字符
  • 大多数标点符号,例如., +, *, [,], ?,都有特殊的含义,被称为元字符:
    • .:匹配任意单字符。
    • ?:匹配指定字符0次或1次。
    • +:匹配指定字符至少1次。
    • *:匹配指定字符任意次数(包括0次)。
    • []:匹配一组字符其中的字符,[^]反匹配。
  • |:表示多个匹配模式共同作用。
# 匹配文字字符
str_view(fruit, "berry")
 [6] │ bil<berry>
 [7] │ black<berry>
[10] │ blue<berry>
[11] │ boysen<berry>
[19] │ cloud<berry>
[21] │ cran<berry>
[29] │ elder<berry>
[32] │ goji <berry>
[33] │ goose<berry>
[38] │ huckle<berry>
[50] │ mul<berry>
[70] │ rasp<berry>
[73] │ salal <berry>
[76] │ straw<berry>
# 元字符匹配-匹配所有字母a后跟任意一个字母(.)的字符串
# 如需两个字母,那就需要两个.符号
str_view(c("a", "ab", "ae", "bd", "ea", "eabc"), "a.")
[2] │ <ab>
[3] │ <ae>
[6] │ e<ab>c
# 元字符匹配-找出所有包含字母a+三个字母+字母e的水果字符串
str_view(fruit, "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
# 元字符匹配-匹配包含字母a+0个或1个字母b的字符串
str_view(c("a", "ab", "abbc"), "ab?")
[1] │ <a>
[2] │ <ab>
[3] │ <ab>bc
# 元字符匹配-匹配包含字母a+至少一个字母b的字符串
str_view(c("a", "ab", "abbc"), "ab+")
[2] │ <ab>
[3] │ <abb>c
# 元字符匹配-匹配包含字母a+任意个数母b(包括0次)的字符串
str_view(c("a", "ab", "abbc"), "ab*")
[1] │ <a>
[2] │ <ab>
[3] │ <abb>c
# 元字符匹配-查找包含由元音包围的 "x "或由辅音包围的 "y "的单词
str_view(words, "[aeiou]x[aeiou]")
[284] │ <exa>ct
[285] │ <exa>mple
[288] │ <exe>rcise
[289] │ <exi>st
str_view(words, "[^aeiou]y[^aeiou]")
[836] │ <sys>tem
[901] │ <typ>e
# 匹配包含apple,melon,nut的字符串
str_view(fruit, "apple|melon|nut")
 [1] │ <apple>
[13] │ canary <melon>
[20] │ coco<nut>
[52] │ <nut>
[62] │ pine<apple>
[72] │ rock <melon>
[80] │ water<melon>
注记

正则表达式非常紧凑,而且使用了大量标点符号,因此一开始可能会让人觉得难以理解和阅读。别担心,只要多加练习,就可以很好的掌握.

10.6.2 关键函数

我们可以通过stringr包中的一些拥有重要功能的有用的函数来加速理解正则表达式的过程.

10.6.2.1 检测匹配-str_detect()

str_detect()返回一个逻辑值向量,这个逻辑值向量与初始向量长度相同,可以与filter()summarise()函数完美搭配。

  • str_subset()函数返回一个只包含匹配字符串的字符向量。
  • str_which()函数返回一个给出匹配字符串位置的整数向量。
str_detect(c("a", "b", "c"), "[aeiou]")
[1]  TRUE FALSE FALSE
str_subset(c("a", "b", "c"), "[aeiou]")
[1] "a"
str_which(c("a", "b", "c"), "[aeiou]")
[1] 1
# 与mutate()连用
babynames |>
  filter(str_detect(name, "x")) |> # 筛选所有包含x的行
  count(name, wt = n, sort = TRUE) # 以n为基础,计算总数
# A tibble: 974 × 2
   name            n
   <chr>       <int>
 1 Alexander  665492
 2 Alexis     399551
 3 Alex       278705
 4 Alexandra  232223
 5 Max        148787
 6 Alexa      123032
 7 Maxine     112261
 8 Alexandria  97679
 9 Maxwell     90486
10 Jaxon       71234
# ℹ 964 more rows
# 与summarise()连用
babynames |>
  group_by(year) |>
  summarise(prop_x = mean(str_detect(name, "x"))) |>
  # 每年中包含字母“x”的婴儿名字所占比例
  ggplot(aes(x = year, y = prop_x)) +
  geom_line()

10.6.2.2 计数匹配

  • str_count()返回每个字符串中匹配模式出现的次数。
  • 需要注意,匹配的字段之间是不重叠计算的
  • 正则表达式的匹配默认是区分大小写的。在统计计数时,如果遇到不需要区分大小写的情况,可使用以下方式修正:
    • 同时匹配大写和小写字母,如"[aeiouAEIOU]"表示同时匹配大写小写元音字母。
    • 忽略大小写:regex("[aeiou], ignore_case = TRUE")
    • 将数据与先处理为统一模式(小写或大写):str_to_lower()/str_to_upper()
# 一般计数
x <- c("apple", "banana", "pear")
str_count(x, "p")
[1] 2 0 1
# 包含大小写的计数
babynames |>
  count(name) |>
  mutate(
    vowels = str_count(name, regex("[aeiou]", ignore_case = T)),
    vowels_1 =  str_count(name, "[aeiouAEIOU]"),
    consonants = str_count(name, regex("[^aeiou]", ignore_case = T))
  )
# A tibble: 97,310 × 5
   name          n vowels vowels_1 consonants
   <chr>     <int>  <int>    <int>      <int>
 1 Aaban        10      3        3          2
 2 Aabha         5      3        3          2
 3 Aabid         2      3        3          2
 4 Aabir         1      3        3          2
 5 Aabriella     5      5        5          4
 6 Aada          1      3        3          1
 7 Aadam        26      3        3          2
 8 Aadan        11      3        3          2
 9 Aadarsh      17      3        3          4
10 Aaden        18      3        3          2
# ℹ 97,300 more rows

10.6.2.3 替换字符

# 替换
x <- c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-") # 只替换第一个匹配
[1] "-pple"  "p-ar"   "b-nana"
str_replace_all(x, "[aeiou]", "-") # 替换所有匹配
[1] "-ppl-"  "p--r"   "b-n-n-"
# 删除
str_remove(x, "[aeiou]") # 只删除第一个匹配
[1] "pple"  "par"   "bnana"
str_remove_all(x, "[aeiou]") # 删除所有匹配
[1] "ppl" "pr"  "bnn"

10.6.2.4 提取变量

  • 使用 separate_wider_regex() 可将结构化的字符串拆成多列。

    • 如果匹配失败的话,可以使用 too_few = "debug" 可以定位匹配失败的原因(类似@sec-wider-problems)。
df <- tribble(
  ~str              ,
  "<Sheryl>-F_34"   ,
  "<Kisha>-F_45"    ,
  "<Brandon>-N_33"  ,
  "<Sharon>-F_38"   ,
  "<Penny>-F_58"    ,
  "<Justin>-M_41"   ,
  "<Patricia>-F_84"
)

# 使用separate_wider_regex()函数df中的人民、性别和年龄
df |>
  separate_wider_regex(
    str,
    patterns = c(
      "<",
      name = "[A-Za-z]+",
      ">-",
      gender = ".",
      "_",
      age = "[0-9]+"
    )
  )
# A tibble: 7 × 3
  name     gender age  
  <chr>    <chr>  <chr>
1 Sheryl   F      34   
2 Kisha    F      45   
3 Brandon  N      33   
4 Sharon   F      38   
5 Penny    F      58   
6 Justin   M      41   
7 Patricia F      84   

10.6.2.5 模式细节

10.6.2.5.1 转义(escaping)

像字符串一样,正则表达式使用反斜杠 \ 进行转义。所以,为了匹配字面上的 .,需要使用正则表达式 \.。问题在于,我们是用字符串来表示正则表达式的,而字符串中 \ 同样是转义符。因此,要表示正则表达式 \.,需要写成字符串 "\\.",也就是“嵌套式双重转义”

  • 为避免多重嵌套转义的混乱,也可以使用原始字符串语法 r"()" 来表示正则表达式,在这种情况下,反斜杠不需要转义。
  • 对于诸如 .$|*+?{}() 等特殊字符,也可使用字符类 [.][$] 等方式表示其字面值。
# 嵌套式双重转义
dot <- "\\."
str_view(dot)
[1] │ \.
str_view(c("abc", "a.c", "bef"), "a\\.c") #$ 匹配包含a.c的字符串
[2] │ <a.c>
# 使用原始字符串
str_view("a\\b", r"{\\}") # 匹配包含a\b的字符串
[1] │ a<\>b
# 特殊字符类
str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c") # 匹配包含"a.c"的字符串
[2] │ <a.c>
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c") # 匹配包含"任意字符*c"的字符串
[3] │ <a*c>
10.6.2.5.2 锚点(Anchors)

默认情况下,正则表达式会匹配字符串中的任意部分,但有时我们希望匹配字符串的开头或结尾。^ 用于匹配字符串的开头,$ 用于匹配字符串的结尾。

  • 若希望整个字符串完全匹配某模式,同时使用 ^$ 即可。
  • \b 可用于匹配单词边界(即单词的开头或结尾),常用于避免误匹配。
# 匹配开头
str_view(fruit, "^a") # 匹配以字母a开头的字符串
[1] │ <a>pple
[2] │ <a>pricot
[3] │ <a>vocado
# 匹配结尾
str_view(fruit, "a$") # 匹配以字母e结尾的字符串
 [4] │ banan<a>
[15] │ cherimoy<a>
[30] │ feijo<a>
[36] │ guav<a>
[56] │ papay<a>
[74] │ satsum<a>
# 完全匹配
str_view(fruit, "apple") # 匹配包含apple字符串
 [1] │ <apple>
[62] │ pine<apple>
str_view(fruit, "^apple$") # 完全匹配apple字符串
[1] │ <apple>
# 查找所有`sum()`的方法,\b匹配边界,避免匹配到"summarize(df)"等
x <- c("summary(x)", "summarize(df)", "rowsum(x)", "sum(x)")
str_view(x, "\\bsum\\b")
[4] │ <sum>(x)
10.6.2.5.3 字符(Character Classes)

字符类用于匹配集合中的任一字符。可用 [] 来构造,例如 [abc] 匹配 a、b 或 c,[^abc] 匹配除这三者外的任意字符。在 [] 内部,除 ^ 外,还有两个具有特殊含义的字符:

  • - 表示范围,如 [a-z] 表示小写字母,[0-9] 表示数字;
  • \ 用于转义特殊字符,如 [\^\-\]] 匹配 ^、-、]

此外,一些常见的字符类还有简写形式:

  • 数字相关:\d:数字,\D:非数字;
  • \s:空白字符,\S:非空白;
  • \w:字母或数字,\W:非字母或数字。
x <- "abcd ABCD 12345 -!@#%."
str_view(x, "[abc]+") # 匹配abc
[1] │ <abc>d ABCD 12345 -!@#%.
str_view(x, "[a-z]+") # 匹配小写字母
[1] │ <abcd> ABCD 12345 -!@#%.
str_view(x, "[^a-z0-9]+") # 匹配除小写字母和数字
[1] │ abcd< ABCD >12345< -!@#%.>
# 注意-字符同样需要转义
str_view("a-b-c", "[a-c]")  # 匹配a到c的小写字母
[1] │ <a>-<b>-<c>
str_view("a-b-c", "[a\\-c]") # 匹配a,-,c三个字符
[1] │ <a><->b<-><c>
10.6.2.5.4 量词-控制(Match Quantifiers)

量词用于指定前一个字符或字符类的重复次数。常用的量词有:

  • ?:匹配0次或1次;
  • +:匹配1次或多次;
  • *:匹配0次或多次;
  • {n}:匹配恰好 n 次;
  • {n,}:匹配至少 n 次;
  • {n,m}:匹配至少 n 次,但不超过 m 次。
10.6.2.5.5 分组与捕获(Groups and capturing)
  • 使用圆括号 () 可将多个字符组合成一个整体,称为分组。分组后的整体可与量词结合使用,也可通过反向引用在替换操作中重复使用。

  • 括号不仅可以控制优先级,还能创建“捕获组”(capturing group),形如(...),以便在后续使用匹配子模式的结果。具体来看:\1 表示与第一个括号的内容相同,\2 表示第二个,以此类推。

str_view(fruit, "(..)\\1") # 找出两个连续字符重复的词
 [4] │ b<anan>a
[20] │ <coco>nut
[22] │ <cucu>mber
[41] │ <juju>be
[56] │ <papa>ya
[73] │ s<alal> berry
str_view(words, "^(..).*\\1$") # 找出首尾两个字符一样的单词
[152] │ <church>
[217] │ <decide>
[617] │ <photograph>
[699] │ <require>
[739] │ <sense>

10.6.2.6 模式控制

10.6.2.6.1 正则表达式标志

有一些设置可以用来调整正则表达式的细节,这在其他语言中通常被称为 flags(标志)。可以通过 regex() 函数将字符串包裹起来来使用这些设置。常用的标志有:

  • ignore_case = TRUE:匹配时忽略大小写。
  • dotall = TRUE:让.匹配一切字符,包括\n
  • multiline = TRUE:让 ^$ 匹配每一行的开头和结尾。
  • comments = TRUE:允许使用空格和注释来增强可读性。可以在正则表达式中添加空格和 # 注释来解释每一部分。这些空格默认会被忽略,如要匹配空格或 #,则需使用反斜杠转义。
# 忽略大小写
bananas <- c("banana", "Banana", "BANANA")
str_view(bananas, "banana") # 只匹配"banana"
[1] │ <banana>
str_view(bananas, regex("banana", ignore_case = TRUE)) # 同时匹配大小写
[1] │ <banana>
[2] │ <Banana>
[3] │ <BANANA>
# .匹配一切字符
x <- "Line 1\nLine 2\nLine 3"
str_view(x, ".Line") # 会产生报错
# 以下代码将正则表达式 .Line 改写为允许点号 . 匹配任意字符(包括换行符),这样就能跨越行与行之间的 \n,成功匹配 \nL 这种中间有换行的情况。
str_view(x, regex(".Line", dotall = TRUE))
[1] │ Line 1<
    │ Line> 2<
    │ Line> 3
# 让 ^ 和 $ 匹配每一行的开头和结尾
str_view(x, "^Line") # 只匹配第一行
[1] │ <Line> 1
    │ Line 2
    │ Line 3
str_view(x, regex("^Line", multiline = TRUE)) # 匹配每一行的Line
[1] │ <Line> 1
    │ <Line> 2
    │ <Line> 3
# 正则中加入注释
phone <- regex(
  r"(
    \(?     # 可选的左括号
    (\d{3}) # 三位区号
    [)\-]?  # 可选的右括号或短横线
    \ ?     # 可选空格
    (\d{3}) # 三位号码
    [\ -]?  # 可选空格或短横线
    (\d{4}) # 四位号码
  )",
  comments = TRUE
)
str_extract(c("514-791-8141", "(123) 456 7890", "123456"), phone)
[1] "514-791-8141"   "(123) 456 7890" NA              
10.6.2.6.2 不用正则表达式的标志

除了 regex() 函数包裹的正则表达式标志设置,还可以通过 fixed() 函数来关闭正则表达式的规则,直接按字面意义匹配。

  • fixed()中也可以配合 ignore_case = TRUE 忽略大小写:
str_view(c("", "a", "."), fixed(".")) # 只匹配真正的 “.” 字符
[3] │ <.>
str_view("x X", fixed("X", ignore_case = TRUE))
[1] │ <x> <X>

10.7 总结

  • 正则表达式是最紧凑的语言表达之一,一开始它们肯定会令人困惑,但当我们不断训练我们眼睛来阅读它们并训练我们的大脑来理解它们时,我们就会解锁一项可以在 R 和许多其他地方使用的强大技能。

  • https://www.regular-expressions.info/tutorial.html 包含了有关正则表达式的高级功能,我们可以使用它来了解正则表达式的最高级功能以及它们在幕后如何工作。

TODO: 深入学习正则表达式。