5.3 文本和字符串

文本和字符串是一个复杂的话题。十进制数字永远由0-9构成,并且一个数字永远代表一个值;逻辑值永远只有TRUE, FALSE,NA`;而文本的内容千变万化:它有字母,有数字,有标点符号,有空格空行;不同的语言有不同的书写和标点符号规则;同样的文字可能有不同格式的编码;同样的一个字可能有不同的含义……这使文本的处理比数值的处理丰富且复杂得多。本书仅介绍几个重要的概念,具体的细节请查看相关的文档。

5.3.1 stringrstringi

stringihttp://www.gagolewski.com/software/stringi/ )提供了一系列高效可靠,功能丰富的函数用于处理任何编码的任何语言的文本。stringrhttps://stringr.tidyverse.org )是stringi的简洁版,囊括了stringi中对于数据分析最常用的函数。stringrtidyverse的一部分,因此加载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()


  1. str_view()只会显示第一个匹配,str_view_all()会显示全部。后面讲到的所有以_all结尾的函数都有其对应的非all版本,只会作用于第一个匹配。