字符

字符串 属于 R

2 个练习

关于 字符串

背景

字符串就是引号之间的一段文本。

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系列中有一些包要直观得多、灵活得多,而且前后一致。

Base R

现在仍有很多遗留代码在使用旧式的字符串函数。Exercism 上的大多数社区解答也是如此。

此外,对于小任务来说,使用内置函数而不是导入一个庞大的库可能更合适。R 赛道的维护者在编写示例解答、验证我们 GitHub 仓库 上的练习时,往往就采用这种做法。

因此,了解一些旧的做法仍然值得。

下面介绍的函数都以最简单的形式给出,但大多数还有可选的实参。详情请参阅文档。

nchar

返回字符串中的字符数。

nchar("string")
#> [1] 6

nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7

“字符”到底指什么,这个(_相当复杂!_的)话题超出了本文的范围。只需注意上面例子中的两点:

  • 虽然 R 内部的字符串是用 C 实现的,但它们并不以\0结尾(否则长度会多出 1)。
  • 多字节的 Unicode 字符会按照你预期(或希望)的方式处理。“Hrōðgār”有 7 个字符(也叫“rune”或“码点”),但存储时需要 10 个字节。

传给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”表示“全局”搜索)。

顾名思义,模式可以是一个正则表达式,我们会在后面的概念中介绍。

Advanced

为什么这些旧的字符串函数看起来像 Unix shell 命令?

虽然 R 诞生于 1993 年,但它在设计上要大体向下兼容 S 语言。

S 语言是在贝尔实验室设计的,最早出现于 1976 年。贝尔实验室还创造了 Unix(始于 1969 年)和 C 语言(1972 年)。

那可真是来自更早时代的一支了不起的团队!我们只能猜测,他们彼此交流、互相交换了想法。

Tidyverse

往好处想,我们可以说旧式的字符串函数并不是 R 能提供的最好的东西(也有不那么宽容的看法)。

为了改善这种状况,stringr库自 2009 年起就一直在开发中,如今已经是 Tidyverse 集合的重要组成部分。

用library(stringr)或library(tidyverse)把它导入你的命名空间。

一些主要特性包括:

  • 所有函数名都带有str_前缀,并力求让用途一目了然。
  • 实参的顺序力求保持一致,以输入数据作为第一个实参。
  • 输入可以小到单个字符串,也可以大到复杂的数据结构。函数会尽量适应,并做出最合理的处理。

和所有 Tidyverse 包一样,stringr有完善的文档,通常由包的作者撰写。

stringr提供了几十个函数,它们往往有多种可选的实参。一开始这可能有点让人不知所措,所以文档就是你的好朋友。

想一想你想放进去什么(单个字符串?向量?还是更复杂的东西?),又想得到什么。

只要能找到正确的函数和正确的实参,你想要的任何事情大概都有简便的做法。

插叙:管道快速入门

用管道|>连接函数的用法,会在教学大纲的其他部分更详细地讨论。

现在,知道以下几点会很有用:

  • 管道左侧的结果会成为右侧函数的第一个实参(这里的“右侧”不考虑可选的换行)。
  • 其余的实参可以写在这个函数调用里。
  • 在 R 中,我们必须把管道接到函数的_调用_上,而不只是_名字_上,所以括号总是必需的(这一点与其他一些语言不同)。

下面的例子演示了这种语法。各个函数会在后面讨论。

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"
Advanced

stringr库建立在 stringi 库之上,后者提供更底层的函数。

如果你有上面还没讨论到的特定需求,那么:

  1. 查阅stringr的文档,它涵盖的选项比我们在这篇已经很长的概念文档里所能提到的还要多。
  2. 查阅stringi的文档,它描述了多得多的功能。
通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 字符串

练习已锁定

再解锁 1 个练习即可练习 字符串