9.字符串

1 简介

接下来将学习 R 中的字符串操作。本节中你将学习字符串如何工作以及如何手动创建字符串的基础知识,重点部分是正则表达式。正则表达式很有用,因为字符串通常包含非结构化或半结构化数据,而正则表达式是一种描述字符串模式的简洁语言。

1.1 加载包

本节介绍的字符串操作使用的是stringr包,它是 tidyverse 核心部分之一。

library(tidyverse)

2 字符串基础

可以使用单引号或双引号创建字符串。如果你想创建包含双引号的字符串,可以通过单引号来创建,如下所示:

string1 <- "This is a string"
string2 <- 'If I want to include a "quote" inside a string, I use single quotes'

有的时候你可能会忘记关闭引号,则会出现+连续字符:

> "This is a string without a closing quote
+ 
+ 
+ HELP I'M STUCK

如果遇到这种情况,按 Ctrl + c 退出或者补全引号结束。

要在字符串中只包含单引号或双引号,可以使用\它来“转义”它:

double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'"

这意味着如果你想包含一个文字反斜杠,则需要两个反斜杠:"\\"

注意,打印字符串与字符串本身不同,因为打印要查看字符串的原始内容:

x <- c("\"", "\\")
x
#> [1] "\"" "\\"
writeLines(x)
#> "
#> \

还有一些其他特殊字符。最常见的是"\n"换行符和"\t"制表符,但您可以通过?'"'?"'"来查看"'的帮助文档。有时还会看到类似"\u00b5"的字符串,这是一种在所有平台上都适用的书写非英文字符的方法:

x <- "\u00b5"
x
#> [1] "µ"

把多个字符串存储在一个字符向量中,可以使用c()命令创建:

c("one", "two", "three")
#> [1] "one"   "two"   "three"

2.1 字符串长度

虽然R 中的Base包含许多处理字符串的函数,但我们将使用 stringr 中的函数。它们有更直观的名称,并且都以str_。例如,str_length()字符串中的字符数:

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

如果你在RStudio使用str_,则这个str_前缀特别有用,当你输入str_时会触发可以使用的 stringr 函数:

image.png

2.2 字符串合并

要合并两个或多个字符串,使用str_c()

str_c("x", "y")
#> [1] "xy"
str_c("x", "y", "z")
#> [1] "xyz"

使用sep参数来控制它们的分隔方式:

str_c("x", "y", sep = ", ")
#> [1] "x, y"

与 R 中的大多数其他函数一样,缺失值具有传染性。如果希望它们打印为"NA",请使用str_replace_na()

x <- c("abc", NA)
str_c("|-", x, "-|")
#> [1] "|-abc-|" NA
str_c("|-", str_replace_na(x), "-|")
#> [1] "|-abc-|" "|-NA-|"

如上所示,str_c()被向量化,它会自动将较短的向量回收到与最长的向量相同的长度:

str_c("prefix-", c("a", "b", "c"), "-suffix")
#> [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"

长度为 0 的对象被丢弃。这在if条件语句下结合使用时特别有用:

name <- "Hadley"
time_of_day <- "morning"
birthday <- FALSE

str_c(
  "Good ", time_of_day, " ", name,
  if (birthday) " and HAPPY BIRTHDAY",
  "."
)
#> [1] "Good morning Hadley."

要将字符串向量合并为单个字符串,使用collapse

str_c(c("x", "y", "z"), collapse = ", ")
#> [1] "x, y, z"

2.3 字符串子集

提取字符串的一部分字符可以使用 str_sub()str_sub()需要提供startend参数获取该子串的位置:

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
#> [1] "App" "Ban" "Pea"
# negative numbers count backwards from end
str_sub(x, -3, -1)
#> [1] "ple" "ana" "ear"

请注意,str_sub()如果字符串太短,则不会出错:

str_sub("a", 1, 5)
#> [1] "a"

您还可以使用str_sub()的赋值形式来修改字符串:

str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))
x
#> [1] "apple"  "banana" "pear"

2.4 语言环境

上面使用的str_to_lower()将文本更改为小写。还可以使用str_to_upper()str_to_title()。然而,改变大小写比它最初看起来更复杂,因为不同的语言有不同的改变大小写的规则。可以通过指定区域设置来选择要使用的规则集:

