R数据科学chapter10

#使用stringr处理字符串

#正则表达式(regular expression,regexp)

library(tidyverse)

library(stringr)

#字符串基础

#可以使用单引号或双引号来创建字符串。

#与其他语言不同,单引号和双引号在 R 中没有区别。

#我们推荐使用 ",除非你想要创建包含多个 " 的一个字符串

string1 <- "This is a string"

string2 <- 'To put a "quote" inside a string, use single quotes'

#如果忘记了结尾的引号,你会看到一个 +,这是一个续行符:

#如果遇到了这种情况,可以按 Esc 键,然后重新输入。

#如果想要在字符串中包含一个单引号或双引号,可以使用 \ 对其进行“转义”

double_quote <- "\"" # or '"'

single_quote <- '\'' # or "'"

#这意味着,如果想要在字符串中包含一个反斜杠,就需要使用两个反斜杠:\\。

#字符串的打印形式与其本身的内容不是相同的,因为打印形式中会显示出转义字符。

#如果想要查看字符串的初始内容,可以使用 writelines() 函数

x <- c("\"", "\\")

x

#> [1] "\"" "\\"

writeLines(x)

#> "

#> \

#最常用的是换行符 \n 和制表符 \t

x <- "\u00b5"

x

#> [1] "μ"

writeLines(x)

#多个字符串通常保存在一个字符向量中,你可以使用 c() 函数来创建字符向量

c("one", "two", "three")

#> [1] "one"  "two"  "three"

#字符串长度

#我们将使用 stringr 中的函数,这些函数的名称更直观,并且都是以 str_ 开头的。

#str_length() 函数可以返回字符串中的字符数量:

str_length(c("a", "R for data science", NA))

#字符串组合

#要想组合两个或更多字符串,可以使用 str_c() 函数:

str_c("x", "y")

#> [1] "xy"

str_c("x", "y", "z")

#> [1] "xyz"

#可以使用 sep 参数来控制字符串间的分隔方式:

str_c("x", "y", sep = ", ")

#> [1] "x, y"

#缺失值是可传染的。

#如果想要将它们输出为 "NA",可以使用 str_ replace_na():

x <- c("abc", NA)

str_c("|-", x, "-|")

#> [1] "|-abc-|" NA

str_c("|-", str_replace_na(x), "-|")

#> [1] "|-abc-|" "|-NA-|"

#str_c() 函数是向量化的,它可以自动循环短向量,

#使得其与最长的向量具有相同的长度:

str_c("prefix-", c("a", "b", "c"), "-suffix")

#长度为 0 的对象会被无声无息地丢弃。这与 if 结合起来特别有用:

name <- "Hadley"

time_of_day <- "morning"

birthday <- FALSE

str_c(

  "Good ", time_of_day, " ", name,

  if (birthday) " and HAPPY BIRTHDAY",

  "."

)

#要想将字符向量合并为字符串,可以使用 collapse() 函数:

str_c(c("x", "y", "z"), collapse = ", ")

#字符串取子集

# str_sub() 函数来提取字符串的一部分

#str_sub() 函数中还有 start 和 end 参数,

#它们给出了子串的位置(包括 start 和 end 在内)

x <- c("Apple", "Banana", "Pear")

str_sub(x, 1, 3)

# 负数表示从后往前数

str_sub(x, -3, -1)

#即使字符串过短,str_sub() 函数也不会出错,它将返回尽可能多的字符

str_sub("a", 1, 5)

#str_sub() 函数的赋值形式来修改字符串

str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))

x

#区域设置

# str_to_lower() 函数将文本转换为小写,

#你还可以使用 str_to_upper() 或 str_to_title() 函数。

#不同的语言有不同的转换规则。

#你可以通过明确区域设置来选择使用哪种规则

# 土耳其语中有带点和不带点的两个i,它们在转换为大写时是不同的:

str_to_upper(c("i", "ı"))

#> [1] "I" "I"

str_to_upper(c("i", "ı"), locale = "tr")

#> [1] "İ" "I"

#区域设置可以参考 ISO 639 语言编码标准,语言编码是 2 或 3 个字母的缩写。

