原文地址:http://r4ds.had.co.nz/strings.html
作者:Hadley Wickham
---
文章目录:
1 介绍
1.1 前提条件
2 字符串基本操作
2.1 字符串长度
2.2 合并字符串
2.3 分割字符串
2.4 字符串本地化处理
2.5 小练习
3 用正则表达式匹配字符串
3.1 基本匹配操作
3.2 锚点
3.3 字符类型和多选符
3.4 重复匹配
3.5 分组和反向引用
4 相关工具
4.1 检测匹配结果
4.2 小练习
4.3 提取匹配结果
4.4 分组匹配
4.5 替换匹配结果
4.6 切分
4.7 寻找匹配位置
5 其他模式类型
5.1 小练习
6 正则表达式的其他用途
7 stringi库
7.1 小练习
---
1 介绍
这篇文章将为你介绍如何使用R语言来处理字符串。你学会字符串的基本操作和如何创建字符串,但这章的重点会在正则表达式上。由于字符串通常包含未经处理、格式混乱的数据,所以正则表达式会在此发挥很大的作用。正则表达式是种简明的语言,用于描述字符串中的组织模式。当你第一次看到正则表达式时,他们就像躺在你键盘上的猫一样无从下手,但当你逐步理解时,你就明白了其意义。
1.1 前提条件
这篇文章将以stringr包作为字符串处理的重心。由于我们通常不总会处理到文本数据,所以stringr包不是tidyverse包的核心组成部分,所以我们需要额外导入这个包。
library(tidyverse) library(stringr)
2 字符串基本操作
你可以通过单引号或双引号来创建字符串。不同于其他语言,这两种方法并不会造成不同的结果。但我推荐使用双引号"",除非你想要创建包含有多个双引号的字符串。
string1 <- "This is a string" string2 <- 'If I want to include a "quote" inside a string, I use single quotes'
如果你忘记加上第二个双引号(或单引号)就回车,在第二行行首会出现+,在这里我们叫它续行符:
> "This is a string without a closing quote + + + HELP I'M STUCK
如果发生了这样的情况,可以按Esc来结束这次输入,也可以在结尾输入第二个双引号(或单引号)来完成输入。
要想在字符串输入单引号或双引号而不引起错误的话,你可以使用\来为其“转义”:
double_quote <- "\"" # or '"' single_quote <- '\'' # or "'"
这意味着,若是你想要输入反斜线符号"\",则需要输入两次:"\"。
但要当心,字符串的打印表示和其自身并不一样,因为打印表示会显示出转义符。如果想要看到其真正的样子,要用writeLines():
x <- c("\"", "\\") x #> [1] "\"" "\\" writeLines(x) #> " #>
这类特殊字符并不算多。最常用的便是"\n",用来表示换行符(newline),还有"\t",表示制表符(tab),你也可以通过输入?'"'或?"'"来查看完整的转义字符列表。有时你也会看到类似"\u00b5"的字符,这是种输入非英文字符的方法,适用于所有平台:
x <- "\u00b5" x #> [1] "µ"
多个字符串通常存在一个字符向量中,你可以通过c()来创建:
c("one", "two", "three") #> [1] "one" "two" "three"
2.1 字符串长度
R语言中本身自带有多个函数来处理字符串,但我们在此不使用它们,因为它们用起来不方便且难记。在此我们会使用stringr包提供的函数。这些函数的名字相较前者更为直观,而且都以str_开头。例如,str_length()表示字符串中的字符数量:
str_length(c("a", "R for data science", NA)) #> [1] 1 18 NA
如果你使用RStudio的话,str_前缀会更为好用,因为当你输入str_时会触发其自动完成功能,让你能看到所有stringr包中的函数:
2.2 合并字符串
要想合并两个及以上的字符串,就要用str_c():
str_c("x", "y") #> [1] "xy" str_c("x", "y", "z") #> [1] "xyz"
使用seq参数来控制其合并后字符串之间的字符:
str_c("x", "y", sep = ", ") #> [1] "x, y"
就如R语言中的其他函数一样,缺失值处理起来很麻烦。如果你想让缺失值输出为"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()的输出结果也是向量,若是参数中含有固定字符串和向量,则会将固定字符串分别和各个向量结合:
tr_c("prefix-", c("a", "b", "c"), "-suffix") #> [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"
而字符大小为0的对象则会被丢弃。这个特性在和if一同使用时会尤其有用:
name <- "Hadley" time_of_day <- "morning" birthday <- FALSE str_c( "Good ", time_of_day, " ", name, if (birthday) " and HAPPY BIRTHDAY", "." ) #> [1] "Good morning Hadley."
要是想要将一个字符串向量结合成单个字符串,使用collapse参数:
str_c(c("x", "y", "z"), collapse = ", ") #> [1] "x, y, z"
2.3 分割字符串
你可以通过使用str_sub()来分割字符串。就如字符串一样,str_sub()也通过接受起点和终点的参数来确定切片的位置:
x <- c("Apple", "Banana", "Pear") str_sub(x, 1, 3) #> [1] "App" "Ban" "Pea" # negative numbers count backwards from end str_sub(x, -3, -1) #> [1] "ple" "ana" "ear"
需要注意的是,str_sub()在字符串比切片始末点短时也不会出错,因为它会返回尽可能多的字符:
str_sub("a", 1, 5) #> [1] "a"
你也可以使用赋值形式来修改字符串:
str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1)) x #> [1] "apple" "banana" "pear"
2.4 字符串本地化处理
之前我曾说过使用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语言编码标准决定,其参数通常是两个或三个字母的缩写。如果你不知道你自己语言的参数代码,参阅维基上的列表。如果你为该参数留空,则默认使用当前机子的语言本地化。
另一个受本地化影响较大的操作是分类。R语言自带的order()和sort()函数基于当前本地化来分类字符串。如果你想要让分类在不同电脑间有更好的表现,就使用str_sort()和str_order()吧,这两个函数也带有locale参数:
x <- c("apple", "eggplant", "banana") str_sort(x, locale = "en") # English #> [1] "apple" "banana" "eggplant" str_sort(x, locale = "haw") # Hawaiian #> [1] "apple" "eggplant" "banana"
2.5 小练习
有些不使用stringr包的代码中,通常会有paste()和paste0()。它们之间有什么不同?它们相当于stringr包中的什么函数呢?这些函数在处理缺失值时又有什么不同呢?
用自己的话来阐述str_c()中sep和collapse参数之间的的不同。
使用str_length()和str_sub()来从一个字符串中提取中间的字符。当字符串中的字符数量是复数时你怎么办?
str_wrap()有什么用?你什么时候会用到它?
str_trim()有什么用?它是不是str_trim()的反面函数?
写出一个函数,让其完成如下类似操作:将向量c("a", "b", "c")转变为字符串"a, b, and c"。思考若是其向量长度为0,1,或2时它会怎样。
3 用正则表达式匹配字符串
正则表达式是个十分简明的语言,其让你能描述字符串中的组成模式。你需要一些时间来理解正则表达式,但一旦你能看懂它们,你会发现它们有用极了。
在这里,我们将使用str_view()和str_view_all()来学习正则表达式。这两个函数输入一个字符向量和一个正则表达式,输出给你匹配结果。我们会从最简单的正则表达式开始,慢慢过渡到更复杂的表达式。一旦你学会模式匹配方法,你会学会如何应用这些点子到大量stringr函数中。
3.1 基本匹配操作
最简单的模式匹配是匹配对应一模一样的字符串:
x <- c("apple", "banana", "pear") str_view(x, "an")
接下来学习.的用法,它能匹配任何字符(除了换行符):
str_view(x, ".a.")
但若是"."能匹配任何字符,它怎么才能匹配自己,即"."字符呢?你只需要输入转义字符来告诉正则表达式你想匹配它就好了。就如字符串一样,正则表达式也使用反斜线\来转义特殊字符。所以要想匹配".",你只需要使用\.就好了。然而这也会造成麻烦。我们使用字符串来表现正则表达式,而\在字符串里也是个转义符号。所以若是要创建正则表达式的.,我们需要输入字符串"\."。
# To create the regular , we need \dot <- "\\." # But the itself only contains one: writeLines(dot) #> \. # And this tells R to look for an explicit . str_view(c("abc", "a.c", "bef"), "a\\.c")
如果\在正则表达式里用作转义字符,那要匹配反斜线\要怎么做呢?你还是需要让其转义,创建正则表达式\。要想使用正则表达式,你需要个字符串,这里还需要转义\。所以,你想要匹配反斜线\的话,你要输入"\\"————你需要四个反斜线才能匹配到一个真正的反斜线!
x <- "a\\b" writeLines(x) #> a\b str_view(x, "\\\\")
3.1.1 小练习
解释为什么这些字符串不能匹配到一个\:"\", "\", "\\"。
如何匹配"'\?
\..\..\..这个正则表达式会匹配到什么?如何用一个字符串表现它?
3.2 锚点
默认情况下,正则表达式会匹配任何位置的字符串。使用锚点对正则表达式十分有用,那样正则表达式就能确定要匹配字符串首端或是末端的特定字符串。
你可以使用:
^来匹配字符串的首端
$来匹配字符串的末端
x <- c("apple", "banana", "pear") str_view(x, "^a")
str_view(x, "a$")
要想记住这两个符号,试试我从Evan Misshula学到的记忆法:
如果你一开始就拥有了力量(^),那你到最后会收获金钱($)。
要想让正则表达式只匹配完整的一个字符串,使用^和$来匹配:
x <- c("apple pie", "apple", "apple cake") str_view(x, "apple")
str_view(x, "^apple$")
你也可以使用\b来匹配两边有边界的单词。在R里我不经常用到它,但我在使用RStudio时会用来查找某个函数是否是其他函数的组成部分。例如,我会搜索\bsum\b来避免匹配到summarise, summary, rowsum这些函数。
3.2.1 小练习
如何匹配"$^$"这个字符串?
根据string::words中的语料库,创建正则表达式来分别查找符合下列条件的单词:
以"y"开头
以"x"结束。
正好是三个字母组成。(拒绝使用str_length(),这是作弊!)
带有七个以上的字母。
由于这个列表比较长,你或许想要使用str_view()中的match参数来只显示匹配到的或是未匹配到的单词。
3.3 字符类型和多选符
正则表达式中还有很多特殊符号能够匹配多个字符。比如前面已经出现的.,可以匹配除了换行符的任何字符。以下是四个这类符号:
\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")
3.3.1 小练习
使用正则表达式匹配以下条件的单词:
由元音开头。
只包含辅音字母的(提示:想想如何匹配"非"元音字母)。
以ed结尾,但不以eed结尾。
以ing或ise结尾
验证一下构词规则"i通常在e之前,除非在前面有c"
"q"后面是否总跟着"u"?
写出个正则表达式,匹配一个由英式英语而不是美式英语写出的单词。
写出个正则表达式,匹配常见的电话号码。
3.4 重复匹配
这一部分我们将学习控制匹配的次数:
?: 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]+')
注意,这种操作符的优先度较高,所以你可以用colou?r来匹配其英式形式和美式形式。这意味着,其多数情况下要输入括号来注明,例如bana(na)+。
你也可以标明需要匹配的次数:
{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}")
默认情况下,这些匹配是"贪婪匹配":他们将匹配尽可能长的字符串。当然你也能设置"懒惰匹配",匹配出尽可能短的字符,只需在后面加上?即可。这是正则表达式的高级特性,而且十分有用:
str_view(x, 'C{2,3}?')
str_view(x, 'C[LX]+?')
3.4.1 小练习
用{m,n}来表示?,+,*这三种匹配方式。
用自己的话说一下这些正则表达式的作用(仔细阅读,判断我是用一个正则表达式还是一个字符串来定义正则表达式):
^.*$
"\\{.+\\}"
\d{4}-\d{2}-\d{2}
"\\\\{4}"
写出个正则表达式,找出符合以下条件的单词:
以三个辅音字母开头。
带有连续三个原因。
带有连续两个及以上元音-辅音字母对。
去完成链接中的正则表达式练习
3.5 分组和反向引用
之前的部分,你学会如何使用括号来给表达式消除歧义。括号也能用于定义"分组",这些分组能让你在之后使用\1、\2这样的符号进行反向引用。例如,以下正则表达式会找到所有包含重复字母组合的水果单词:
str_view(fruit, "(..)\\1", match = TRUE)
3.5.1 小练习
说出以下表达式会匹配到什么:
(.)\1\1
"(.)(.)\\2\\1"
(..)\1
"(.).\\1.\\1"
"(.)(.)(.).*\\3\\2\\1"
写出符合以下匹配条件的正则表达式:
以相同字符开始和结束。
包含重复的字母对(例如,"church"包含两个"ch")
包含一个重复了至少三次的字母(例如,"eleven"有三个"e")
4 相关工具
到这里你已经学会了正则表达式的基本用法,是时候将它们用于解决实际问题了。这部分你将学会大量stringr包里的函数,它们能让你:
确定哪些字符串能够被匹配
找到匹配结果的位置
提取匹配结果的内容
替换匹配结果为其他内容
基于匹配结果切分字符串
这里要注意,因为正则表达式十分强大,想要用一个正则表达式来解决所有难题也很简单。
就如Jamie Zawinski所说:
有些人面对一个难题时,会想“我知道,我会用正则表达式来解决”。于是,他们现在遇到了两个难题。
作为个警示的例子,看看下面这个用来检查email地址是否有效的正则表达式:
(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t] )+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?: \r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:( ?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\0 31]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+ (?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?: (?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z |(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n) ?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n) ?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t] )*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])* )(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t] )+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*) *:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+ |\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r \n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?: \r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t ]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031 ]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\]( ?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(? :(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(? :\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(? :(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)? [ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]| \\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<> @,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|" (?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t] )*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(? :[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[ \]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000- \031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|( ?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,; :\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([ ^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\" .\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\] |\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \0 00-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@, ;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(? :[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])* (?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\". \[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[ ^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\] ]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*( ?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:( ?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[ \["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t ])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t ])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(? :\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+| \Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?: [^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n) ?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[" ()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n) ?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<> @,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@, ;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t] )*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)? (?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\". \[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?: \r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[ "()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t]) *))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]) +|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z |(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:( ?:\r\n)?[ \t])*))*)?;\s*)
这是个相当典型的例子(因为email地址通常惊人地复杂),但在实际代码里经常被用到。你可以查看该StackOverflow链接以获得更多细节。
别忘了你在使用编程语言,你任意使用其他工具。通常情况下,创建一系列简单的正则表达式会比创建一整个复杂的正则表达式要简单。如果你创建正则表达式过程中遇到困难,你可以停下想想,看是否能将问题简化成简单点的问题,在遇到下个问题前解决所有问题。
4.1 检测匹配结果
要想知道一个字符向量是否符合一个模式,使用str_detect()函数。它会返回输入向量一样长度的逻辑向量:
x <- c("apple", "banana", "pear") str_detect(x, "e") #> [1] TRUE FALSE TRUE
记住,当你在数值型环境中使用逻辑向量时,FALSE的值为0,而TRUE的值为1。所以要是你想知道关于匹配结果的一些情况,sum()和mean()在此会很有用:
# 有多少单词以t开头? sum(str_detect(words, "^t")) #> [1] 65 # 由元音结尾的单词占多大比例? mean(str_detect(words, "[aeiou]$")) #> [1] 0.277
当你遇到复杂的逻辑匹配条件时(例如,要匹配a或b,但没有d的话不匹配c),相较于使用正则表达式,使用多个str_detect()来调用逻辑运算符会处理起来更简单。例如,下面是两个匹配没有元音字母单词的方法:
# 找到所有至少包含一个元音字母的单词,将其除外 no_vowels_1 <- !str_detect(words, "[aeiou]") # 找到所有只包含辅音字母的单词 no_vowels_2 <- str_detect(words, "^[^aeiou]+$") identical(no_vowels_1, no_vowels_2) #> [1] TRUE
两种方法的结果是一样的,但我觉得第一种方法明显更简单明了。当你的正则表达式变得相当复杂时,试着将其分成一个个小部分,再给它们分别取名字,最后再用逻辑运算符来将它们结合起来。
通常我们使用str_detect()来选择符合某种模式的元素。你可以将其运用于逻辑切片,或是较为方便的str_subset()分装器:
words[str_detect(words, "x$")] #> [1] "box" "sex" "six" "tax" str_subset(words, "x$") #> [1] "box" "sex" "six" "tax"
通常情况下,你的字符串属于某个数据框中的一列,我们会使用filter()处理它:
df <- tibble( word = words, i = seq_along(word) ) df %>% filter(str_detect(words, "x$")) #> # A tibble: 4 × 2 #> word i #> <chr> <int> #> 1 box 108 #> 2 sex 747 #> 3 six 772 #> 4 tax 841
str_detect()的一个变体则是str_count(),这个函数不是简单返回对或错,而是告诉你在这一个字符串中有多少个符合条件的匹配结果:
x <- c("apple", "banana", "pear") str_count(x, "a") #> [1] 1 3 1 # On average, how many vowels per word? mean(str_count(words, "[aeiou]")) #> [1] 1.99
和mutate()一起使用str_count()效果也很好:
df %>% mutate( vowels = str_count(word, "[aeiou]"), consonants = str_count(word, "[^aeiou]") ) #> # A tibble: 980 × 4 #> word i vowels consonants #> <chr> <int> <int> <int> #> 1 a 1 1 0 #> 2 able 2 2 2 #> 3 about 3 3 2 #> 4 absolute 4 4 4 #> 5 accept 5 2 4 #> 6 account 6 3 4 #> # ... with 974 more rows
值得注意的是,这些匹配结果不会重叠。例如,在abababa这个字符串中,你觉得aba会被匹配到多少次呢?正则表达式告诉你是两次而不是三次:
str_count("abababa", "aba") #> [1] 2 str_view_all("abababa", "aba")
注意str_view_all()的用法。就如你刚知道的,很多stringr函数都是成对出现的:一个函数用于匹配单个结果,另一个匹配所有结果。而那后者则通常带有后缀_all。
4.2 小练习
对于以下练习,试着使用单个正则表达式,和多个str_detect()调用来分别完成。
找到所有以x开头或结尾的单词。
找到所有以元音字母开头和以辅音字母结束的单词。
有没有包含所有元音字母的单词。
什么单词的元音字母数量最多?什么单词中元音字母所占比例最大?(提示:这个问题中的分母是什么呢?)
4.3 提取匹配结果
要想提取文本里匹配结果,可以使用str_extract()。这次,我将用个更为复杂的例子。我将用到Harvard sentences,这是用于检测VOIP系统的一套文本,但在练习正则表达式时也一样有用。它们内置于string::sentences里:
length(sentences) #> [1] 720 head(sentences) #> [1] "The birch canoe slid on the smooth planks." #> [2] "Glue the sheet to the dark blue background." #> [3] "It's easy to tell the depth of a well." #> [4] "These days a chicken leg is a rare dish." #> [5] "Rice is often served in round bowls." #> [6] "The juice of lemons makes fine punch."
如果我们想找到所有包含颜色的句子,我们首先创建一个以颜色名字构成的向量,然后将其转变为正则表达式:
colours <- c("red", "orange", "yellow", "green", "blue", "purple") colour_match <- str_c(colours, collapse = "|") colour_match #> [1] "red|orange|yellow|green|blue|purple"
现在我们就能选出那些包含颜色词的句子了,然后提取出来看看它们都是什么:
has_colour <- str_subset(sentences, colour_match) matches <- str_extract(has_colour, colour_match) head(matches) #> [1] "blue" "blue" "red" "red" "red" "blue"
注意,str_extract()只会提取其首次匹配的结果。我们可以直接选取带有一个匹配结果以上的所有句子:
more <- sentences[str_count(sentences, colour_match) > 1] str_view_all(more, colour_match)
str_extract(more, colour_match) #> [1] "blue" "green" "orange"
这是stringr函数中的常见模式,因为只处理一次匹配能让你使用更为简单的数据结构。要想得到所有匹配结果,使用str_extract_all()。它将返回一个列表:
str_extract_all(more, colour_match) #> [[1]] #> [1] "blue" "red" #> #> [[2]] #> [1] "green" "red" #> #> [[3]] #> [1] "orange" "red"
你将在lists部分和iteration部分了解到更多关于列表的信息。
如果你使用simplify = TRUE参数,str_extract_all()会返回一个矩阵:
str_extract_all(more, colour_match, simplify = TRUE) #> [,1] [,2] #> [1,] "blue" "red" #> [2,] "green" "red" #> [3,] "orange" "red" x <- c("a", "a b", "a b c") str_extract_all(x, "[a-z]", simplify = TRUE) #> [,1] [,2] [,3] #> [1,] "a" "" "" #> [2,] "a" "b" "" #> [3,] "a" "b" "c"
4.3.1 小练习
在之前的例子中,你会发现那个正则表达式会匹配到"flickered",但它不是颜色词。修改该正则表达式以修复这个问题。
从 Harvard sentences 数据中,提取:
每个句子的第一个单词。
所有以ing结尾的单词。
所有复数词。
4.4 分组匹配
这篇文章前面些内容里,我们谈到了使用括号来明确优先级和匹配时的反向引用。你也可以使用括号来提取整个匹配结果的某些部分。例如,我们要想提取句子里的名词。我们可以直接找出那些在"a"或"the"后面的单词。想要在正则表达式中定义一个“单词”的方式比较特别,所以在此我将使用个方法来匹配尽量符合条件的单词:一串至少有一个非空格字符的字符串。
noun <- "(a|the) ([^ ]+)" has_noun <- sentences %>% str_subset(noun) %>% head(10) has_noun %>% str_extract(noun) #> [1] "the smooth" "the sheet" "the depth" "a chicken" "the parked" #> [6] "the sun" "the huge" "the ball" "the woman" "a helps"
str_extract()会返回完整的匹配结果;而str_match()会返回每个独立的组成部分,返回形式将是个矩阵而不是字符向量:
has_noun %>% str_match(noun) #> [,1] [,2] [,3] #> [1,] "the smooth" "the" "smooth" #> [2,] "the sheet" "the" "sheet" #> [3,] "the depth" "the" "depth" #> [4,] "a chicken" "a" "chicken" #> [5,] "the parked" "the" "parked" #> [6,] "the sun" "the" "sun" #> [7,] "the huge" "the" "huge" #> [8,] "the ball" "the" "ball" #> [9,] "the woman" "the" "woman" #> [10,] "a helps" "a" "helps"
(很明显,我们的方法仍有很大欠缺,它不仅匹配了名词还匹配了smooth和parked这类形容词。)
如果你的数据是tibble类型,通常使用tidyr::extract()来处理更简单。它用起来和str_match()差不多,但要你为匹配结果命名:
tibble(sentence = sentences) %>% tidyr::extract( sentence, c("article", "noun"), "(a|the) ([^ ]+)", remove = FALSE ) #> # A tibble: 720 × 3 #> sentence article noun #> * <chr> <chr> <chr> #> 1 The birch canoe slid on the smooth planks. the smooth #> 2 Glue the sheet to the dark blue background. the sheet #> 3 It's easy to tell the depth of a well. the depth #> 4 These days a chicken leg is a rare dish. a chicken #> 5 Rice is often served in round bowls. <NA> <NA> #> 6 The juice of lemons makes fine punch. <NA> <NA> #> # ... with 714 more rows
如str_extract()一样,如果你想要得到每个字符串的所有匹配结果,你要用str_match_all()来获取。
4.4.1 小练习
找到所有紧跟着数字的单词,例如在"one"或"two"之后的单词。将这些数字连同单词一起提取出来。
找到所有的缩略词。将其以单引号为分界切片。
4.5 替换匹配结果
str_replace()和str_replace_all()可以让你以其他字符串替换匹配结果。最简单的用法是将匹配结果替换为某个确定的字符串:
x <- c("apple", "pear", "banana") str_replace(x, "[aeiou]", "-") #> [1] "-pple" "p-ar" "b-nana" str_replace_all(x, "[aeiou]", "-") #> [1] "-ppl-" "p--r" "b-n-n-"
使用str_replace_all()还可以进行多重替换:
x <- c("1 house", "2 cars", "3 people") str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three")) #> [1] "one house" "two cars" "three people"
不只是用写好的字符串来替换,你还可以用反向引用的结果来作为替换值。下列代码中我改变了匹配到的第二和第三个单词的位置:
sentences %>% str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>% head(5) #> [1] "The canoe birch slid on the smooth planks." #> [2] "Glue sheet the to the dark blue background." #> [3] "It's to easy tell the depth of a well." #> [4] "These a days chicken leg is a rare dish." #> [5] "Rice often is served in round bowls."
4.5.1 小练习
替换所有正斜线为反斜线。
用replace_all()来实现简单的str_to_lower()的功能。
用words中的所有第一和最后一个字母。哪些被替换后的字符串仍是正确的单词?
4.6 切分
用str_split()来将字符串切分为更小的部分。例如,我们可以将句子切分成单词:
sentences %>% head(5) %>% str_split(" ") #> [[1]] #> [1] "The" "birch" "canoe" "slid" "on" "the" "smooth" #> [8] "planks." #> #> [[2]] #> [1] "Glue" "the" "sheet" "to" "the" #> [6] "dark" "blue" "background." #> #> [[3]] #> [1] "It's" "easy" "to" "tell" "the" "depth" "of" "a" "well." #> #> [[4]] #> [1] "These" "days" "a" "chicken" "leg" "is" "a" #> [8] "rare" "dish." #> #> [[5]] #> [1] "Rice" "is" "often" "served" "in" "round" "bowls."
因为每个部分的单词字母都不一样,这将返回一个列表。如果你在处理一个单字符长度的向量,最简单的方法是提取列表中的第一个元素:
"a|b|c|d" %>% str_split("\\|") %>% .[[1]] #> [1] "a" "b" "c" "d"
否则,你可以使用simplify = TRUE返回矩阵:
sentences %>% head(5) %>% str_split(" ", simplify = TRUE) #> [,1] [,2] [,3] [,4] [,5] [,6] [,7] #> [1,] "The" "birch" "canoe" "slid" "on" "the" "smooth" #> [2,] "Glue" "the" "sheet" "to" "the" "dark" "blue" #> [3,] "It's" "easy" "to" "tell" "the" "depth" "of" #> [4,] "These" "days" "a" "chicken" "leg" "is" "a" #> [5,] "Rice" "is" "often" "served" "in" "round" "bowls." #> [,8] [,9] #> [1,] "planks." "" #> [2,] "background." "" #> [3,] "a" "well." #> [4,] "rare" "dish." #> [5,] "" ""
你也可以控制列数的数量:
fields <- c("Name: Hadley", "Country: NZ", "Age: 35") fields %>% str_split(": ", n = 2, simplify = TRUE) #> [,1] [,2] #> [1,] "Name" "Hadley" #> [2,] "Country" "NZ" #> [3,] "Age" "35"
除了以模式来切分,你也可以以字符、行数、句子和单词来切分:
x <- "This is a sentence. This is another sentence." str_view_all(x, boundary("word"))
str_split(x, " ")[[1]] #> [1] "This" "is" "a" "sentence." "" "This" #> [7] "is" "another" "sentence." str_split(x, boundary("word"))[[1]] #> [1] "This" "is" "a" "sentence" "This" "is" #> [7] "another" "sentence"
4.6.1 小练习
切分类似"apples, pears, and bananas"这样的字符串为一个个单词。
为什么用boundary(word)会比用" "来切分更好?
以空字符("")切分有什么用?试一下再阅读文档。
4.7 寻找匹配位置
str_locate()和str_locate_all()可以给你提供每次匹配的始末点。你可以使用str_locate()来找到匹配模式,用str_sub()来提取和/或修改他们。
5 其他模式类型
当使用的模式是字符串时,它将会自动包装成一次调用给regex():
# The regular call: str_view(fruit, "nana") # Is shorthand for 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(" \\(? # optional opening parens (\\d{3}) # area code [)- ]? # optional closing parens, dash, or space (\\d{3}) # another three numbers [ -]? # optional space or dash (\\d{3}) # three more numbers ", comments = TRUE) str_match("514-791-8141", phone) #> [,1] [,2] [,3] [,4] #> [1,] "514-791-814" "514" "791" "814"
dotall = TRUE将让.匹配所有字符,包括\n。
以下是可以代替部分regex()功能的其他函数:
fixed():能够匹配准确的字节序列。它会无视所有特别的正则表达式并在很低的水平上操作。这意味着你可以避免复杂的转义操作并可以比正则表达式匹配得更快:
microbenchmark::microbenchmark( fixed = str_detect(sentences, fixed("the")), regex = str_detect(sentences, "the"), times = 20 ) #> Unit: microseconds #> expr min lq mean median uq max neval #> fixed 157 164 228 170 272 603 20 #> regex 588 611 664 635 672 1103 20
用fixed()处理非英文数据时要小心。因为通常会有很多种方法显示相同的字符,所以处理起来会很麻烦。例如,定义“á”有两种方法:用单独的该字符表示或是用"a"加上一个音符符号来表示:
a1 <- "\u00e1" a2 <- "a\u0301" c(a1, a2) #> [1] "á" "á" a1 == a2 #> [1] FALSE
这样渲染出来的样子是一致的,但是定义过程却完全不同,所以fixed()在此无法得到匹配结果。然而使用coll()则可以按照人类对字符的对比规则来匹配:
str_detect(a1, fixed(a2)) #> [1] FALSE str_detect(a1, coll(a2)) #> [1] TRUE
coll():使用标准排序规则来对比字符串。这在进行不分大小写的匹配中十分有用。但要注意,coll()在比较字符时使用了locale参数来控制所使用的规则。
# 这意味着你还需要注意其规则的不同 # 在做部分大小写的匹配时: i <- c("I", "İ", "i", "ı") i #> [1] "I" "İ" "i" "ı" str_subset(i, coll("i", ignore_case = TRUE)) #> [1] "I" "i" str_subset(i, coll("i", ignore_case = TRUE, locale = "tr")) #> [1] "İ" "i"
fixed()和regex()都接收ignore_case参数,但它们不允许你选择本地化设置:它们都总会使用默认的本地化设置。你可以用以下代码查看当前的默认设置:
stringi::stri_locale_info() #> $Language #> [1] "en" #> #> $Country #> [1] "US" #> #> $Variant #> [1] "" #> #> $Name #> [1] "en_US"
然而coll()的不足之处就是速度。因为其识别字符是否一致的规则十分复杂,coll()相对于regex()和fixed()运行速度会慢很多。
如你所知,你可以用str_split()中的boundary()来匹配边界。你也可以在其他函数使用这个参数:
x <- "This is a sentence." str_view_all(x, boundary("word"))
str_extract_all(x, boundary("word")) #> [[1]] #> [1] "This" "is" "a" "sentence"
5.1 小练习
用regex()和fixed()分别找到所有带有\的字符串。
sentences数据中哪五个词最常见?
6 正则表达式的其他用途
在R语言基本函数中有两个十分有用的函数也用到了正则表达式:
apropos()会从全局环境中搜索所有可用对象。这在你无法记住函数名时特别有用。
apropos("replace") #> [1] "%+replace%" "replace" "replace_na" "str_replace" #> [5] "str_replace_all" "str_replace_na" "theme_replace"
dir()将列出该目录下的所有文件。而pattern参数则接收一个正则表达式,返回符合模式的文件名。例如,你可以这样从当前目录找到所有R Markdown文件:
head(dir(pattern = "\\.Rmd$")) #> [1] "communicate-plots.Rmd" "communicate.Rmd" "datetimes.Rmd" #> [4] "EDA.Rmd" "explore.Rmd" "factors.Rmd"
7 stringi
stringr是在stringi的基础上构建的。stringr在你学习R语言时会很有用,因为这个包只用了一小部分函数,却解决了大多数字符串处理会遇到的问题。而stringi则是以功能的全面为目的。stringi包含了任何你需要的函数:stringi有232个函数,而stringr有43个函数。
如果你发现在用stringr包无法处理一些问题的话,你可以试试stringi中的函数。这两个包的用法很相似,所以你可以很自然地从stringr过渡到stringi。二者最主要的区别在于前缀:str_和stri_。
7.1 小练习
找到stringi包中符合下列功能的函数:
用来统计单词数量的函数。
用来找到重复字符串的函数。
生成随机文本的函数。
在使用stri_sort()函数时,如何控制分类的语言?