# Turkish has two i's: with and without a dot, and it
# has a different rule for capitalising them:
str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"

语言环境指定为 ISO 639 语言代码,它是两个或三个字母的缩写。如果您还不知道您的语言的代码,维基百科可以查看。如果将区域设置留空,它将使用操作系统提供的当前区域设置。

排序也受语言环境影响。Base R中order()sort()函数使用当前语言环境对字符串进行排序。如果你想要在不同计算机进行排序,可以使用str_sort()str_order()并添加locale参数:

x <- c("apple", "eggplant", "banana")

str_sort(x, locale = "en")  # English
#> [1] "apple"    "banana"   "eggplant"

str_sort(x, locale = "haw") # Hawaiian
#> [1] "apple"    "eggplant" "banana"

还有一些比较常用的基本函数:str_length()str_wrap()str_trim()等等。

3 正则表达式匹配模式

正则表达式是一种非常简洁的语言,它允许在字符串匹配模式中设定相应的匹配模式。刚开始学习时可能比较难理解,只要你们掌握了它,用处将非常大。

要学习正则表达式,我们将使用str_view()str_view_all()。这些函数接受一个字符向量和一个正则表达式,并展示它们是如何匹配的。我们将从非常简单的正则表达式开始,然后逐渐变得越来越复杂。一旦学会了模式匹配,你会了解如何应用到各种 stringr 函数中。

3.1 基本匹配

简单的匹配精确的字符串:

x <- c("apple", "banana", "pear")
str_view(x, "an")
image.png

接下来进行稍复杂的匹配:.匹配任何字符(换行符除外)

str_view(x, ".a.")
image.png

如果要匹配.,理论上需要用正则表达式\.。然而这会产生一个问题。我们用字符串来表示正则表达式,\在字符串中也用作转义符。所以要创建正则表达式,\.,则需要使用"\\."

# To create the regular expression, we need \\
dot <- "\\."

# But the expression itself only contains one:
writeLines(dot)
#> \.

# And this tells R to look for an explicit .
str_view(c("abc", "a.c", "bef"), "a\\.c")
image.png

\在正则表达式中用作转义字符,那么如何匹配\呢?首先你需要转义它,创建正则表达式\\。创建该正则表达式,您需要使用一个字符串,该字符串也需要转义\。这意味着匹配\你需要写成"\\\\"——即需要四个反斜杠来匹配一个!

x <- "a\\b"
writeLines(x)
#> a\b

str_view(x, "\\\\")
image.png
x = c("a\'b",'c\"d',"e\\f")
writeLines(x)
#> a'b
#> c"d
#> e\f
str_view(x,"\"")
image.png

3.2 指定锚点

默认情况下,正则表达式将匹配字符串的任何部分。锚点在正则表达式中很常用,在字符串的开头或结尾进行匹配,可以使用:

  • ^ 匹配字符串的开头。
  • $ 匹配字符串的结尾。
x <- c("apple", "banana", "pear")
str_view(x, "^a")
image.png
str_view(x, "a$")
image.png

要强制使用正则表达式精确匹配一个完整的字符串,锚定位置用^$

x <- c("apple pie", "apple", "apple cake")
str_view(x, "apple")
image.png
str_view(x, "^apple$")
image.png

可以使用\b匹配单词之间的边界。例如,寻找\bsum\b避免匹配summarisesummaryrowsum等等。

如何匹配字符串"$^$"

x <- "$100^2999$"
str_view(x,"^\\$.*\\$$")
image.png

3.3 字符类替代

有许多特殊模式可以匹配多个字符。前面已经了解了.,它匹配除换行符之外的任何字符。还有其他四个有用的工具:

  • \d: 匹配任何数字。
  • \s: 匹配任何空格(例如空格、制表符、换行符)。
  • [abc]: 匹配 a、b 或 c。
  • [^abc]: 匹配除 a、b 或 c 之外的任何内容。

请记住,要创建包含\d\s的正则表达式,需要对\字符串进行转义,即"\\d""\\s"

如果想匹配单个字符,可以通过[ ]来代替\使用。

# Look for a literal character that normally has special meaning in a regex
str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
image.png
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
image.png
str_view(c("abc", "a.c", "a*c", "a c"), "a[ ]")
image.png

