5.3 文本和字符串
文本和字符串是一个复杂的话题。十进制数字永远由0-9构成,并且一个数字永远代表一个值;逻辑值永远只有TRUE
, FALSE,
NA`;而文本的内容千变万化:它有字母,有数字,有标点符号,有空格空行;不同的语言有不同的书写和标点符号规则;同样的文字可能有不同格式的编码;同样的一个字可能有不同的含义……这使文本的处理比数值的处理丰富且复杂得多。本书仅介绍几个重要的概念,具体的细节请查看相关的文档。
5.3.1 stringr
和stringi
包
stringi
(http://www.gagolewski.com/software/stringi/ )提供了一系列高效可靠,功能丰富的函数用于处理任何编码的任何语言的文本。stringr
(https://stringr.tidyverse.org )是stringi
的简洁版,囊括了stringi
中对于数据分析最常用的函数。stringr
是tidyverse
的一部分,因此加载tidyverse
的时候会自动加载stringr
.
stringr
中的函数名都是以str_
开头的,后面我们会看到很多例子。
stringr()
的cheatsheet(https://www.rstudio.com/resources/cheatsheets/#stringr )非常实用,尤其是第二页的正则表达。一定要把它背熟!
5.3.2 基础
字符串必须用单引号和双引号包围。在双引号包围的环境下,可以很容易打出英澳常用的单引号和欧洲语言中的“撇”;在单引号包围的环境下,可以很容易打出北美和中国常用的双引号。否则需要使用转义字符 (escape character), \
. 以下是几个正确的例子。
"'The unexamined life is not worth living' —Socrates"
#> [1] "'The unexamined life is not worth living' —Socrates"
"La science n'a pas de patrie."
#> [1] "La science n'a pas de patrie."
'"老子曰:“知不知,尚矣;不知知,病矣。"'
#> [1] "\"老子曰:“知不知,尚矣;不知知,病矣。\""
'l\'homme'
#> [1] "l'homme"
用反斜杠进行转义后,
x <- "Joe says \"Hi!\""
直接print()
并不能看出什么名堂:
x # 即`print(x)`
#> [1] "Joe says \"Hi!\""
我们要使用writeLines()
来查看所有需要转义的地方处理之后的结果:
writeLines(x)
#> Joe says "Hi!"
如果想表达一个字面意思的反斜杠\
,你需要输入"\\"
.
writeLines("\\")
#> \
\n
(newline)为换行符,\t
(tab)为制表符。
writeLines(c("Guten\n\n\tMorgen.", "Guten\n\n\tTag"))
#> Guten
#>
#> Morgen.
#> Guten
#>
#> Tag
所有的通过\
实现的符号请参见help("'")
(关于引号的帮助)。此外,请通过stringr()
的cheatsheet了解正则表达式中通过\
实现的更多patterns.
5.3.3 正则表达式
正则表达式 (regular expression, regex, RE)用于匹配符合特定规则/模式 (pattern)的字符组合。
stringr
提供了一个帮助理解学习正则表达式的函数,它会高亮所有的匹配字符,str_view_all()
28. 它的第一个参数是字符串,第二个是模式(即规则):
str_view_all(c("Plasmodium falciparum", "Schizosaccharomyces pombe"), "m")
这是最简单的一种匹配:直接用无特殊含义的字母/数字。下面这个例子是用\d
匹配字符串中所有的数字:
str_view_all("as9df2gh5jk7lo2", "\\d")
为什么要输入两个反斜杠呢?因为你要使用\d
作为一个regex,而你需要把它通过字符的形式去输入,因此d
前面那一个反斜杠需要通过另一个反斜杠去转义。当你实际上输入"\\d"
时,R才会把这串字符理解成\d
,即writeLines("\\d")
. 同理,如果你要匹配一个字面的反斜杠,你需要输入\\\\
:
x <- "This is a literal \\"
x
#> [1] "This is a literal \\"
writeLines(x)
#> This is a literal \
str_view_all(x, "\\\\")
5.3.3.1 细节
正则表达式的规则非常多,R关于正则表达式的官方文档在?rergex
,不过我认为RStudio的cheatsheet(和stringr
包一起的)更好用(https://www.rstudio.com/resources/cheatsheets/#stringr )。
虽然刚接触的时候正则表达式很令人头大,但是多背,多用,很快你就能把它们记下来,然后正则表达式将会成为你可靠而强大的助手。
此外还有一些我认为有用的帮助理解正则表达式的文章(不一定是R语言里的,但是很多内容都是共通的):
5.3.4 寻找匹配
str_detect(string, pattern)
: 是否存在匹配?
str_detect(c("abc", "cde", "bomb"), "b")
#> [1] TRUE FALSE TRUE
str_which(string, pattern)
: 每个元素各有几处匹配?
str_count(c("abc", "cde", "bomb", "bbb"), "b")
#> [1] 1 0 2 3
str_locate_all(string, pattern)
: 查找所有字符串中匹配的位置:
x <- c("Saccharomyces cerevisiae",
"Cannizzaro reaction",
"Meerwein-Ponndorf-Verley reduction",
"bbb",
"acd")
str_view_all(x, "(\\w)\\1") # 查找两个连续的字母
str_locate_all(x, "(\\w)\\1")
#> [[1]]
#> start end
#> [1,] 3 4
#>
#> [[2]]
#> start end
#> [1,] 3 4
#> [2,] 6 7
#>
#> [[3]]
#> start end
#> [1,] 2 3
#> [2,] 12 13
#>
#> [[4]]
#> start end
#> [1,] 1 2
#>
#> [[5]]
#> start end
5.3.5 子集和修改
5.3.5.1 str_sub()
:根据索引提取和修改子字符串
str_sub(string, start = 1, end = -1)
: 根据索引提取子字符串。
A <- c("Danio Rerio", "Xenopus Laevis")
str_sub(A, 1, 7) # 第1到第7个字符
#> [1] "Danio R" "Xenopus"
str_sub(A, 4, 4) # 第4个字符
#> [1] "i" "o"
str_sub(A, -4, -2) # 倒数第4至倒数第2
#> [1] "eri" "evi"
我们还可以通过索引修改某个位置的字符:
W <- "D. Rerio"
str_sub(W, 4, 4) <- str_to_lower(str_sub(W, 4, 4))
W
#> [1] "D. rerio"
5.3.5.2 str_subset()
:返回有匹配的字符串
str_subset(string, pattern)
:返回有匹配的字符串
str_subset(c("abc", "aaa", "bbb", "aca", "bbc", "asl"), "a")
#> [1] "abc" "aaa" "aca" "asl"
5.3.5.3 str_match_all()
和str_extract_all()
:提取匹配的子字符串
str_match_all(string, pattern)
: 第一列是匹配的整个子字符串,第二列和之后是每个正则表达圆括号组对应的子字符串。下面这个例子有两个圆括号组:
str_match_all(sentences[1:5], "(a|the) ([^ ]+)")
#> [[1]]
#> [,1] [,2] [,3]
#> [1,] "the smooth" "the" "smooth"
#>
#> [[2]]
#> [,1] [,2] [,3]
#> [1,] "the sheet" "the" "sheet"
#> [2,] "the dark" "the" "dark"
#>
#> [[3]]
#> [,1] [,2] [,3]
#> [1,] "the depth" "the" "depth"
#> [2,] "a well." "a" "well."
#>
#> [[4]]
#> [,1] [,2] [,3]
#> [1,] "a chicken" "a" "chicken"
#> [2,] "a rare" "a" "rare"
#>
#> [[5]]
#> [,1] [,2] [,3]
str_extract_all(string, pattern)
则只会返回整个子字符串。
str_extract_all(sentences[1:5], "(a|the) ([^ ]+)")
#> [[1]]
#> [1] "the smooth"
#>
#> [[2]]
#> [1] "the sheet" "the dark"
#>
#> [[3]]
#> [1] "the depth" "a well."
#>
#> [[4]]
#> [1] "a chicken" "a rare"
#>
#> [[5]]
#> character(0)
5.3.5.4 `str_replace_all():修改匹配的子字符串
str_replace_all(string, pattern, replacement)
: 通过正则表达修改子字符串。这里我们把粗心的种名首字母大写改正成小写,然后再简写属名:
A <- c("Danio Rerio", "Xenopus Laevis")
p1 <- "(?<=\\s)."
p2 <- "[:lower:]+(?=\\s)"
A %>% str_replace(p1, str_to_lower(str_extract(A, p1))) %>%
str_replace(p2, ".")
#> [1] "D. rerio" "X. laevis"
来一千个,一万个不标准的,长度不等的双名都不怕。
和str_to_lower()
相关的函数还有str_to_upper()
, str_to_title()
和str_to_sentence()
. 它们的作用都顾名思义。
5.3.6 合并和分解
5.3.6.1 str_c()
:字符串的合并
str_c(..., sep = "", collapse = NULL)
: 合并字符串:
str_c("a", "b", "c", sep = "")
#> [1] "abc"
参数sep
是被合并的字符串之间的连接字符;它可以是任何字符,包括空格和无(比如上面的例子;用sep = ""
表示无连接字符)。
当需要合并的字符串保存在一个向量里时,用collapse
而不是sep
:
str_c(c("a", "b", "c"), collapse = "[x@")
#> [1] "a[x@b[x@c"
str_c()
可以执行向量化运算:
str_c("prefix", c("a", "b", "c"), "suffix", sep = "-")
#> [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"
混沌在各地的称呼 <- str_c(
str_c(
"地区",
c("北京", "湖北", "巴蜀", "两广", "闽台"),
sep = ":"
),
str_c(
"称呼",
c("混沌", "包面", "抄手", "云吞", "扁食"),
sep = ":"
),
sep = " "
)
writeLines(混沌在各地的称呼)
#> 地区:北京 称呼:混沌
#> 地区:湖北 称呼:包面
#> 地区:巴蜀 称呼:抄手
#> 地区:两广 称呼:云吞
#> 地区:闽台 称呼:扁食
它还常与if
语句联用:
win <- 2
score <- str_c(
"张三",
if (win == 1) "赢\n" else "输\n",
"李四",
if (win == 2) "赢" else "输",
sep = ""
)
writeLines(score)
#> 张三输
#> 李四赢
5.3.6.2 str_split()
, str_split_fixed()
5.3.6.3 str_glue()
, str_glue_data()
str_view()
只会显示第一个匹配,str_view_all()
会显示全部。后面讲到的所有以_all
结尾的函数都有其对应的非all
版本,只会作用于第一个匹配。↩