#R 基础包中的 order() 和 sort() 函数使用当前区域设置对字符串进行排序。

#str_sort() 和 str_order() 函数可以使用 locale 参数来进行区域设置

#使用正则表达式进行模式匹配

#通过 str_view() 和 str_view_all() 函数来学习正则表达式

#这两个函数接受一个字符 向量和一个正则表达式,并显示出它们是如何匹配的。

#最简单的模式是精确匹配字符串:

x <- c("apple", "banana", "pear")

str_view(x, "an")

#另一个更复杂一些的模式是使用 .,它可以匹配任意字符(除了换行符)

str_view(x, ".a.")

#可以匹配任意字符,那么如何匹配字符 . 呢?

#你需要使用一个“转义”符号来告诉正则表达式实际上就是要匹配 . 这个字符,而不是使用 . 来匹配其他字符

#如果要匹配 .,那么你需要的正则表达式就是 \.

#因为我们使用字符串来表示正则表达式,而且 \ 在字符串中也用作转义字符,

#所以正则表达式 \. 的字符串形式应是 \\.

## 要想建立正则表示式,我们需要使用\\

dot <- "\\."

# 实际上表达式本身只包含一个\:

writeLines(dot)

#> \.

# 这个表达式告诉R搜索一个.

str_view(c("abc", "a.c", "bef"), "a\\.c")

#如果 \ 在正则表达式中用作转义字符,那么如何匹配 \ 这个字符呢?

#我们还是需要去除其特殊意义,建立形式为 \\ 的正则表达式。

#要想建立这样的正则表达式,我们需要使用一个 字符串,其中还需要对 \ 进行转义。

#这意味着要想匹配字符 \,我们需要输入 "\\\\"

#你需要 4 个反斜杠来匹配 1 个反斜杠!

x <- "a\\b"

writeLines(x)

str_view(x, "\\\\")

#本书将正则表达式写作 \.,将表示正则表达式的字符串写作 "\\."。

#锚点

#正则表达式会匹配字符串的任意部分。

#我们需要在正则表达式中设置锚点,以便 R 从字符串的开头或末尾进行匹配。

#• ^ 从字符串开头进行匹配。

#• $ 从字符串末尾进行匹配。

x <- c("apple", "banana", "pear")

str_view(x, "^a")

str_view(x, "a$")

#如果想要强制正则表达式匹配一个完整字符串,那么可以同时设置 ^ 和 $ 这两个锚点

x <- c("apple pie", "apple", "apple cake")

str_view(x, "apple")

str_view(x, "^apple$")

#字符类与字符选项

#很多特殊模式可以匹配多个字符。

#• \d 可以匹配任意数字。

#• \s 可以匹配任意空白字符(如空格、制表符和换行符)。

#• [abc] 可以匹配 a、b 或 c。

#• [^abc] 可以匹配除 a、b、c 外的任意字符。

#要想创建包含 \d 或 \s 的正则表达式,

#你需要在字符串中对 \ 进行转义,因此需要输入 "\\d" 或 "\\s"。

#使用字符选项创建多个可选的模式

#abc|d..f 可以匹配 abc 或 deaf

#因为 | 的优先级很低,所以 abc|xyz 匹配的是 abc 或 xyz,而不是 abcyz 或 abxyz。

#如果优先级让人感到困惑,那么可以使用括号让其表达得更清晰一些:

str_view(c("grey", "gray"), "gr(e|a)y")

#str_trim()去除字符串两边的空格,str_pad()在两边增加空格

#重复

#正则表达式的另一项强大功能是,其可以控制一个模式能够匹配多少次。

#这里的正则表达式指的的离它最近的那个字符的次数

#• ?:0次或1次。

#• +:1 次或多次。

#• *:0 次或多次。

x <- "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"

str_view(x, "CC?")

str_view(x, "CC+")

str_view(x, 'C[LX]+')

#• {n}:匹配 n 次。

#• {n,}:匹配 n 次或更多次。

#• {,m}:最多匹配 m 次。

#• {n, m}:匹配 n 到 m 次。

str_view(x, "C{2}")

str_view(x, "C{2,}")