这适用于大多数(但不是全部)正则表达式元字符:$ . | ? * + ( ) [ {。然而这些字符:] \ ^ -,由于有特殊含义,必须使用反斜杠转义来处理。

你可以在一种或多种替代模式之间进行选择。例如,abc|d..f将匹配 '“abc”' 或"deaf". 请注意, |的优先级较低,因此abc|xyz匹配abcxyz,不匹配abcyzabxyz。如果匹配比较复杂,可以使用括号区分:

str_view(c("grey", "gray"), "gr(e|a)y")
image.png

3.4 重复模式

控制匹配模式的次数:

  • ?: 0次或1次
  • +: 1次或多次
  • *: 0次或多次
x <- "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"
str_view(x, "CC?")
image.png
str_view(x, "CC+")
image.png
str_view(x, 'C[LX]+')
image.png

指定具体匹配次数:

  • {n}: 只匹配n次
  • {n,}: 匹配n次或多于n次
  • {,m}: 最多不超过m次
  • {n,m}: 在 n 和 m 之间
str_view(x, "C{2}")
image.png
str_view(x, "C{2,}")
image.png
str_view(x, "C{2,3}")
image.png

默认情况下都是采用贪婪模式进行匹配的,贪婪模式将匹配尽可能长的字符串。如果想用非贪婪模式匹配,可以在匹配项后面通过添加?来实现。

str_view(x, 'C{2,3}?')
image.png
str_view(x, 'C[LX]+?')
image.png

3.5 分组和反向引用

前面,我们已经知道圆括号可以解决匹配复杂表达式。其实括号还创建一个编号的捕获组(编号 1、2 等)。捕获组存储括号内的正则表达式匹配字符串部分。如果想反向引用(参考先前由捕获组匹配的相同的文本),比如\1\2等等。

str_view(fruit, "(..)\\1", match = TRUE)
image.png

4 匹配工具

前面我们已经了解了正则表达式的基础知识,那么在实际运用中的stringr函数可以解决哪些问题呢?

  • 确定哪些字符串匹配。
  • 找到匹配的位置。
  • 提取匹配内容。
  • 用新值替换匹配项。
  • 根据匹配拆分字符串。

在我们继续之前,请注意一点:因为正则表达式非常强大,所以很容易使用单个正则表达式解决所有问题。

下面这个例子是检查电子邮件地址是否有效的正则表达式:

(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:
\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(
?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ 
\t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\0
31]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\
](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+
(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:
(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z
|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)
?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\
r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[
 \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)
?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t]
)*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[
 \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*
)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)
*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+
|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r
\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:
\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t
]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031
]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](
?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?
:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?
:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?
:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?
[ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] 
\000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|
\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>
@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"
(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t]
)*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?
:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[
\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-
\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(
?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;
:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([
^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\"
.\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\
]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\
[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\
r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] 
\000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]
|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \0
00-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\
.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,
;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?
:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*
(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".
\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[
^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]
]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*(
?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(
?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[
\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t
])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t
])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?
:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|
\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:
[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\
]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)
?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["
()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)
?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[
 \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,
;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t]
)*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?
(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".
\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:
\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[
"()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])
*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])
+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\
.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z
|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(
?:\r\n)?[ \t])*))*)?;\s*)

在实际使用的时候,这个电子邮件地址检测就非常冗余了,关于这个问题的讨论,可参考http://stackoverflow.com/a/201378的 stackoverflow 讨论。

4.1 查看是否匹配

要查看字符向量是否与正则表达式匹配,请使用str_detect()。它返回一个与输入长度相同的逻辑向量:

x <- c("apple", "banana", "pear")
str_detect(x, "e")
#> [1]  TRUE FALSE  TRUE

当我们进行数字计算(summean)时:FALSE变为0,TRUE变为1

# How many common words start with t?
sum(str_detect(words, "^t"))
#> [1] 65
# What proportion of common words end with a vowel?
mean(str_detect(words, "[aeiou]$"))
#> [1] 0.2765306

当您有复杂的逻辑条件(例如除了d之外匹配 a 或 b 但不匹配 c )时,将调用多个str_detect()与逻辑运算符组合,而不是依靠单个正则表达式。例如,有两种方法可以查找所有不包含任何元音的单词:

