字符串是引号之间的一段文本。 目前所有仍受支持的 R 版本都用 UTF-8 编码字符串,因此世界上大多数语言都能得到支持。
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(),但这需要一些特定的函数,我们会在下面讨论。
在 base R 中,大多数事情都能做到,但很多程序员觉得它对字符串操作的支持相当有限,而且让人困惑。
幸运的是,tidyverse的包要直观得多,也更灵活、更自洽。
仍有大量遗留代码在使用旧式字符串函数。 Exercism 上大多数社区解答也是如此。
此外,对于小任务来说,使用内置函数比导入一个庞大的库更实用。 R 赛道的维护者在编写用于验证练习的范例解答时,往往也会采用这种做法。
因此,了解一些旧方法的用法仍然是有价值的。
nchar返回字符串中字符的个数。
nchar("string")
#> [1] 6
nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7
“字符”究竟指什么,这个话题(很复杂!)超出了本文的范围。 只需留意上面例子中的几点:
\0结尾(否则长度会多 1)。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函数(以及若干更晚出现的语言:Wikipedia 列出了大约 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"
客气地说,旧式字符串函数并不是 R 能提供的最好的东西(也有不那么客气的看法)。
为了改善这种情况,stringr库从 2009 年起就在持续开发,如今已是 Tidyverse 集合中的重要组成部分。
用library(stringr)或library(tidyverse)把它导入你的命名空间。
一些关键功能包括:
str_前缀,并力求让用途一目了然。和所有 Tidyverse 包一样,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_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 带来了简单的字符串插值。
大括号{ }里的表达式会被求值,并替换为其文本表示。
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
R 的一个非常常见的用途,是把杂乱的数据整理成一致的格式以便分析。 为了帮助做到这一点,有多种把文本拆分成子串的方法。 大多数都超出了当前的范围。
str_split_1是最简单的,它按照指定的模式(没有默认模式)把单个字符串拆分成由若干片段组成的向量。
s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R" "Julia" "Python"
有些语言(主要是欧洲语言)区分大写和小写字母。
有一组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"
在这个练习中,你将处理日志行。
每一行日志都是一个字符串,格式如下:"[<LEVEL>]: <MESSAGE>"。
日志级别一共有三种:
INFOWARNINGERROR你有三个任务,每个任务都会给出一行日志,要求你对它做点什么。
你可以自由选用 base R 中的旧式函数,也可以使用stringr库,两者都能给出简短可行的解法。
实现message函数,返回日志行中的消息:
message("[ERROR]: Invalid operation")
#> "Invalid operation"
开头和结尾的空白字符都应去掉:
message("[WARNING]: Disk almost full\r\n")
#> "Disk almost full"
实现log_level函数,返回日志行的日志级别,并且要用小写形式返回:
log_level("[ERROR]: Invalid operation")
#> "error"
实现reformat函数,重新格式化日志行:把消息放在前面,日志级别放在后面并加上括号:
reformat("[INFO]: Operation completed")
#> "Operation completed (info)"