写在前面
本系列推文为《R for Data Science (2)》的中文翻译版本。所有内容都通过开源免费的方式上传至Github,欢迎大家参与贡献,详细信息见:
Books-zh-cn 项目介绍:
Books-zh-cn:开源免费的中文书籍社区
r4ds-zh-cn Github 地址:
https://github.com/Books-zh-cn/r4ds-zh-cn
r4ds-zh-cn 网站地址:
https://books-zh-cn.github.io/r4ds-zh-cn/
目录
-
14.1 介绍
-
14.2 创建字符串
-
14.3 从数据创建许多字符串
-
14.4 从字符串中提取数据
14.1 介绍
到目前为止,您已使用过大量字符串却未深入了解其细节。 现在我们将深入探索字符串的本质特性,并掌握一些强大的字符串处理工具。
首先我们将详细介绍创建字符串和字符向量的方法。 接着学习如何从数据中生成字符串,以及反过来如何从数据中提取字符串。 随后我们将探讨处理单个字母的工具。 本章最后将介绍针对单个字母操作的函数,并简要讨论英语使用习惯在处理其他语言时可能导致的认知偏差。
下一章我们将继续研究字符串,届时您将深入了解正则表达式的强大功能。
14.1.1 先决条件
本章我们将使用 stringr 包中的函数,该包是核心 tidyverse 的组成部分。 我们还会使用 babynames 数据集,因为它提供了一些有趣的字符串供我们操作。
library(tidyverse)
library(babynames)
您可以快速识别何时在使用 stringr 函数,因为所有 stringr 函数都以 str_ 开头。 如果您使用 RStudio,这个特性尤其有用,因为输入 str_ 会触发自动补全功能,帮助您回忆可用的函数。