# Find all words containing at least one vowel, and negate
no_vowels_1 <- !str_detect(words, "[aeiou]")
# Find all words consisting only of consonants (non-vowels)
no_vowels_2 <- str_detect(words, "^[^aeiou]+$")
identical(no_vowels_1, no_vowels_2)    # 判断对象是否完全相等
#> [1] TRUE

上面两种方法的结果完全相同,但是第一种更容易理解。

str_detect()的一个常见用途是选择与模式匹配的元素。可以使用逻辑子集或str_subset()来实现:

words[str_detect(words, "x$")]
#> [1] "box" "sex" "six" "tax"
str_subset(words, "x$")
#> [1] "box" "sex" "six" "tax"

在通常情况下,你所需的字符串是数据框的一列,可以通过filter过滤:

df <- tibble(
  word = words, 
  i = seq_along(word)
)
df %>% 
  filter(str_detect(word, "x$"))
#> # A tibble: 4 x 2
#>   word      i
#>   <chr> <int>
#> 1 box     108
#> 2 sex     747
#> 3 six     772
#> 4 tax     841

str_count()str_detect()的一个变体,而不是简单的 yes 或 no,它显示字符串中匹配的次数:

x <- c("apple", "banana", "pear")
str_count(x, "a")
#> [1] 1 3 1

# On average, how many vowels per word?
mean(str_count(words, "[aeiou]"))
#> [1] 1.991837

mutate()经常使用str_count()

df %>% 
  mutate(
    vowels = str_count(word, "[aeiou]"),
    consonants = str_count(word, "[^aeiou]")
  )
#> # A tibble: 980 x 4
#>   word         i vowels consonants
#>   <chr>    <int>  <int>      <int>
#> 1 a            1      1          0
#> 2 able         2      2          2
#> 3 about        3      3          2
#> 4 absolute     4      4          4
#> 5 accept       5      2          4
#> 6 account      6      3          4
#> # … with 974 more rows

请注意,匹配过程不会重复匹配。例如,在"abababa" 中,将"aba"匹配多少次?正则表达式说的是两个,而不是三个:

str_count("abababa", "aba")
#> [1] 2
str_view_all("abababa", "aba")
image.png

4.2 匹配提取

要提取匹配的文本,使用str_extract()。通过使用Harvard_sentences例子。

length(sentences)
#> [1] 720
head(sentences)
#> [1] "The birch canoe slid on the smooth planks." 
#> [2] "Glue the sheet to the dark blue background."
#> [3] "It's easy to tell the depth of a well."     
#> [4] "These days a chicken leg is a rare dish."   
#> [5] "Rice is often served in round bowls."       
#> [6] "The juice of lemons makes fine punch."

想象一下,我们想要找到所有包含颜色的句子。我们首先创建一个颜色名称向量,然后将其转换为单个正则表达式:

