字符串就是引号之间的一段文本。
mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"
遗憾的是,这些术语可能会让熟悉其他语言的程序员感到困惑。
尽管type是character,但 R 并不区分单个字母(在其他一些语言中称为char,但在 R 中_不是_)和很长的字符串。两者都是单个数据项,可以互换地放进单引号' '或双引号" "中。
风格指南建议使用双引号,除非你想在字符串内部使用双引号而不对它们进行转义。
str2 <- 'String with "quotes"' # convenience format
str2
#> [1] "String with \"quotes\"" # standard format
因此,character vector就是由字符串组成的向量,其中每个字符串都是单个元素。
s <- c("some", "strings")
s
#> [1] "some" "strings"
length(s)
#> [1] 2
length(s[1]) # the number of vector elements, not the string length
#> [1] 1
nchar(s[1]) # the number of characters in the string "some"
#> [1] 4
在上面的例子中,length(s[1])等价于length("some"):它给出的是_向量元素_的个数,而不是_字符串长度_。
操作单个字符串显然是可行的,就像上面例子中的nchar()那样,但这需要一些专门的函数,我们会在下面讨论。
正如上面看到的,有些标点字符需要加上反斜杠\,也就是进行“转义”。
对大多数有经验的程序员来说,这个列表不会让他们感到意外,因为这一主题在各种语言中已经相当标准化了。
除了引号(如上所示),这个列表还包括换行符\n、制表符\t、反斜杠\\,以及以\U或\u开头的 Unicode 符号。
为了避免使用大量转义字符,可以在r"( )"中定义原始字符串:
r"(raw "string")"
#> [1] "raw \"string\""
对于包含圆括号( )的原始字符串,还有许多其他的语法选项。
目前所有受支持的 R 版本都用 UTF-8 对字符串进行编码,因此可以支持世界上大多数语言。
"आधुनिक मानक हिन्दी"
#> [1] "आधुनिक मानक हिन्दी"
此外,许多 R 函数会感知locale,并尽量遵循本地的约定来进行字符串排序、大小写转换、日期和时间处理等。
本教学大纲主要会以美式英语为例:一方面因为它是常见的默认设置,另一方面也因为大纲作者恰好居住在美国。
区域设置是一个复杂的主题,所以请针对你自己的情况查找相关文档。
在 base R 中,大多数事情都是_可以做到_的,但许多程序员觉得它对字符串操作的支持相当有限,而且让人困惑。
幸运的是,tidyverse系列中有一些包要直观得多、灵活得多,而且前后一致。
现在仍有很多遗留代码在使用旧式的字符串函数。Exercism 上的大多数社区解答也是如此。
此外,对于小任务来说,使用内置函数而不是导入一个庞大的库可能更合适。R 赛道的维护者在编写示例解答、验证我们 GitHub 仓库 上的练习时,往往就采用这种做法。
因此,了解一些旧的做法仍然值得。
下面介绍的函数都以最简单的形式给出,但大多数还有可选的实参。详情请参阅文档。
nchar返回字符串中的字符数。
nchar("string")
#> [1] 6
nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7
“字符”到底指什么,这个(_相当复杂!_的)话题超出了本文的范围。只需注意上面例子中的两点:
\0结尾(否则长度会多出 1)。传给nchar的内容要小心。对于非character的值,结果仍然可能难以预测,不过在较新的 R 版本中这一点已经有所改善。
grep在字符串向量中搜索模式,返回匹配处的下标。
grep("Jav", c("Java", "Python", "Javascript"))
#> [1] 1 3
paste和paste0
把任意数量的字符串合并成一个字符串。
可以为paste指定一个可选的分隔符:默认是空格。
paste0(v)等价于paste(v, sep = "")。
paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintf为了更精细地控制字符串的组装,R 从 C 那里照搬了sprintf函数(后来的一些语言也照搬了:维基百科列出了大约 30 种)。
r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimws可以用trimws()去掉开头和(或)结尾的空白字符:
s <- " messy string "
trimws(s)
#> [1] "messy string"
还有一个which = "left"(或 "right")形参,可以只去掉一端。
substr作用于单个字符串,返回两个(含端点)位置之间的子字符串。
substr("Exercism", 5, 7)
#> [1] "cis"
strsplit根据一个分隔符字符串,把一个字符串拆分成 R 数组。 分隔符是必需的(没有默认值),但可以是空字符串。
strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R" "Python" "Julia"
strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
数组会在单独的概念中介绍。如果你还没有学到教学大纲的那一部分,那么现在只需要知道,unlist()函数(在这个例子里)会把数组转换成向量。
unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
regexpr和regexpr
这些函数的名字是“regular expression”(正则表达式)的缩写,它们在字符串中搜索模式出现的位置,返回一个(可能)有用的数据数组。提到它们只是为了完整:还有更好的选择!
regexpr只返回第一次出现的位置,gregexpr返回所有出现的位置(“g”表示“全局”搜索)。
顾名思义,模式可以是一个正则表达式,我们会在后面的概念中介绍。
为什么这些旧的字符串函数看起来像 Unix shell 命令?
虽然 R 诞生于 1993 年,但它在设计上要大体向下兼容 S 语言。
S 语言是在贝尔实验室设计的,最早出现于 1976 年。贝尔实验室还创造了 Unix(始于 1969 年)和 C 语言(1972 年)。
那可真是来自更早时代的一支了不起的团队!我们只能猜测,他们彼此交流、互相交换了想法。
往好处想,我们可以说旧式的字符串函数并不是 R 能提供的最好的东西(也有不那么宽容的看法)。
为了改善这种状况,stringr库自 2009 年起就一直在开发中,如今已经是 Tidyverse 集合的重要组成部分。
用library(stringr)或library(tidyverse)把它导入你的命名空间。
一些主要特性包括:
str_前缀,并力求让用途一目了然。和所有 Tidyverse 包一样,stringr有完善的文档,通常由包的作者撰写。
stringr提供了几十个函数,它们往往有多种可选的实参。一开始这可能有点让人不知所措,所以文档就是你的好朋友。
想一想你想放进去什么(单个字符串?向量?还是更复杂的东西?),又想得到什么。
只要能找到正确的函数和正确的实参,你想要的任何事情大概都有简便的做法。
用管道|>连接函数的用法,会在教学大纲的其他部分更详细地讨论。
现在,知道以下几点会很有用:
下面的例子演示了这种语法。各个函数会在后面讨论。
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
str_length统计码点数量(类似于nchars)。
还有各种其他函数用来处理空白字符,包括str_trim()和str_squish()。在任何数据科学项目开始时,这往往是一项至关重要的清理工作。
# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R")) #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right") # add spaces if necessary
#> [1] "Julia " "Python " "R "
str_trim("Julia Python R ") # remove leading & trailing whitespace
#> [1] "Julia Python R"
str_squish("Julia Python R ") # trim, collapse multiple spaces
#> [1] "Julia Python R"
str_sub(string, start, end)函数会返回从start到end(含两端)的子字符串,而str_sub_all()可以作用于多个字符串。
用法非常灵活。start和end分别默认指向第一个和最后一个字符。它们可以是下标的向量。和向量索引不同,这里的负值会像 Python 那样从字符串末尾往回数,这一点很受欢迎。
s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab" "de" "fgh"
s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"
# str_sub_all takes vector input, gives list output
str_sub_all(c(s, "ijklmnop"), c(1, 4, 6), c(2, 5, -1))
[[1]]
#> [1] "ab" "de" "fgh"
[[2]]
#> [1] "ij" "lm" "nop"
str_flatten会把字符向量转换成单个字符串。collapse实参会被插入到每个元素之间,默认值是""。
c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"
str_c会把多个字符向量转换成一个字符向量,必要时进行循环补齐。
str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"
str_glue把简单的字符串插值带到了 R 中。大括号{ }里的表达式会被求值,并替换成它的文本表示。
如果提供了多个表达式字符串,它们也会像str_flatten那样拼接起来。字符串还可以选择去掉开头和结尾的空白字符。
r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631
尽管str_glue很灵活(也相当复杂),但在它底层的glue库中还有更丰富的可能性。
R 的一个非常常见的用途,是把杂乱的数据整理成分析所需的一致格式。为此,有几种把文本拆分成子字符串的方法。
str_split_1是最简单的,它把单个字符串按指定的模式拆分成由若干片段组成的向量(没有默认模式)。
s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R" "Julia" "Python"
str_split_i接受一个字符向量和一个下标n,返回由每个输入字符串的第n个子字符串组成的向量。
s2 <- c(s, "C Kotlin, F#") #> [1] "R Julia Python" "C Kotlin, F#"
str_split_i(s2, " ", 2) # 2nd element of each
#> [1] "Julia" "Kotlin,"
str_split接受一个字符向量,返回由向量组成的数组。我们的教学大纲里有一个数组概念,其中解释了它的输出。
str_split(s2, " ")
[[1]]
#> [1] "R" "Julia" "Python"
[[2]]
#> [1] "C" "Kotlin," "F#"
还有str_split_fixed,它返回一个矩阵。矩阵会在后面的概念中介绍。
要匹配整个字符串,str_equal()可以使用多种“相等”的定义。最简单的情况下,默认区分大小写,但可以修改。区域设置和 Unicode 会带来多得多的可能性。
str_equal("Odin", "odin")
#> [1] FALSE
str_equal("Odin", "odin", ignore_case = TRUE)
#> [1] TRUE
还有很多其他函数,我们会在正则表达式概念中更详细地讨论它们。
有些语言(主要是欧洲语言)区分大写字母和小写字母。stringr有一组函数可以按符合区域设置的方式在两者之间转换:
str_to_lower()str_to_upper()str_to_title():每个单词的首字母变成大写str_to_sentence():句子的首字母变成大写str_to_upper("elixir") # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά") # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"
str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"
若要根据模式匹配来替换子字符串,str_replace()会替换模式第一次出现的地方,str_replace_all()会替换所有出现的地方。
若要根据字符下标来替换子字符串,str_sub()还有一种赋值形式。
s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"
str_dup用来生成重复项,str_unique用来去除重复项。
默认情况下str_dup没有分隔符,但可以指定一个。
str_dup("xyz", 4, sep = ", ")
#> [1] "xyz, xyz, xyz, xyz"
str_unique是unique()的扩展。两者都会从向量中去除重复的条目,但stringr的函数还能处理大小写敏感以及各种与区域设置相关的匹配。
inp <- c("some", "strings", "Some", "Strings")
str_unique(inp)
#> [1] "some" "strings" "Some" "Strings"
str_unique(inp, ignore_case = TRUE)
#> [1] "some" "strings"