str_view(x, "C{2,3}")

#分组与回溯引用

#括号还可以定义“分组”,

#你可以通过回溯引用(如 \1、\2 等)来引用这些分组

#以下的正则表达式可以找出名称中有重复的一对字母的所有水果

str_view(fruit, "(..)\\1", match = TRUE)

#• 确定与某种模式相匹配的字符串;

#匹配检测

#要想确定一个字符向量能否匹配一种模式,可以使用 str_detect() 函数。

#它返回一个与输入向量具有同样长度的逻辑向量:

x <- c("apple", "banana", "pear")

str_detect(x, "e")

#从数学意义上来说,逻辑向量中的 FALSE 为 0,TRUE 为 1。

#在匹配特别大的 向量时,sum() 和 mean() 函数能够发挥更大的作用

# 有多少个以t开头的常用单词?

sum(str_detect(words, "^t"))

#> [1] 65

# 以元音字母结尾的常用单词的比例是多少?

mean(str_detect(words, "[aeiou]$"))

#> [1] 0.277

#当逻辑条件非常复杂时(例如,匹配 a 或 b,但不匹配 c,除非 d 成立),

#一般来说,相对于创建单个正则表达式,

#使用逻辑运算符将多个 str_detect() 调用组合起来会更容易。

#以下两种方法均可找出不包含元音字母的所有单词:

# 找出至少包含一个元音字母的所有单词,然后取反

no_vowels_1 <- !str_detect(words, "[aeiou]")

## 找出仅包含辅音字母(非元音字母)的所有单词

no_vowels_2 <- str_detect(words, "^[^aeiou]+$")

identical(no_vowels_1, no_vowels_2)

#两种方法的结果是一样的,但我们认为第一种方法明显更容易理解。

#如果正则表达式过于复杂,则应该将其分解为几个更小的子表达式,

#将每个子表达式的匹配结果赋给一个变量,并使用逻辑运算组合起来。

#str_detect() 函数的一种常见用法是选取出匹配某种模式的元素。

#你可以通过逻辑取子集方式来完成这种操作,

words[str_detect(words, "x$")]

#也可以使用便捷的 str_subset() 包装器函数:

str_subset(words, "x$")

#然而,字符串通常会是数据框的一列,此时我们可以使用 filter 操作:

df <- tibble(

  word = words,

  i = seq_along(word)

)

df %>%

  filter(str_detect(words, "x$"))

#str_detect() 函数的一种变体是 str_count(),

#后者不是简单地返回是或否,而是返回字符串中匹配的数量:

x <- c("apple", "banana", "pear")

str_count(x, "a")

## 平均来看,每个单词中有多少个元音字母?

mean(str_count(words, "[aeiou]"))

#str_count() 也完全可以同 mutate() 函数一同使用:

df %>%

  mutate(

    vowels = str_count(word, "[aeiou]"),

    consonants = str_count(word, "[^aeiou]")

  )

#注意,匹配从来不会重叠。

#例如,在 "abababa" 中,模式 "aba" 会匹配多少次?

#正则表达式会告诉你是 2 次,而不是 3 次:

str_count("abababa", "aba")

#注意 str_view_all() 函数的使用。

#你很快就会知道,很多 stringr 函数都是成对出现的:

#一个函数用于单个匹配,另一个函数用于全部匹配,后者会有后缀 _all。

str_view_all("abababa", "aba")

#规律:str_view一个字符串只匹配一次,str_view_all匹配多次,但二者都不匹配重叠

#提取匹配内容

#要想提取匹配的实际文本,我们可以使用 str_extract() 函数。

length(sentences)

#假设我们想要找出包含一种颜色的所有句子。

#首先,我们需要创建一个颜色名称向量,然后将其转换成一个正则表达式

colors <- c(

  "red", "orange", "yellow", "green", "blue", "purple"

)

color_match <- str_c(colors, collapse = "|")

color_match

#现在我们可以选取出包含一种颜色的句子,

#再从中提取出颜色,就可以知道有哪些颜色了:

has_color <- str_subset(sentences, color_match)

matches <- str_extract(has_color, color_match)

#注意,str_extract() 只提取第一个匹配。