colours <- c("red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match
#> [1] "red|orange|yellow|green|blue|purple"

现在我们可以选择包含颜色的句子,然后提取颜色来找出它是哪个:

has_colour <- str_subset(sentences, colour_match)
matches <- str_extract(has_colour, colour_match)
head(matches)
#> [1] "blue" "blue" "red"  "red"  "red"  "blue"

请注意,str_extract()仅提取第一个匹配项。通过查看匹配项超过 1个的句子:

more <- sentences[str_count(sentences, colour_match) > 1]
str_view_all(more, colour_match)
image.png

str_extract(more, colour_match)
#> [1] "blue"   "green"  "orange"

这是 stringr 函数的常见模式,因为使用单个匹配项使用更简单的数据结构。要获取所有匹配项,请使用str_extract_all()。它返回一个列表:

str_extract_all(more, colour_match)
#> [[1]]
#> [1] "blue" "red" 
#> 
#> [[2]]
#> [1] "green" "red"  
#> 
#> [[3]]
#> [1] "orange" "red"

如果使用simplify = TRUE,str_extract_all()将返回一个矩阵,其中的短匹配自动扩展到与最长匹配的长度相同:

str_extract_all(more, colour_match, simplify = TRUE)
#>      [,1]     [,2] 
#> [1,] "blue"   "red"
#> [2,] "green"  "red"
#> [3,] "orange" "red"

x <- c("a", "a b", "a b c")
str_extract_all(x, "[a-z]", simplify = TRUE)
#>      [,1] [,2] [,3]
#> [1,] "a"  ""   ""  
#> [2,] "a"  "b"  ""  
#> [3,] "a"  "b"  "c"

4.3 分组匹配

我们讨论了使用括号来澄清优先级和匹配时的反向引用。你还可以使用括号提取复杂匹配的部分。例如,假设我们想从句子中提取名词。我们将查找“a”或“the”之后的任何单词。在正则表达式中定义“单词”有点棘手,所以这里我使用一个简单的近似值:至少一个不是空格的字符的序列。

noun <- "(a|the) ([^ ]+)"

has_noun <- sentences %>%
  str_subset(noun) %>%
  head(10)
has_noun %>% 
  str_extract(noun)
#>  [1] "the smooth" "the sheet"  "the depth"  "a chicken"  "the parked"
#>  [6] "the sun"    "the huge"   "the ball"   "the woman"  "a helps"

str_extract()给我们完整的匹配;str_match()给出每个单独的匹配。它返回一个矩阵,而不是字符向量,其中一列用于完整匹配,然后列对应匹配的组:

has_noun %>% 
  str_match(noun)
#>       [,1]         [,2]  [,3]     
#>  [1,] "the smooth" "the" "smooth" 
#>  [2,] "the sheet"  "the" "sheet"  
#>  [3,] "the depth"  "the" "depth"  
#>  [4,] "a chicken"  "a"   "chicken"
#>  [5,] "the parked" "the" "parked" 
#>  [6,] "the sun"    "the" "sun"    
#>  [7,] "the huge"   "the" "huge"   
#>  [8,] "the ball"   "the" "ball"   
#>  [9,] "the woman"  "the" "woman"  
#> [10,] "a helps"    "a"   "helps"

如果您的数据在 tibble 中,则使用[tidyr::extract()](https://tidyr.tidyverse.org/reference/extract.html)。它的工作原理类似于str_match()但需要您命名匹配项,然后将其放置在新列中:

tibble(sentence = sentences) %>% 
  tidyr::extract(
    sentence, c("article", "noun"), "(a|the) ([^ ]+)", 
    remove = FALSE
  )
#> # A tibble: 720 x 3
#>   sentence                                    article noun   
#>   <chr>                                       <chr>   <chr>  
#> 1 The birch canoe slid on the smooth planks.  the     smooth 
#> 2 Glue the sheet to the dark blue background. the     sheet  
#> 3 It's easy to tell the depth of a well.      the     depth  
#> 4 These days a chicken leg is a rare dish.    a       chicken
#> 5 Rice is often served in round bowls.        <NA>    <NA>   
#> 6 The juice of lemons makes fine punch.       <NA>    <NA>   
#> # … with 714 more rows

就像str_extract(),如果您想要每个字符串的所有匹配项,则需要str_match_all().

4.4 匹配替换

str_replace()str_replace_all()允许您用新字符串替换匹配项。最简单的用法是用固定字符串替换模式:

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_replace_all()可以通过提供一个向量名执行多个替换:

x <- c("1 house", "2 cars", "3 people")
str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))
#> [1] "one house"    "two cars"     "three people"

您可以使用反向引用来引用匹配的组,而不是用固定字符串替换。在下面的代码中,我翻转了第二个和第三个单词的顺序。

sentences %>% 
  str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>% 
  head(5)
#> [1] "The canoe birch slid on the smooth planks." 
#> [2] "Glue sheet the to the dark blue background."
#> [3] "It's to easy tell the depth of a well."     
#> [4] "These a days chicken leg is a rare dish."   
#> [5] "Rice often is served in round bowls."

4.5 分割

使用str_split()将一个字符串分解成多个。例如,我们可以将句子拆分为单词:

sentences %>%
  head(5) %>% 
  str_split(" ")