14.2 创建字符串
本书前面曾顺带创建过字符串但未讨论细节。 首先可以使用单引号(')或双引号(")创建字符串。 两者行为并无差异,根据一致性原则,tidyverse style guide 建议使用 ",除非字符串本身包含多个 "。
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 IN A STRING
若遇到这种情况且无法判断该闭合哪个引号,请按 Esc 键取消操作后重试。
14.2.1 转义
要在字符串中包含原义的单引号或双引号,可使用反斜杠 \ 进行转义:
double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'"
因此若需要在字符串中包含原义的反斜杠,则必须对其进行转义:"\\":
backslash <- "\\"
请注意字符串的打印显示结果与其实际内容不同,因为打印显示会呈现转义符(换句话说,当打印字符串时,你可以复制粘贴输出内容来重建该字符串)。 To see the raw contents of the string, use str_view(): 要查看字符串的原始内容,请使用 str_view():
x <- c(single_quote, double_quote, backslash)
x
#> [1] "'" "\"" "\\"
str_view(x)
#> [1] │ '
#> [2] │ "
#> [3] │ \
14.2.2 原始字符串
创建包含多个引号或反斜杠的字符串会很快变得令人困惑。 为了说明这个问题,我们创建一个包含定义 double_quote 和 single_quote 变量代码块内容的字符串:
tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#> │ single_quote <- '\'' # or "'"
这里出现了大量反斜杠! (这种情况有时被称为 leaning toothpick syndrome。)要避免转义,可以使用原始字符串:
tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#> │ single_quote <- '\'' # or "'"
原始字符串通常以 r"( 开头,以 )" 结尾。 但如果字符串包含 )",则可以改用 r"[]" 或 r"{}",若仍不足以处理,还可以插入任意数量的破折号使起始和结束标记唯一,例如 r"--()--"``、r"---()---"`` 等。原始字符串的灵活性足以处理任何文本。
14.2.3 其他特殊字符
除了 \"、\' 和 \\ 之外,还有一些其他可能用到的特殊字符。 最常见的是\n(换行符)和\t(制表符)。 有时你还会看到以\u或\U开头的Unicode转义字符串。 这是一种可在所有系统中编写非英文字符的方法。 你可以在?Quotes中查看其他特殊字符的完整列表。
x <- c("one\ntwo", "one\ttwo", "\u00b5", "\U0001f604")
x
#> [1] "one\ntwo" "one\ttwo" "µ" "😄"
str_view(x)
#> [1] │ one
#> │ two
#> [2] │ one{\t}two
#> [3] │ µ
#> [4] │ 😄
请注意,str_view() 使用蓝色背景来显示制表符以便更容易识别。 处理文本时的挑战之一在于空白字符可能存在多种表现形式,这种背景色有助于您察觉异常情况。
14.2.4 练习
-
创建包含以下值的字符串:
-
He said "That's amazing!" -
\a\b\c\d -
\\\\\\
-
-
在你的 R 会话中创建该字符串并打印。 特殊字符 "\u00a0" 会如何显示?
str_view()会如何呈现它? 你能通过搜索查到这个特殊字符的含义吗?x <- "This\u00a0is\u00a0tricky"
14.3 从数据创建许多字符串
现在你已学会手动创建字符串的基础知识,接下来我们将详细探讨如何基于其他字符串生成新字符串。 这将帮助你解决一个常见问题:如何将自编文本与数据框中的字符串组合。 例如,你可以将 "Hello" 与 name 变量结合来创建问候语。 我们将演示如何使用 str_c() 和 str_glue() 实现这一功能,以及如何将它们 mutate() 结合使用。 这自然引出了关于哪些字符串函数可与 summarize() 配合使用的问题,因此本节最后将讨论字符串的汇总函数 str_flatten()。
14.3.1 str_c()
str_c() 可接受任意数量的向量作为参数,并返回一个字符向量:
str_c("x", "y")
#> [1] "xy"
str_c("x", "y", "z")
#> [1] "xyz"
str_c("Hello ", c("John", "Susan"))
#> [1] "Hello John" "Hello Susan"
str_c() 与基础的 paste0() 非常相似,但遵循 tidyverse 的循环和缺失值传递规则,专为与 mutate() 配合使用而设计:
df <- tibble(name = c("Flora", "David", "Terra", NA))
df |> mutate(greeting = str_c("Hi ", name, "!"))
#> # A tibble: 4 × 2
#> name greeting
#> <chr> <chr>
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA> <NA>
若希望以其他方式显示缺失值,可使用 coalesce() 进行替换。根 据具体需求,可选择在 str_c() 内部或外部使用:
df |>
mutate(
greeting1 = str_c("Hi ", coalesce(name, "you"), "!"),
greeting2 = coalesce(str_c("Hi ", name, "!"), "Hi!")
)
#> # A tibble: 4 × 3
#> name greeting1 greeting2
#> <chr> <chr> <chr>
#> 1 Flora Hi Flora! Hi Flora!
#> 2 David Hi David! Hi David!
#> 3 Terra Hi Terra! Hi Terra!
#> 4 <NA> Hi you! Hi!
14.3.2 str_glue()
如果你使用 str_c() 混合许多固定字符串和变量字符串,会发现需要输入大量 ",导致代码整体目标难以看清。 glue package 通过str_glue()提供了另一种解决方案:只需输入一个具有特殊功能的字符串------任何在 {} 内的内容都会像在引号外一样被解析执行:
df |> mutate(greeting = str_glue("Hi {name}!"))
#> # A tibble: 4 × 2
#> name greeting
#> <chr> <glue>
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA> Hi NA!
如你所见,str_glue() 目前会将缺失值转换为 "NA" 字符串,这不幸地导致了与str_c() 的不一致。
你可能也好奇如果需要在字符串中包含常规的{或}时会发生什么。 如果你猜测需要以某种方式转义它们,那么你的思路是正确的。 技巧在于glue采用了略微不同的转义技术:不是使用像\这样的特殊字符作为前缀,而是将特殊字符加倍:
df |> mutate(greeting = str_glue("{{Hi {name}!}}"))
#> # A tibble: 4 × 2
#> name greeting
#> <chr> <glue>
#> 1 Flora {Hi Flora!}
#> 2 David {Hi David!}
#> 3 Terra {Hi Terra!}
#> 4 <NA> {Hi NA!}
14.3.3 str_flatten()
str_c() 和 str_glue() 与 mutate() 配合良好,因为它们的输出长度与输入相同。 如果你需要适用于 summarize() 的函数,即总是返回单个字符串? 这就是str_flatten()的功能:它接收字符向量并将向量的每个元素组合成单个字符串:
str_flatten(c("x", "y", "z"))
#> [1] "xyz"
str_flatten(c("x", "y", "z"), ", ")
#> [1] "x, y, z"
str_flatten(c("x", "y", "z"), ", ", last = ", and ")
#> [1] "x, y, and z"
这使得它能与 summarize() 良好配合:
df <- tribble(
~ name, ~ fruit,
"Carmen", "banana",
"Carmen", "apple",
"Marvin", "nectarine",
"Terence", "cantaloupe",
"Terence", "papaya",
"Terence", "mandarin"
)
df |>
group_by(name) |>
summarize(fruits = str_flatten(fruit, ", "))
#> # A tibble: 3 × 2
#> name fruits
#> <chr> <chr>
#> 1 Carmen banana, apple
#> 2 Marvin nectarine
#> 3 Terence cantaloupe, papaya, mandarin
14.3.4 练习
-
比较以下输入中
paste0()与str_c()的结果差异:str_c("hi ", NA) str_c(letters[1:2], letters[1:3]) -
paste()和paste0()有什么区别? 如何用str_c()实现与paste()等效的功能? -
将下列表达式在
str_c()和str_glue()之间进行转换:a.
str_c("The price of ", food, " is ", price)b.
str_glue("I'm {age} years old and live in {country}")c.
str_c("\\section{", title, "}")
14.4 从字符串中提取数据
将多个变量压缩到单个字符串中的情况十分常见。 在本节中,您将学习使用四个 tidyr 函数来提取这些变量:
-
df |> separate_longer_delim(col, delim) -
df |> separate_longer_position(col, width) -
df |> separate_wider_delim(col, delim, names) -
df |> separate_wider_position(col, widths)
仔细观察可以发现这里存在通用模式:separate_,接着 longer 或 wider,然后 _,最后是 delim 或 position。 这是因为这四个函数由两个更简单的基础操作组合而成:
-
与
pivot_longer()和pivot_wider()类似,_longer函数通过创建新行使输入数据框变长,而_wider函数通过生成新列使输入数据框变宽。 -
delim使用分隔符(如", "或" ")拆分字符串;position按指定宽度进行分割,如c(3, 5, 2)。
我们将在 Chapter 15 讨论该函数家族的最后一个成员 separate_wider_regex()。 它是 wider 函数中最灵活的,但需要先了解正则表达式才能使用。
接下来两节将介绍这些 separate 函数的基本原理,先讲解拆分为行(相对简单),再讲解拆分为列。 最后我们将讨论 wider 函数提供的诊断问题工具。
14.4.1 分成行
当每行的组成部分数量不一致时,将字符串拆分为行往往最为实用。 最常见的情况是需要使用 separate_longer_delim() 根据分隔符进行拆分:
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
在实际应用中 separate_longer_position() 较为少见,但一些旧数据集确实会采用非常紧凑的格式,每个字符都被用来记录一个值:
df2 <- tibble(x = c("1211", "131", "21"))
df2 |>
separate_longer_position(x, width = 1)
#> # A tibble: 9 × 1
#> x
#> <chr>
#> 1 1
#> 2 2
#> 3 1
#> 4 1
#> 5 1
#> 6 3
#> # ℹ 3 more rows
14.4.2 分成列
当每个字符串包含固定数量的组成部分且需要将其展开为列时,将字符串拆分为列最为实用。 这比对应的 longer 函数稍复杂些,因为需要为列命名。 例如在以下数据集中,x 由代码、版本号和年份组成,以"."分隔。 使用 separate_wider_delim() 时,我们需要在两个参数中分别指定分隔符和列名:
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
如果某个特定片段不需要,可以使用 NA 名称将其从结果中省略:
df3 |>
separate_wider_delim(
x,
delim = ".",
names = c("code", NA, "year")
)
#> # A tibble: 3 × 2
#> code year
#> <chr> <chr>
#> 1 a10 2022
#> 2 b10 2011
#> 3 e15 2015
separate_wider_position() 的工作方式略有不同,因为通常需要指定每列的宽度。 因此需要提供一个命名的整数向量,其中名称表示新列的名称,值表示该列占据的字符数。 通过不命名某些值可以将其从输出中省略:
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
--------------- 未完待续 ---------------
本期翻译贡献:
@TigerZ生信宝库
注:本文已开启快捷转载,欢迎大家转载,只需标明文章出处即可。