#我们可以先选取出具有多于一种匹配的所有句子,

#然后就可以很容易地看到更多匹配

more <- sentences[str_count(sentences, color_match) > 1]

str_view_all(more, color_match)

str_extract(more, color_match

#要想得到所有匹配,可以使用 str_extract_all() 函数,它会返回一个列表:

str_extract_all(more, color_match)

#如果设置了simplify = TRUE,那么str_extract_all()会返回一个矩阵,

str_extract_all(more, color_match, simplify = TRUE)

#其中较短的匹配会扩展到与最长的匹配具有同样的长度:

x <- c("a", "a b", "a b c")

str_extract_all(x, "[a-z]", simplify = TRUE)

#每个句子的第一个单词。

str_extract(sentences,"^([^ ]+) ")

#以ing结尾的所有单词。

ing<-sentences[str_detect(sentences,"([^ ]+)ing ")]

str_trim(string = str_extract(ing,"([^ ]+)ing "),side = "right")

#分组匹配

#假设我们想从句子中提取出名词。

#找出跟 在 a 或 the 后面的所有单词。

#因为使用正则表达式定义“单词”有一点难度,

#所以我们使用一种简单的近似定义——

#至少有 1 个非空格字符的字符序列:

noun <- "(a|the) ([^ ]+)"

has_noun <- sentences %>%

  str_subset(noun) %>%

  head(10)

has_noun %>%

  str_extract(noun)

#str_extract() 函数可以给出完整匹配;

#str_match() 函数则可以给出每个独立分组。

#str_ match() 返回的不是字符向量,而是一个矩阵,

#其中一列是完整匹配,后面的列是每个分组的匹配:

has_noun %>%

  str_match(noun)

#这种启发式名词检测的效果并不好,它还找出了一些形容词,比如 smooth 和 parked。

#如果数据是保存在 tibble 中的,那么使用 tidyr::extract() 会更容易。

#这个函数的工作方式与 str_match() 函数类似,

#只是要求为每个分组提供一个名称,以作为新列放在 tibble 中:

tibble(sentence = sentences) %>%

  tidyr::extract(

    sentence, c("article", "noun"), "(a|the) ([^ ]+)",

    remove = FALSE )

#与 str_extract() 函数一样,

#如果想要找出每个字符串的所有匹配,你需要使用 str_match_ all() 函数。

#替换匹配内容

#str_replace() 和 str_replace_all() 函数可以使用新字符串替换匹配内容。

#最简单的应用是使用固定字符串替换匹配内容

x <- c("apple", "pear", "banana")

str_replace(x, "[aeiou]", "-")

str_replace_all(x, "[aeiou]", "-")

#通过提供一个命名向量,使用 str_replace_all() 函数可以同时执行多个替换:

x <- c("1 house", "2 cars", "3 people")

str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))

#除了使用固定字符串替换匹配内容,

#你还可以使用回溯引用来插入匹配中的分组。

#在下面的代码中,我们交换了第二个单词和第三个单词的顺序:

sentences %>%

  str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>%

  head(5)

#拆分

#str_split() 函数可以将字符串拆分为多个片段。

#我们可以将句子拆分成单词:

sentences %>%

  head(5) %>%

  str_split(" ")

#因为字符向量的每个分量会包含不同数量的片段,

#所以 str_split() 会返回一个列表。

#如果你拆分的是长度为 1 的向量,那么只要简单地提取列表的第一个元素即可

"a|b|c|d" %>%

  str_split("\\|") %>%

  .[[1]]

#> [1] "a" "b" "c" "d"

#否则,和返回列表的其他 stringr 函数一样,

#你可以通过设置 simplify = TRUE 返回一个矩阵:

sentences %>%

  head(5) %>%

  str_split(" ", simplify = TRUE)

#你还可以设定拆分片段的最大数量:

fields <- c("Name: Hadley", "Country: NZ", "Age: 35")

fields %>% str_split(": ", n = 2, simplify = TRUE)

#你还可以通过字母、行、句子和单词边界(boundary() 函数)来拆分字符串:

x <- "This is a sentence. This is another sentence."