#> [[1]]
#> [1] "The"     "birch"   "canoe"   "slid"    "on"      "the"     "smooth" 
#> [8] "planks."
#> 
#> [[2]]
#> [1] "Glue"        "the"         "sheet"       "to"          "the"        
#> [6] "dark"        "blue"        "background."
#> 
#> [[3]]
#> [1] "It's"  "easy"  "to"    "tell"  "the"   "depth" "of"    "a"     "well."
#> 
#> [[4]]
#> [1] "These"   "days"    "a"       "chicken" "leg"     "is"      "a"      
#> [8] "rare"    "dish."  
#> 
#> [[5]]
#> [1] "Rice"   "is"     "often"  "served" "in"     "round"  "bowls."

因为每个组可能包含不同数量元素,所以这将返回一个列表。如果您使用的是长度为 1 的向量,最简单的方法就是提取列表的第一个元素:

"a|b|c|d" %>% 
  str_split("\\|") %>% 
  .[[1]]
#> [1] "a" "b" "c" "d"

您可以使用simplify = TRUE返回一个矩阵:

sentences %>%
  head(5) %>% 
  str_split(" ", simplify = TRUE)
#>      [,1]    [,2]    [,3]    [,4]      [,5]  [,6]    [,7]     [,8]         
#> [1,] "The"   "birch" "canoe" "slid"    "on"  "the"   "smooth" "planks."    
#> [2,] "Glue"  "the"   "sheet" "to"      "the" "dark"  "blue"   "background."
#> [3,] "It's"  "easy"  "to"    "tell"    "the" "depth" "of"     "a"          
#> [4,] "These" "days"  "a"     "chicken" "leg" "is"    "a"      "rare"       
#> [5,] "Rice"  "is"    "often" "served"  "in"  "round" "bowls." ""           
#>      [,9]   
#> [1,] ""     
#> [2,] ""     
#> [3,] "well."
#> [4,] "dish."
#> [5,] ""

您还可以请求最大件数:

fields <- c("Name: Hadley", "Country: NZ", "Age: 35")
fields %>% str_split(": ", n = 2, simplify = TRUE)
#>      [,1]      [,2]    
#> [1,] "Name"    "Hadley"
#> [2,] "Country" "NZ"    
#> [3,] "Age"     "35"

除了按模式拆分字符串,boundary()还可以按字符、行、句子和单词拆分:

x <- "This is a sentence.  This is another sentence."
str_view_all(x, boundary("word"))
image.png

str_split(x, " ")[[1]]
#> [1] "This"      "is"        "a"         "sentence." ""          "This"     
#> [7] "is"        "another"   "sentence."
str_split(x, boundary("word"))[[1]]
#> [1] "This"     "is"       "a"        "sentence" "This"     "is"       "another" 
#> [8] "sentence"

4.6 匹配项定位

str_locate()str_locate_all()提供每次匹配的开始和结束位置。当其他函数都不能满足我们的需求时,可以使用str_locate()查找匹配的模式,str_sub()提取或修改它们。

5 其他类型的匹配

当您使用字符串匹配时,调用中regex()

# The regular call:
str_view(fruit, "nana")
# Is shorthand for
str_view(fruit, regex("nana"))

您可以使用regex() 的其他参数来控制匹配的详细信息:

  • ignore_case = TRUE允许字符匹配它们的大写或小写形式。这始终使用当前语言环境。

    bananas <- c("banana", "Banana", "BANANA")
    str_view(bananas, "banana")
    
image.png
str_view(bananas, regex("banana", ignore_case = TRUE))
image.png
  • multiline = TRUE允许^$匹配每一行的开始和结束,而不是整个字符串的开始和结束。

    x <- "Line 1\nLine 2\nLine 3"
    str_extract_all(x, "^Line")[[1]]
    #> [1] "Line"
    str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]
    #> [1] "Line" "Line" "Line"
    
  • comments = TRUE允许您使用注释和空格使复杂的正则表达式更易于理解。空格将被忽略, #之后的所有内容也是如此。要匹配数字,您需要对其进行转义: "\\ "

    phone <- regex("
      \\(?     # optional opening parens
      (\\d{3}) # area code
      [) -]?   # optional closing parens, space, or dash
      (\\d{3}) # another three numbers
      [ -]?    # optional space or dash
      (\\d{3}) # three more numbers
      ", comments = TRUE)
    
    str_match("514-791-8141", phone)
    #>      [,1]          [,2]  [,3]  [,4] 
    #> [1,] "514-791-814" "514" "791" "814"
    
  • dotall = TRUE允许.匹配所有内容,包括\n.

  • fixed(): 完全匹配指定的字节序列。它忽略所有特殊的正则表达式并在非常低的级别上运行。这使您可以避免复杂的转义,并且可以比正则表达式快得多。以下通过microbenchmark测试表明,对于一个简单示例,它的速度大约快了 3 倍。

    microbenchmark::microbenchmark(
      fixed = str_detect(sentences, fixed("the")),
      regex = str_detect(sentences, "the"),
      times = 20
    )
    #> Unit: microseconds
    #>   expr     min       lq     mean   median       uq     max neval
    #>  fixed 100.392 101.3465 118.7986 105.9055 108.8545 367.118    20
    #>  regex 346.595 349.1145 353.7308 350.2785 351.4135 403.057    20
    

    小心使用fixed()非英语数据。这是有问题的,因为通常有多种方式来表示同一个字符。例如,有两种方法可以定义“á”:作为单个字符或作为“a”加一个重音符号:

    a1 <- "\u00e1"
    a2 <- "a\u0301"
    c(a1, a2)
    #> [1] "á" "á"
    a1 == a2
    #> [1] FALSE
    

    它们的渲染方式相同,但由于定义不同, fixed()因此找不到匹配项。相反,您可以使用coll(), 以符合人类字符比较规则:

    str_detect(a1, fixed(a2))
    #> [1] FALSE
    str_detect(a1, coll(a2))
    #> [1] TRUE
    
  • coll(): 使用标准排序规则比较字符串。这对于进行不区分大小写的匹配很有用。请注意,coll()采用一个 locale参数来控制用于比较字符的规则。然而世界不同地区使用不同的规则!

    # That means you also need to be aware of the difference
    # when doing case insensitive matches:
    i <- c("I", "İ", "i", "ı")
    i
    #> [1] "I" "İ" "i" "ı"
    
    str_subset(i, coll("i", ignore_case = TRUE))
    #> [1] "I" "i"
    str_subset(i, coll("i", ignore_case = TRUE, locale = "tr"))
    #> [1] "İ" "i"
    

    fixed()regex()都有ignore_case参数,但他们不能选择的语言环境:他们总是使用默认的语言环境。使用以下代码查看

    stringi::stri_locale_info()
    #> $Language
    #> [1] "zh"
    
    #> $Country
    #> [1] "CN"
    
    #> $Variant
    #> [1] ""
    
    #> $Name
    #> [1] "zh_CN"
    

    coll()的缺点是速度;因为识别哪些字符相同的规则很复杂,coll()regex()相比fixed()相对较慢。

  • str_split()可以使用boundary()来匹配边界。还可以将其与其他函数一起使用:

    x <- "This is a sentence."
    str_view_all(x, boundary("word"))
    
image.png
```
str_extract_all(x, boundary("word"))
#> [[1]]
#> [1] "This"     "is"       "a"        "sentence"
```

6 正则表达式的其他应用

Base R 中有两个有用的函数也使用正则表达式:

  • apropos()搜索全局环境中所有可用的对象。在你记不清楚函数的名称时很有用。

    apropos("replace")
    #> [1] "%+replace%"       "replace"          "replace_na"       "setReplaceMethod"
    #> [5] "str_replace"      "str_replace_all"  "str_replace_na"   "theme_replace"
    
  • dir()列出目录中的所有文件。该pattern参数采用正则表达式,仅返回与模式匹配的文件名。例如,您可以使用以下命令查找当前目录中的所有 R Markdown 文件:

    head(dir(pattern = "\\.Rmd$"))
    #> [1] "communicate-plots.Rmd" "communicate.Rmd"       "datetimes.Rmd"        
    #> [4] "EDA.Rmd"               "explore.Rmd"           "factors.Rmd"
    

7 stringi

stringr 建立在stringi包之上。stringr 在您学习时很有用,因为它公开了一组最少的函数,这些函数是经过精心挑选来处理最常见的字符串操作函数的。stringi 的设计是全面的。它几乎包含您可能需要的所有函数:stringi 有 250 个函数,stringr 有 49 个。

如果您发现自己很难在 stringr 中做某事,那么可以看一看 stringi。这两个包的工作方式非常相似。主要区别在于前缀:str_vs stri_

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容