str_view_all(x, boundary("word"))

str_split(x, " ")[[1]]

str_split(x, boundary("word"))[[1]]

#定位匹配内容

#str_locate() 和 str_locate_all() 函数可以给出每个匹配的开始位置和结束位置。

#其他类型的模式

#当使用一个字符串作为模式时,R 会自动调用 regex() 函数对其进行包装:

# 正常调用:

str_view(fruit, "nana")

# 上面形式是以下形式的简写

str_view(fruit, regex("nana"))

#你可以使用 regex() 函数的其他参数来控制具体的匹配方式。

#• ignore_case = TRUE既可以匹配大写字母,也可以匹配小写字母,

#  它总是使用当前的区域设置:

bananas <- c("banana", "Banana", "BANANA")

str_view(bananas, "banana")

str_view(bananas, regex("banana", ignore_case = TRUE))

#• multiline = TRUE可以使得^和$从每行的开头和末尾开始匹配,

# 而不是从完整字符串 的开头和末尾开始匹配:

x <- "Line 1\nLine 2\nLine 3"

str_extract_all(x, "^Line")[[1]]

#> [1] "Line"

str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]

#> [1] "Line" "Line" "Line"

#• comments = TRUE可以让你在复杂的正则表达式中加入注释和空白字符,

#以便更易理解。 匹配时会忽略空格和 # 后面的内容。

#如果想要匹配一个空格,你需要对其进行转义:"\\ ":

phone <- regex("

\\(?

(\\d{3})

[)- ]?

(\\d{3})

[ -]?

(\\d{3})

", comments = TRUE)

# 可选的开括号

# 地区编码

# 可选的闭括号、短划线或空格 # 另外3个数字

# 可选的空格或短划线

# 另外3个数字

str_match("514-791-8141", phone)

#• dotall = TRUE 可以使得 . 匹配包括 \n 在内的所有字符。

#除了 regex(),你还可以使用其他 3 种函数。

#• fixed() 函数可以按照字符串的字节形式进行精确匹配,

#它会忽略正则表达式中的所有特殊字符,并在非常低的层次上进行操作。

#这样可以让你不用进行那些复杂的转义操作, 而且速度比普通正则表达式要快很多。

microbenchmark::microbenchmark(

  fixed = str_detect(sentences, fixed("the")),

  regex = str_detect(sentences, "the"),

  times = 20

)

#在匹配非英语数据时,要慎用 fixed() 函数。它可能会出现问题,因为此时同一个字符 经常有多种表达方式。例如,定义 á 的方式有两种:一种是单个字母 a,另一种是 a 加 上重音符号:

a1 <- "\u00e1"

a2 <- "a\u0301"

c(a1, a2)

#> [1] "á" "a

́ a1 == a2

#> [1] FALSE

#你可以使用接下来将要介绍的 coll() 函数,按照我们使用的字符比较规则来进行匹配

#• coll() 函数使用标准排序规则来比较字符串,这在进行不区分大小写的匹配时是非常 有效的。注意,可以在 coll() 函数中设置 locale 参数,以确定使用哪种规则来比较字符。 遗憾的是,世界各地所使用的规则是不同的!

#• 在介绍 str_split() 函数时,你已经知道可以使用 boundary() 函数来匹配边界。你还可 以在其他函数中使用这个函数:

x <- "This is a sentence."

str_view_all(x, boundary("word"))

str_extract_all(x, boundary("word"))

#正则表达式的其他应用

#R 基础包中有两个常用函数,它们也可以使用正则表达式。

#• apropos() 函数可以在全局环境空间中搜索所有可用对象。

#当不能确切想起函数名称时, 这个函数特别有用:

apropos("replace")

#• dir() 函数可以列出一个目录下的所有文件。

#dir() 函数的 patten 参数可以是一个正则 表达式,此时它只返回与这个模式相匹配的文件名。

#例如,你可以使用以下代码返回当 前目录中的所有 R Markdown 文件:

head(dir(pattern = "\\.Rmd$"))

#(如果更喜欢使用 *.Rmd 这样的“通配符”,

#你可以通过 glob2rx() 函数将其转换为正则表达式。)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容