41 R语言的文本处理

41.1 简单的文本处理

在信息爆炸性增长的今天, 大量的信息是文本型的, 如互联网上的大多数资源。 R具有基本的文本数据处理能力, 而且因为R的向量语言特点和强大的统计计算和图形功能, 用R处理文本数据是可行的。

41.1.1 字符型常量与字符型向量

字符串常量写在两个双撇号或者两个单撇号中间, 建议仅使用双撇号, 因为这是大多数常见程序语言的做法。 如果内容中有单撇号或者双撇号, 可以在前面加反斜杠\。 为了在字符串中写一个反斜杠, 需要写成两个, 比如路径C:\work写成R字符串, 要写成"C:\\work"。 注意, 这些规定都是针对程序中的字符串常量, 数据中的文本类型数据是不需要遵照这些规定的。

在用print()显示字符串变量时, 也会按照上述的办法显示, 比如字符串内的双撇号会被自动加上前导反斜杠, 但保存的实际内容中并没有反斜杠。

字符串中可以有一些特殊字符, 如"\n"表示换行符, "\t"表示制表符, "\r"表示回车符,等等。

R的字符型向量每个元素是一个字符串, 如:

s <- c("123", "abc", "张三李四", "@#$%^&")
s
## [1] "123"      "abc"      "张三李四" "@#$%^&"

R中处理文本型数据的函数有文件访问函数以及readLinesnchar, pastesprintfformatformatCsubstring等函数。

R支持正则表达式, 函数grep, grepl, sub, gsub, regexpr, gregexpr, strsplit与正则表达式有关。

字符型函数一般都是向量化的, 对输入的一个字符型向量的每个元素操作。

R扩展包stringr和stringi提供了更方便、功能更强的字符串功能, 包括正则表达式功能。 其中stringr是常用功能, stringi是更基本、更灵活的功能, 一般使用stringr就足够了。 stringr包的函数名大多都以str_开头。

下面先介绍常用的较简单的字符串函数, 包括stringr包的函数与基本R函数。

library(stringr)

41.1.2 字符串连接、重复

stringr::str_c()用来把多个输入自变量按照元素对应组合为一个字符型向量, 用sep指定分隔符,默认为不分隔。 类似于R中向量间运算的一般规则, 各自变量长度不同时短的自动循环使用。 非字符串类型自动转换为字符型。 如

str_c(c("x", "y"), c("a", "b"), sep="*")
## [1] "x*a" "y*b"
str_c("data", 1:3, ".txt")
## [1] "data1.txt" "data2.txt" "data3.txt"

字符型缺失值参与连接时, 结果变成缺失值; 可以用str_replace_na()函数将待连接的字符型向量中的缺失值转换成字符串"NA"再连接。

collapse选项要求将连接后的字符型向量的所有元素连接在一起, collapse的值为将多个元素合并时的分隔符。 如

str_c(c("a", "bc", "def"), collapse="---")
## [1] "a---bc---def"

在使用了collapse时如果有多个要连接的部分, str_c()函数先将各部分连接成为一个字符型向量, 然后再把结果的各个向量元素连接起来。 如

str_c("data", 1:3, ".txt", sep="", collapse=";")
## [1] "data1.txt;data2.txt;data3.txt"

stringr::str_flatten()类似于stringr::str_c()仅有collapse参数作用一样, 仅将一个字符型向量的各个元素按照collapse参数指定的分隔符连接成一个长字符串, collapse默认值是空字符串,如:

str_flatten(c("a", "bc", "def"), collapse="---")
## [1] "a---bc---def"
str_flatten(c("a", "bc", "def"))
## [1] "abcdef"

基本R的paste()函数与stringr::str_c()函数有类似的用法, 但是参数sep的默认值是空格。 基本R的paste0()函数相当于stringr::str_c()函数固定sep参数为空字符串。 如:

paste(c("x", "y"), c("a", "b"), sep="*")
## [1] "x*a" "y*b"
paste("data", 1:3, ".txt", sep="")
## [1] "data1.txt" "data2.txt" "data3.txt"
paste0("data", 1:3, ".txt")
## [1] "data1.txt" "data2.txt" "data3.txt"
paste(c("a", "bc", "def"), collapse="---")
## [1] "a---bc---def"
paste("data", 1:3, ".txt", sep="", collapse=";")
## [1] "data1.txt;data2.txt;data3.txt"

stringr::str_dup(string, times)类似于rep()函数, 可以将字符型向量的元素按照times指定的次数在同一字符串内重复,如:

str_dup(c("abc", "长江"), 3)
## [1] "abcabcabc"    "长江长江长江"

也可以针对每个元素指定不同重复次数,如

str_dup(c("abc", "长江"), c(3, 2))
## [1] "abcabcabc" "长江长江"

41.1.3 格式化输出

41.1.3.1 format()函数

format()函数可以将一个数值型向量的各个元素按照统一格式转换为字符型, 如:

as.character(1.000)
## [1] "1"
as.character(1.2)
## [1] "1.2"
as.character(1.23)
## [1] "1.23"
format(c(1.000, 1.2, 1.23))
## [1] "1.00" "1.20" "1.23"

选项digitsnsmall共同控制输出的精度, nsmall控制非科学记数法显示时小数点后的至少要有的位数, digits控制至少要有的有效位数。 这使得输出的宽度是不可控的, 如:

format(c(pi, pi*10000), digits=8, nsmall=4)
## [1] "    3.1415927" "31415.9265359"

width参数指定至少要有的输出宽度, 不足时默认在左侧用空格填充,如:

format(1.000, width=6, nsmall=2)
## [1] "  1.00"

format()还有许多选项, 详见函数的帮助。

41.1.3.2 sprintf()函数

format()函数无法精确控制输出长度和格式。 sprintf是C语言中sprintf的向量化版本, 可以把一个元素或一个向量的各个元素按照C语言输出格式转换为字符型向量。 第一个自变量是C语言格式的输出格式字符串, 其中%d表示输出整数,%f表示输出实数, %02d表示输出宽度为2、不够左填0的整数, %6.2f表示输出宽度为6、宽度不足时左填空格、含两位小数的实数, 等等。

比如,标量转换

sprintf("%6.2f", pi)
## [1] "  3.14"

又如,向量转换:

sprintf("tour%03d.jpg", c(1, 5, 10, 15, 100))
## [1] "tour001.jpg" "tour005.jpg" "tour010.jpg" "tour015.jpg" "tour100.jpg"

还可以支持多个向量同时转换,如:

sprintf("%1dx%1d=%2d", 1:5, 5:1, (1:5)*(5:1))
## [1] "1x5= 5" "2x4= 8" "3x3= 9" "4x2= 8" "5x1= 5"

41.1.3.3 字符串插值函数

许多脚本型程序设计语言都有在字符串的内容中插入变量值的功能, R本身不具有这样的功能, sprintf()函数有类似作用但只是一个不方便使用的副作用。

stringr::str_glue()stringr::str_glue_data()提供了字符串插值的功能。 只要在字符串内用大括号写变量名, 则函数可以将字符串内容中的变量名替换成变量值,如:

name <- "李明"
tele <- "13512345678"
str_glue("姓名: {name}\n电话号码: {tele}\n")
## 姓名: 李明
## 电话号码: 13512345678

上面的例子直接用了换行符"\n"来分开不同内容。 也可以输入多个字符串作为自变量, 内容自动连接在一起,可以用参数.sep指定分隔符:

name <- "李明"
tele <- "13512345678"
str_glue("姓名: {name}, ", "电话号码: {tele}")
## 姓名: 李明, 电话号码: 13512345678
str_glue("姓名: {name}", "电话号码: {tele}", .sep="; ")
## 姓名: 李明; 电话号码: 13512345678

也可以直接在str_glue()中指定变量值,如:

str_glue("姓名: {name}", "电话号码: {tele}", .sep="; ",
         name = "张三", tele = "13588888888")
## 姓名: 张三; 电话号码: 13588888888

stringr::str_glue_data()则以一个包含变量定义的对象.x为第一自变量, 类型可以是环境、列表、数据框等。如:

str_glue_data(list(name = "王五", tele = "13500000000"),
              "姓名: {name}", "电话号码: {tele}", .sep="; ")
## 姓名: 王五; 电话号码: 13500000000

41.1.4 字符串长度

stringr::str_length(string)求字符型向量string每个元素的长度。 一个汉字长度为1。

str_length(c("a", "bc", "def", "北京"))
## [1] 1 2 3 2

函数nchar(text)计算字符串长度,默认按照字符个数计算而不是按字节数计算, 如

nchar(c("a", "bc", "def", "北京"))
## [1] 1 2 3 2

注意函数对输入的字符型向量每个元素计算长度。

nchar()加选项type="bytes"可用按字符串占用的字节数计算, 这时一个汉字占用多个字节(具体占用多少与编码有关)。 如

nchar(c("a", "bc", "def", "北京"), type="bytes")
## [1] 1 2 3 4

41.1.5 取子串

stringr::str_sub(string, start, end)字符串字串, 用开始字符位置start和结束字符位置end设定字串位置。 用负数表示倒数位置。 默认开始位置为1, 默认结束位置为最后一个字符。

如:

str_sub("term2017", 5, 8)
## [1] "2017"
str_sub(c("term2017", "term2018"), 5, 8)
## [1] "2017" "2018"
str_sub("term2017", 5)
## [1] "2017"
str_sub("term2017", -4, -1)
## [1] "2017"
str_sub("term2017", end=4)
## [1] "term"

取子串时,一般按照字符个数计算位置,如

str_sub("北京市海淀区颐和园路5号", 4, 6)
## [1] "海淀区"

当起始位置超过总长度或结束位置超过第一个字符时返回空字符串; 当起始位置超过结束位置是返回空字符串。 如:

str_sub("term2017", 9)
## [1] ""
str_sub("term2017", 1, -9)
## [1] ""
str_sub("term2017", 8, 5)
## [1] ""

可以对str_sub()结果赋值,表示修改子串内容,如:

s <- "term2017"
str_sub(s, 5, 8) <- "18"
s
## [1] "term18"

字符串替换一般还是应该使用专用的替换函数如stringr::str_replace_all()gsub()

基本R的substring(text, first, last)函数与stringr::str_sub()功能相同, 但firstlast参数不允许用负数, last的默认值是一个很大的数,所以省略last时会取到字符串末尾。 substring()对三个参数text, first, last都是向量化的, 长度不一致时按照一般的不等长向量间运算规则处理。如:

substring(c("term2017", "term2018"), first=c(1, 5), last=c(4, 8))
## [1] "term" "2018"
substring("term2017", first=c(1, 5), last=c(4, 8))
## [1] "term" "2017"

substring()也允许修改某个字符串的指定子串的内容,如

s <- "123456789"
substring(s, 3, 5) <- "abc"
s
## [1] "12abc6789"

R的substr(x, start, stop)作用类似, 但是仅支持x为字符型向量, startstop是标量。

41.1.6 字符串变换

41.1.6.1 大小写

stringr::str_to_upper(string)将字符型向量string中的英文字母都转换为大写。 类似函数有stringr::str_to_lower(string)转换为小写, stringr::str_to_title(string)转换为标题需要的大小写, stringr::str_to_scentence(string)转换为句子需要的大小写。 这都是针对英文的, 选项locale用来选语言,locale="en"为默认值。

基本R的toupper()将字符型向量的每个元素中的小写字母转换为大写, tolower()转小写。

41.1.6.2 字符变换表

基本R的chartr(old, new, x)函数指定一个字符对应关系, 旧字符在old中,新字符在new中,x是一个要进行替换的字符型向量。 比如,下面的例子把所有!替换成.,把所有;替换成,

chartr("!;", ".,", c("Hi; boy!", "How do you do!"))
## [1] "Hi, boy."       "How do you do."
chartr("。,;县", ".,;区", "昌平县,大兴县;固安县。")
## [1] "昌平区,大兴区;固安区."

第二个例子中被替换的标点是中文标点,替换成了相应的英文标点。

41.1.6.3 空白处理

stringr::str_trim(string, side)返回删去字符型向量string每个元素的首尾空格的结果, 可以用side指定删除首尾空格("both")、开头空格("left")、末尾空格("right")。 如:

str_trim(c("  李明", "李明  ", "  李明  ", "李  明"))
## [1] "李明"   "李明"   "李明"   "李  明"
str_trim(c("  李明", "李明  ", "  李明  ", "李  明"), side="left")
## [1] "李明"   "李明  " "李明  " "李  明"
str_trim(c("  李明", "李明  ", "  李明  ", "李  明"), side="right")
## [1] "  李明" "李明"   "  李明" "李  明"

stringr::str_squish(string)对字符型向量string每个元素, 删去首尾空格,将重复空格变成单个,返回变换后的结果。如:

str_squish(c("  李明", "李明  ", "  李明  ", "李  明"))
## [1] "李明"  "李明"  "李明"  "李 明"

基本R函数trimws(x, which)str_trim()作用类似, 选项which="left"可以仅删去开头的空格, 选项which="right"可以仅删去结尾的空格。

trimws(c("  李明", "李明  ", "  李明  ", "李  明"))
## [1] "李明"   "李明"   "李明"   "李  明"
trimws(c("  李明", "李明  ", "  李明  ", "李  明"), which="left")
## [1] "李明"   "李明  " "李明  " "李  明"
trimws(c("  李明", "李明  ", "  李明  ", "李  明"), which="right")
## [1] "  李明" "李明"   "  李明" "李  明"

为了去掉输入字符串中所有空格,可以用gsub()替换功能,如:

gsub(" ", "", c("  李明", "李明  ", "  李明  ", "李  明"), fixed=TRUE)
## [1] "李明" "李明" "李明" "李明"

stringr::str_pad(string, width)可以将字符型向量string的每个元素加长到width个字符, 不足时左补空格,已经达到或超过width的则不变,如:

str_pad(c("12", "1234"), 3)
## [1] " 12"  "1234"

可以用选项side选择在哪里填补空格, 默认为"left", 还可选"right""both"

stringr::str_wrap()可以将作为字符型向量的长字符串拆分成近似等长的行, 行之间用换行符分隔。

41.1.6.4 排序

基本R函数sort()可以用来对字符型向量的各个元素按照字典序排序, 但是字符的先后顺序是按照操作系统的当前编码值次序, 见关于locales的帮助。

str_sort(x)对字符型向量x排序。 可以用locale选项指定所依据的locale, 不同的locale下次序不同。 默认为"en"即英语, 中国大陆的GB编码(包括GBK和GB18030)对应的locale是"zh"

str_order(x)返回将x的各个元素从小到大排序的下标序列。

41.1.7 简单匹配与查找

41.1.7.1 开头和结尾匹配

基本R的startsWith(x, prefix)可以判断字符型向量x的每个元素是否以prefix开头, 结果为一个与x长度相同的逻辑型向量。如

startsWith(c("xyz123", "tu004"), "tu")
## [1] FALSE  TRUE

endsWith(x, suffix)可以判断字符型向量x的每个元素是否以suffix结尾, 如

endsWith(c("xyz123", "tu004"), "123")
## [1]  TRUE FALSE

stringr包的str_starts(string, pattern)判断string的每个元素是否以模式pattern开头, 加选项negate=TRUE表示输出反面结果。 pattern是正则表达式, 如果需要用非正则表达式,可以用fixed()或者coll()保护,如:

str_starts(c("xyz123", "tu004"), fixed("tu"))
## [1] FALSE  TRUE
str_starts(c("xyz123", "tu004"), coll("tu"))
## [1] FALSE  TRUE

stringr包的str_ends(string, pattern)判断是否以给定模式结尾。

41.1.7.2 中间匹配

函数grep(), grepl()等可以用于查找子字符串, 位置不限于开头和结尾, 详见“正则表达式”章节。

grepl()函数中加fixed=TRUE选项表示查找一般文本内容(非正则表达式)。 比如,查找字符串中是否含有our:

grepl("our", c("flavor", "tournament"), fixed=TRUE)
## [1] FALSE  TRUE

41.1.8 字符串替换

gsub(pattern, replacement, x, fixed=TRUE) 把字符型向量x中每个元素中出现的子串 pattern都替换为replacement。 如

gsub("the", "**",
     c("New theme", "Old times", "In the present theme"),
     fixed=TRUE)
## [1] "New **me"           "Old times"          "In ** present **me"

设有些应用程序的输入要求使用逗号“,”分隔, 但是用户可能输入了中文逗号“,”, 就可以用gsub()来替换:

x <- c("15.34,14.11", "13.25,16.92")
x <- gsub(",", ",", x, fixed=TRUE); x
## [1] "15.34,14.11" "13.25,16.92"

例子中x的第二个元素中的逗号是中文逗号。

函数sub()gsub()类似,但是仅替换第一次出现的pattern

41.1.9 字符串拆分

stringr::str_split(string, pattern)对字符型向量string的每一个元素按分隔符pattern进行拆分, 每个元素拆分为一个字符型向量,结果是一个列表,列表元素为字符型向量。 其中pattern是正则表达式, 为了按照固定模式拆分,用fixed()进行保护。如

x <- c("11,12", "21,22,23", "31,32,33,34")
res1 <- str_split(x, fixed(","))
res1
## [[1]]
## [1] "11" "12"
## 
## [[2]]
## [1] "21" "22" "23"
## 
## [[3]]
## [1] "31" "32" "33" "34"

str_split()可以用选项n指定仅拆分出成几项,最后一项合并不拆分,如:

x <- c("11,12", "21,22,23", "31,32,33,34")
res2 <- str_split(x, fixed(","), n=2)
res2
## [[1]]
## [1] "11" "12"
## 
## [[2]]
## [1] "21"    "22,23"
## 
## [[3]]
## [1] "31"       "32,33,34"

拆分的结果可以用lapply(), sapply()vapply()等函数处理。 例如, 将每个元素的拆分结果转换成数值型:

lapply(res1, as.numeric)
## [[1]]
## [1] 11 12
## 
## [[2]]
## [1] 21 22 23
## 
## [[3]]
## [1] 31 32 33 34

可以用unlist()函数将列表中的各个向量连接成一个长向量,如:

unlist(res1)
## [1] "11" "12" "21" "22" "23" "31" "32" "33" "34"

注意,即使输入只有一个字符串,str_split()的结果也是列表, 所以输入只有一个字符串时我们应该取出结果列表的第一个元素,如

strsplit("31,32,33,34", split=",", fixed=TRUE)[[1]]
## [1] "31" "32" "33" "34"

如果确知每个字符串拆分出来的字符串个数都相同, 可以用stringr::str_split_fixed(), 用参数n指定拆出来的项数, 这时结果为一个字符型矩阵, 原来的每个元素变成结果中的一行:

x <- c("11,12", "21,22", "31,32")
res3 <- str_split_fixed(x, fixed(","), n=2)
res3
##      [,1] [,2]
## [1,] "11" "12"
## [2,] "21" "22"
## [3,] "31" "32"

基本R的strsplit(x,split,fixed=TRUE) 可以把字符型向量x的每一个元素按分隔符split拆分为一个字符型向量, strsplit的结果为一个列表, 每个列表元素对应于x的每个元素。

x <- c("11,12", "21,22,23", "31,32,33,34")
res4 <- strsplit(x, split=",", fixed=TRUE)
res4
## [[1]]
## [1] "11" "12"
## 
## [[2]]
## [1] "21" "22" "23"
## 
## [[3]]
## [1] "31" "32" "33" "34"

41.2 文本文件读写

文本文件是内容为普通文字、用换行分隔成多行的文件, 与二进制文件有区别, 二进制文件中换行符没有特殊含义, 而且二进制文件的内容往往也不是文字内容。 二进制文件的代表有图片、声音, 以及各种专用软件的的私有格式文件, 如Word文件、Excel文件。

对于文本文件,可以用readLines()函数将其各行的内容读入为一个字符型数组, 字符型数组的每一个元素对应于文件中的一行, 读入的字符型数组元素不包含分隔行用的换行符。

最简单的用法是读入一个本地的文本文件, 一次性读入所有内容,用如

lines <- readLines("filename.ext")

其中filename.ext是文件名, 也可以用全路径名或相对路径名。

当文本文件很大的时候, 整体读入有时存不下, 即使能存下处理速度也很慢, 可以一次读入部分行,逐批读入并且逐批处理,这样程序效率更高。 这样的程序要复杂一些,例如

infcon <- file("filename.ext", open="rt")
batch <- 1000
repeat{
  lines <- readLines(infcon, n=batch)
  if(length(lines)==0) break
  ## 处理读入的这些行
}
close(infcon)

以上程序先打开一个文件,inffcon是打开的文件的读写入口(称为一个“连接对象”)。 每次读入指定的行并处理读入的行,直到读入了0行为止, 最后关闭infcon连接。

对文本文件的典型处理是读入后作一些修改, 另外保存。 函数writeLines(lines, con="outfilename.txt")可以将字符型向量lines的各个元素变成输出文件的各行保存起来, 自动添加分隔行的换行符。 如果是分批读入分批处理的, 则写入也需要分批写入, 以上的分批处理程序变成:

infcon <- file("filename.ext", open="rt")
outfcon <- file("outfilename.txt", open="wt")
batch <- 1000
while(TRUE){
  lines <- readLines(infcon, n=batch)
  if(length(lines)==0) break
  ## 处理读入的这些行, 变换成outlines
  writeLines(outlines, con=outfcon)
}
close(outfcon)
close(infcon)

readLines()也可以直接读取网站的网页文件, 如

lines <- readLines(url("https://www.r-project.org/"))
length(lines)
## [1] 116
head(lines)
## [1] "<!DOCTYPE html>"                                                             
## [2] "<html lang=\"en\">"                                                          
## [3] "  <head>"                                                                    
## [4] "    <meta charset=\"utf-8\">"                                                
## [5] "    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">"               
## [6] "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"

readr包的read_lines()write_lines()函数起到与基本R中 readLines()writeLines()类似的作用, read_file()read_file_raw()可以将整个文件读入为一个字符串。

关于读写文件时的编码问题, 详见15.5

41.3 正则表达式

在对字符串进行查找或替换时, 有时要查找替换的不是固定的子串而是某种模式。 比如,要查找或替换连续的三个数字,正文中的电子邮件地址, 网址,电话号码,等等。 正则表达式(regular expressions)用于表示各种复杂模式。 基本R中的正则表达式规则可以用POSIX 1003.2标准或者Perl规则。 建议使用perl语言的正则表达式, 在基本R的有关函数中规定参数perl=TRUE

stringr包提供了更方便的正则表达式功能, 其正则表达式规则是ICU正则表达式规则, 针对UTF-8编码的文本数据, 基本与perl规则兼容。

在正则表达式的模式(pattern)中,

.*+?{}\[]^$()

等字符是特殊字符,有特殊的解释。 除了\之外的其它12个都称为“元字符”(meta characters)。

在R语言中使用正则表达式时, 需要注意R字符型常量中一个\要写成两个。

41.3.1 字面匹配与匹配显示

如果模式中不含特殊字符,匹配为原样的子串。也叫做字面(literal)匹配。 stringr包提供了定义正则表达式、匹配正则表达式、按正则表达式替换、抽取匹配结果、用富文本显示匹配结果等强大功能, 其中str_view()函数可以在HTML输出中或者在RStudio软件中用富文本显示匹配结果, 在源数据中加亮显示匹配。 注意, 如果你现在看的是PDF文件, 结果可能无法显示。

在使用rmarkdown、bookdown创作文章和书籍时, 因为同一源文件需要能同时支持HTML、LaTeX转换PDF, 但str_view()等函数仅支持HTML, 所以需要进行设置。 可以在Rmd源文件开头运行命令:

is_html <- knitr::opts_knit$get("rmarkdown.pandoc.to") == "html"

这可以定义一个变量is_html, 仅在输出格式为HTML时才为TRUE, 然后在包含特殊HTML显示的代码段选项中, 加选项eval = is_html

下面的程序在字符型向量x的三个字符串元素中原因查找子字符串"the"并加亮显示:

x <- c("New theme", "Old times", "In the present theme")
str_view(x, "the")

在RStudio中会在Viewer窗格显示匹配结果, 匹配内容被高亮显示。

源数据中的第三项实际上有两处"the"出现但结果只显示了第一处。 用str_view_all()查看所有匹配,如:

x <- c("New theme", "Old times", "In the present theme")
str_view_all(x, "the")

41.3.2 不区分大小写匹配和regex函数

str_view(string, pattern)中的pattern应该为正则表达式类型, 如果输入了字符串, 会自动被函数regex()转换成正则表达式类型。 正则表达式的模式一般是区分大小写的, 通过在regex()函数中加选项ignore_case=TRUE可以进行不区分大小写的匹配; 在模式前面附加(?i)前缀式选项也可以实现不区分大小写匹配。 如

str_view_all(c("Dr. Wang", "DR. WANG", "dR. W.R."), "Dr")
str_view_all(c("Dr. Wang", "DR. WANG", "dR. W.R."), 
             regex("Dr", ignore_case=TRUE))
str_view_all(c("Dr. Wang", "DR. WANG", "dR. W.R."), "(?i)Dr")

41.3.3 用句点匹配单个字符

在模式中用“.”匹配任意一个字符(除了换行符"\n",能否匹配此字符与选项有关)。 如

s <- c("abc", "cabs", "lab")
str_view_all(s, "ab.")

像句点这样的字符称为元字符(meta characters), 在正则表达式中有特殊作用。 如果需要匹配句点本身,用“[.]”或者“\.”表示。 比如,要匹配a.txt这个文件名,如下做法有错误:

str_view_all(c("a.txt", "a0txt"), "a.txt")

结果连a0txt也匹配了。用“[.]”表示句点则将句点不做特殊解释:

str_view_all(c("a.txt", "a0txt"), "a[.]txt")
str_view_all(c("a.txt", "a0txt"), "a\\.txt")

注意在R语言字符型常量中一个\需要写成两个。

如果仅需按照原样进行查找, 也可以将pattern的字符串用fixed()函数保护,如: 如

str_view_all(c("a.txt", "a0txt"), fixed("a.txt"))

41.3.4 匹配一组字符中的某一个

模式中使用方括号给定一个字符类, 单个字符与字符类中任何一个字符相同都算是匹配成功。 比如,模式“[ns]a.[.]xls” 表示匹配的第一个字符是ns, 第二个字符是a,第三个字符任意,第四个字符是句点, 然后是xls。 测试:

str_view_all(c("sa1.xls", "dna2.xlss", "nat.xls"), "[ns]a.[.]xls")

注意匹配并不需要从开头匹配到结尾, 中间匹配是允许的,类似于搜索符合某种规律的子串。 在上例中第二个元素是从第二个字符开始匹配的,也没有匹配到末尾。

例:模式[Rr]eg[Ee]x可以匹配RegExRegexregexregEx

如果希望完全忽略大小写进行匹配, 可以使用在regex()等函数中指定ignore_case=TRUE选项, 或在模式字符串的最前面添加(?i)选项。

在“[]”中允许用-表示一个范围。 如[a-z]匹配小写英文字母, [A-Z]匹配大写英文字母, [a-zA-Z]匹配大小写的英文字母, [a-zA-Z0-9]匹配大小写的英文字母和数字。

为了匹配一个16进制数字, 可以用[0-9A-Fa-f]

例:模式“[ns]a[0-9][.]xls”要求匹配的第三个字符为数字。

str_view_all(c("sa1.xls", "dna2.xlss", "nat.xls"), "[ns]a[0-9][.]xls")

在方括号内第一个位置的^表示对指定的范围取余集。 例如,模式[ns]a[^0-9][.]xls要求匹配的第三个字符不能为数字:

str_view_all(c("sa1.xls", "dna2.xlss", "nat.xls"), "[ns]a[^0-9][.]xls")

41.3.5 原样匹配元字符

元字符(meta characters)是在正则表达式中有特殊含义的字符。 比如句点可以匹配任意一个字符, 左方括号代表字符集合的开始。 所以元字符不能直接匹配自身, 可以用“[.]”匹配一个句点。 为匹配左方括号,在前面加上转义字符\变成\[, 但是在R字符串中一个\必须用\\表示, 所以模式“\[”在R中写成字符串常量, 必须写成"\\["。 其它的元字符如果要原样匹配也可以在前面加上转义字符\, 比如匹配\本身可以用\\,但是在R字符型常量中需要写成"\\\\"

例,匹配x[5],因为[是元字符,需要写成:

str_view_all(c("int x;", "int x[5]"), "int x\\[5\\]")

Perl中允许用“[[]”表示“[”, 用“[]]”表示“]”, 但stringr包不支持这种做法。

41.3.6 匹配空白

表示空白的元字符有:

\f 换页符
\n 换行符
\r 回车符
\t 制表符
\v 垂直制表符

不同操作系统的文本文件的行分隔符不同, 为了匹配Windows格式的文本文件中的空行, 用“\r\n\r\n”; 为了匹配Unix格式的文本文件中的空行则用“\r\r”。 写成R的字符型常量时, 这些表示本身也是R的相应字符的表示, 所以在R字符型常量中这些字符不需要用两个\表示一个\

匹配任意一个空白字符用“\s”, 这等价于“[ \f\n\r\t\v]”。 大写的“\S”则匹配任意一个非空白的字符。

41.3.7 匹配数字

\d匹配一个数字,相当于[0-9]。 用\D匹配一个非数字。 如

str_view_all(c("n1.xls", "na.xls"), "n\\d[.]xls")

41.3.8 匹配字母、数字、下划线

匹配字母、数字、下划线字符用\w(小写), 等价于[a-zA-Z0-9_]\W(大写)匹配这些字符以外的字符。 如

str_view_all(c("file-s1.xls", "s#.xls"), "s\\w[.]")

可以看出,模式匹配了s1.而没有匹配s#.

41.3.9 十六进制和八进制数

在模式中可以用十六进制数和八进制数表示特殊的字符。 十六进制数用\X引入, 比如\X0A对应\n字符。 八进制数用\0引入, 比如\011表示\t字符。

例如,str_view_all("abc\nefg\n", "\\x0A")可以匹配两个换行符。

41.3.10 POSIX字符类

\d, \w这样的字符类不方便用在方括号中组成字符集合, 而且也不容易记忆和认读。 在模式中方括号内可以用[:alpha:] 表示任意一个字母。 比如,[[:alpha:]]匹配任意一个字母(外层的方括号表示字符集合, 内层的方括号是POSIX字符类的固有界定符)。

这样的POSIX字符类有:

  • [:alpha:]表示任意一个字母;
  • [:lower:]为小写字母;
  • [:upper:]为大写字母;
  • [:digit:]为数字;
  • [:xdigit:]为十六进制数字。
  • [:alnum:]为字母数字(不包括下划线);
  • [:blank:]为空格或制表符;
  • [:space:]为任何一种空白字符,包括空格、制表符、换页符、换行符、回车符;
  • [:print:]为可打印字符;
  • [:graph:][:print:]一样但不包括空格;
  • [:punct:][:print:]中除[:alnum:]和空白以外的所有字符;

例如:

str_view_all(c("x1", "_x", ".x", ".1"), "[[:alpha:]_.][[:alnum:]_.]")

模式匹配长度为2的字符串, 第一个字符是字母、下划线或者小数点, 第二个字符是字母、数字、下划线或者小数点。 这个模式试图匹配由两个字符组成的合法R变量名, 但是最后一个非变量名.1也被匹配了。 解决这样的问题可以采用后面讲到的|备择模式。

41.3.11 匹配开头和末尾

模式匹配相当于在字符串内部搜索某种模式, 如果要从字符串开头匹配, 在模式中取第一个模式规定为^\A。 如果模式中最后一个字符是$\Z, 则需要匹配到字符串末尾。 用\Z匹配字符串末尾时如果末尾有一个换行符则匹配到换行符之前。

str_view_all(c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), "^n\\d[.]xls$")
str_view_all(c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), "\\An\\d[.]xls\\Z")

只匹配了第一个输入字符串。

有时候源文本的每个字符串保存了一个文本文件内容, 各行用\n分隔, 后面将给出匹配每行的行首与行尾的方法。

41.3.12 单词边界

\b匹配单词边界, 这样可以查找作为单词而不是单词的一部分存在的内容。 \B匹配非单词边界。 如

str_view_all(c("a cat meaos", "the category"), "\\bcat\\b")

41.3.13 重复匹配

41.3.13.1 加号重复匹配

模式中在一个字符或字符集合后加后缀+表示一个或多个前一字符。 比如

str_view_all(c("sa1", "dsa123"), "sa[[:digit:]]+")

例如,匹配电子邮件地址:

str_view_all("abc123@efg.com", 
             "^[[:alnum:]_]+@[[:alnum:]_]+[.][[:alnum:]_]+$")

匹配的电子邮件地址在@前面可以使用任意多个字母、数字、下划线, 在@后面由小数点分成两段, 每段可以使用任意多个字母、数字、下划线。 这里用了^$表示全字符串匹配。

41.3.13.2 星号和问号重复匹配

在一个字符或字符集合后加后缀*表示零个或多个前一字符, 后缀?表示零个或一个前一字符。

比如, ^https?://[[:alnum:]./]+$可以匹配http或https开始的网址。 如

str_view_all(c("http://www.163.net", "https://123.456."),
             "^https?://[[:alnum:]_./]+$")

(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)

x[[:digit:]]*能匹配“x”, “x1,” “x123”这样的变量名,如:

str_view_all(c("x", "x1", "x123"), "x[[:digit:]]*")
str_view_all(c("x", "x1", "x123"), "x\\d*")

41.3.13.3 计数重复

问号可以表示零个或一个, 而加号、星号重复不能控制重复次数。 在后缀大括号中写一个整数表示精确的重复次数。 如

str_view_all(c("1", "12", "123", "1234"), "[[:digit:]]{3}")

模式匹配的是三位的数字。 因为没有要求从开头一直匹配到末尾, 所以三位以上数字也能匹配其中开始的三位。

可以在后缀大括号中指定重复的最小和最大次数, 中间用逗号分隔。 比如, 月日年的日期格式可以用

[[:digit:]]{1,2}[-/][[:digit:]]{1,2}[-/][[:digit:]]{2,4}

来匹配。 如 (注意这个模式还会匹配非日期)

pat <- paste0(
  "[[:digit:]]{1,2}[-/]",
  "[[:digit:]]{1,2}[-/]",
  "[[:digit:]]{2,4}")
str_view_all(c("2/4/1998", "13/15/198"), pat)

重复数允许指定为0。 重复数的逗号后面空置表示重复数没有上限。 例如,后缀{3,}表示前一模式必须至少重复3次。

41.3.14 贪婪匹配和懒惰匹配

无上限的重复匹配如*, +, {3,}等缺省是贪婪型的, 重复直到文本中能匹配的最长范围。 比如我们希望找出圆括号这样的结构, 很容易想到用\(.+\)这样的模式(注意圆括号是元字符,需要用反斜杠保护), 但是这不会恰好匹配一次, 模式会一直搜索到最后一个)为止。

例如:

str_view_all("(1st) other (2nd)", "\\(.+\\)")

我们本来期望的是提取两个“(1st)”和“(2nd)”组合, 不料整个地提取了“(1st) other (2nd)”。 这就是因为.+的贪婪匹配。

如果要求尽可能短的匹配, 使用*?, +?, {3,}?等“懒惰型”重复模式。 在无上限重复标志后面加问号表示懒惰性重复。

比如,上例中模式修改后得到了期望的结果:

str_view_all("(1st) other (2nd)", "\\(.+?\\)")

懒惰匹配会造成搜索效率降低, 应仅在需要的时候使用。

41.3.15 句点全匹配与多行模式

句点通配符一般不能匹配换行,如

str_view_all("(1,\n2)", "\\(.+?\\)")

跨行匹配失败。 一种办法是预先用str_replace_all()gsub()把所有换行符替换为空格。 但是这只能解决部分问题。

解决方法是在将模式用regex()保护并加选项dotall=TRUE, 或者在Perl正则表达式开头添加(?s)选项, 这样使得句点通配符可以匹配换行符, 称为句点全匹配模式。 如

str_view_all("(1,\n2)", regex("\\(.+?\\)", dotall=TRUE))
str_view_all("(1,\n2)", "(?s)\\(.+?\\)")

regex()函数中加选项multiline=TRUE, 或者在正则表达式开头用(?m)表示把整个输入字符串看成用换行符分开的多行。 这时^$匹配每行的开头和结尾, “每行”是指字符串中用换行符分开的各个字符子串。 (?s)(?m)可以同时使用。

例:

str_view_all("(1,2)\n(3,4)\n", "^\\(.+?\\)$")

元数据中包含两行内容, 结果没有能够匹配, 这是因为模式要求从整个字符串开头一直匹配到末尾。 增加multiline=TRUE或者(?m)选项则可以匹配两处:

str_view_all("(1,2)\n(3,4)\n", regex("^\\(.+?\\)$", multiline=TRUE))
str_view_all("(1,2)\n(3,4)\n", "(?m)^\\(.+?\\)$")

41.3.15.1 逐行处理

虽然正则表达式有多行和跨行选项, 但是当源数据很长时, 匹配效率会很低。

R的readLines()函数可以把一整个文本文件读成一个字符型向量, 每个元素为一行, 元素中不包含换行符。 R的字符型函数可以对这样的字符型向量每个元素同时处理, 也就实现了逐行处理。

如果字符串x中包含了一整个文本文件内容, 其中以\n分隔各行, 为了实现逐行处理, 可以先用str_split()函数拆分成不同行:

cl <- strs_plit(x, "\r?\n")[[1]]

结果将是一个字符型向量, 每个元素是原来的一行,最后一个元素是空字符串。 如

x <- c("This is first line.\nThis is second line.\n")
cl <- str_split(x, "\r?\n")[[1]]
cl
## [1] "This is first line."  "This is second line." ""

41.3.16 备择模式

如果有两种模式都算正确匹配,则用|连接这两个模式表示两者都可以。 例如,某个人的名字用James和Jim都可以, 表示为James|Jim, 如

str_view(c("James, Bond", "Jim boy"), "James|Jim")

两个字符的合法R变量名的匹配:

str_view_all(c("x1", "_x", ".x", ".1"), 
             "[[:alpha:]_][[:alnum:]_.]|[.][[:alpha:]_]")

41.3.17 分组与捕获

在正则表达式中用圆括号来分出组, 作用是

  • 确定优先规则
  • 组成一个整体
  • 拆分出模式中的部分内容(称为捕获)
  • 定义一段供后续引用或者替换。

圆括号中的模式称为子模式,或者捕获

在使用备择模式时,James|Jim是在单词James和Jim之间选择。 如果希望选择的是中间的ms和Ji怎么办? 可以将备择模式保护起来, 如Jam(es|Ji)m, 就可以确定备择模式的作用范围。

有时一个模式中部分内容仅用于定位, 而实际有用的内容是其中的一部分, 就可以将这部分有用的内容包在圆括号中作为一个捕获。

元字符问号、加号、星号、大括号等表示重复, 前面的例子中都是重复一个字符或者字符类。 如果需要重复由多个字符组成的模式, 如x[[:digit:]]{2}怎么办? 只要将该模式写在括号中,如:

str_view_all(c("x01x02", "_x11x9"), 
             "(x[[:digit:]]{2})+")

上例的元数据中, 第一个元素重复了两次括号中的模式, 第二个元素仅有一次括号中的模式。

注意: 用表示重复的元字符重复某个模式时, 从第二次开始, 并不是要重复前面的子字符串, 而是重复前面的模式。 比如上例中x01x02

如果想严格重复前面的某个子字符串怎么办?

分组是自动编号的, 以左开括号的序号为准(除了作为选项、有名捕获等开扩号以外)。 在替换或者向后引用时, 可以用\1\2等表示匹配中第一个开括号对应的分组, 第二个开扩号对应的分组,……。

在模式中可以用\1, \2等表示严格重复前面捕获的子字符串。 例如,([a-z]{3})\1这样的模式可以匹配如abcabc, uxzuxz这样的三字母重复:

str_view_all(c("abcabc", "aabbcc"), 
             "([a-z]{3})\\1")

又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:

str_view_all(c("2008-08-08", "2017-01-18"), 
             "\\d\\d(\\d\\d)-\\1-\\1")

stringr的函数str_replace_all(string, pattern, replacement)可以指定一个查找模式pattern, 替换模式replacement, 对字符型向量string中的每个元素进行替换。 replacement中的元字符没有特殊解释, 但是\1, \2等代表匹配的子模式(捕获)。

例:希望把带有前导零的数字的前导零删除,可以用如

x <- c("1204", "01204", "001204B")
pat <- "\\b0+([1-9][0-9]*)\\b"
repl <- "\\1"
str_view_all(x, pat)
str_replace_all(x, pat, repl)
## [1] "1204"    "1204"    "001204B"

上例的模式中的\b表示单词边界, 所以中间的0不会被当作前导零, 不是整个数字的也不会被修改。

上例中的str_view_all()仅用于调试目的, 在进行替换时不时必要步骤。

例:为了交换横纵坐标,可以用如下替换

x <- "1st: (5,3.6), 2nd: (2.5, 1.1)"
pat <- paste0(
  "[(]([[:digit:].]+),",
  "[[:space:]]*([[:digit:].]+)[)]", sep="")
repl <- "(\\2, \\1)"
str_view_all(x, pat)
str_replace_all(x, pat, repl)
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"

例: 要匹配yyyy-mm-dd这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:

x <- c("1998-05-31", "2017-01-14")
pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})"
repl <- "\\2/\\3/\\1"
str_view_all(x, pat)
str_replace_all(x, pat, repl)
## [1] "05/31/1998" "01/14/2017"

如果某个分组仅想起到分组作用但是不会提取具体的匹配内容也不会用该组内容做替换, 可以将该组变成“非捕获分组”, 办法是把表示分组开始左圆括号变成(?:三个字符。 这在用分组表示优先级时比较有用, 如"Jam(es|Ji)m"可以写成"Jam(?:es|Ji)m"。 非捕获分组在向后引用和替换时不计入\1\2这样的排列中。

比如,把1921-2020之间的世纪号删去,可以用

x <- c("1978", "2017", "2035")
pat <- "\\A(?:19|20)([0-9]{2})\\Z"
repl <- "\\1"
str_view_all(x, pat)
str_replace_all(x, pat, repl)
## [1] "78" "17" "35"

其中用了非捕获分组使得备择模式19|20优先匹配。 注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:

x <- c("1978", "2017", "2035")
pat1 <- "\\A19(2[1-9]|[3-9][0-9])\\Z"
pat2 <- "\\A20([01][0-9]|20)\\Z"
repl <- "\\1"
str_view_all(x, pat1)
str_view_all(x, pat2)
x %>%
  str_replace_all(pat1, repl) %>%
  str_replace_all(pat2, repl)
## [1] "78"   "17"   "2035"

这里用了stringr包重新定义的管道运算, 与magrittr包定义的管道运算作用相同, 可以将函数的第一个自变量自动输入给管道的下一个处理层级。

41.4 stringr包的正则表达式函数

41.4.1 str_view()函数

str_view(string, pattern)在RStudio中打开Viewer窗格, 显示pattern给出的正则表达式模式在string中的首个匹配。 string是输入的字符型向量。 用str_view_all()显示所有匹配。

如果要匹配的是固定字符串, 写成str_view(string, fixed(pattern))

如果要匹配的是单词等的边界, 模式用boundary()函数表示,如 str_view("a brown fox", boundary("word"))将匹配首个单词。

41.4.2 regex()函数

stringr包用到正则表达式模式的地方, 实际上应该写成regex(pattern), 只写模式本身是一种简写。 regex()函数可以指定ignore_case=TRUE要求不区分大小写, 指定multi_line=TRUE使得^$匹配用换行符分开的每行的开头和结尾, dotall=TRUE使得.能够匹配换行符。 comment=TRUE使得模式可以写成多行, 行尾的井号后面表示注释, 这时空格不再原样匹配, 为了匹配空格需要写在方括号内或者用反斜杠开头。

regex()类似的表示模式的函数有fixed()boundary()coll()

41.4.3 检查那些元素能够匹配

str_detect(string, pattern)返回字符型向量string的每个元素是否匹配pattern中的模式的逻辑型结果。 与基本R的grepl()作用类似。 如

x <- c("New theme", "Old times", "In the present theme")
str_view(x, "the")
str_detect(x, "the")
## [1]  TRUE FALSE  TRUE

上例中的str_view()仅用作调试目的。

str_which(string, pattern)返回字符型向量string的元素当中能匹配pattern中的模式的元素序号。 与基本R的grep()作用类似。 如

x <- c("New theme", "Old times", "In the present theme")
str_which(x, "the")
## [1] 1 3

str_count()则返回模式在每个元素中匹配的次数。 如

str_count(c("123,456", "011"), "[[:digit:]]")
## [1] 6 3

41.4.4 替换

stringr包的str_replace_all(string, pattern, replacement)在字符型向量string的每个元素中查找模式pattern, 并将所有匹配按照replacement进行替换。 在replacement可以用\1, \2中表示模式中的捕获, 除此之外元字符没有特殊作用。

基本R中gsub()有类似功能。

如:

str_replace_all(c("123,456", "011"), ",", "")
## [1] "123456" "011"

又如:

str_replace_all(c("123,456", "011"), 
                "([[:digit:]]+),([[:digit:]]+)", "\\2,\\1")
## [1] "456,123" "011"

注意源数据中第二个元素因为不能匹配所以就原样返回了, 没有进行替换。

str_replace()则仅对输入字符型向量的每个元素中模式的第一次出现进行替换, 不如str_replace_all()常用。

41.4.5 返回匹配的元素

str_subset(string, pattern)返回字符型向量中能匹配pattern的那些元素组成的子集, 与基本R函数grep(pattern, string, value=TRUE)效果相同。 注意,返回的是整个元素而不是匹配的子串。

比如,查找人名中间有空格的:

str_view_all(c("[马思聪]", "[李  明]"), 
             "[[:alpha:]]+[[:space:]]+[[:alpha:]]+")
str_subset(c("[马思聪]", "[李  明]"), 
           "[[:alpha:]]+[[:space:]]+[[:alpha:]]+")
## [1] "[李  明]"

注意上例中仅返回了有匹配的元素, 而且是匹配元素的整个字符串而不是匹配的部分。

当要查找的内容是tibble的一列时, 用filter()str_detct()配合, 可以进行行子集选择。 比如,在数据框的人名中查找中间有空格的名字:

tibble(name=c("马思聪", "李  明")) %>%
  filter(str_detect(name, "[[:alpha:]]+[[:space:]]+[[:alpha:]]+"))
## # A tibble: 1 x 1
##   name  
##   <chr> 
## 1 李  明

41.4.6 提取匹配内容

str_subset()返回的是有匹配的源字符串, 而不是匹配的部分子字符串。 用str_extract(string, pattern)从源字符串中取出首次匹配的子串。

str_view_all("A falling ball", "all")
str_extract("A falling ball", "all")
## [1] "all"

str_extract_all(string, pattern)取出所有匹配子串, 结果是一个列表, 列表的每个元素对应于字符型向量string的每个元素, 结果列表的每个元素是一个字符型数组, 存放所有匹配的子字符串。 如:

x <- c("A falling ball", "Phone call.")
str_view_all(x, "all")
str_extract_all(x, "all")
## [[1]]
## [1] "all" "all"
## 
## [[2]]
## [1] "all"

str_extract_all()可以加选项simplyfy=TRUE, 使得返回结果变成一个字符型矩阵, 每行是原来一个元素中取出的各个子串, 列数等于最大匹配次数, 没有那么多匹配次数的填以空字符串。 如果正常匹配结果不会出现空字符就可以用这种方法简化结果的保存和访问。 如

x <- c("A falling ball", "Phone call.")
str_view_all(x, "all")
str_extract_all(x, "all", simplify=TRUE)
##      [,1]  [,2] 
## [1,] "all" "all"
## [2,] "all" ""

41.4.7 提取分组捕获内容

str_subset()提取的是能匹配模式的元素子集, 而不是匹配的模式或者捕获; str_extract()str_extract_all()提取的是每个元素的首次或者所有匹配的子字符串, 而不是其中的捕获。

str_match(string, pattern)提取每个元素的首次匹配内容以及其中各个捕获分组内容, 结果是一个矩阵, 每行对应于字符型向量string中的一个元素, 结果矩阵的每行的第一个元素是匹配内容,其它元素是各个捕获, 没有则为字符型缺失值(不是空字符串)。

比如,希望匹配中间有空格的人名并捕获空格前后部分:

str_match(c("马思聪", "李  明"), 
          "([[:alpha:]]+)[[:space:]]+([[:alpha:]]+)")
##      [,1]     [,2] [,3]
## [1,] NA       NA   NA  
## [2,] "李  明" "李" "明"

上例中源数据第一个元素没有匹配, 所以结果都是缺失值NA, 第二个元素的结果在第二行, 首先是整个匹配的子字符串, 然后是捕获的两个部分。

stringr::str_match_all(string, pattern)匹配每个字符串中所有出现位置, 结果是一个列表, 每个列表元素对应于输入的字符型向量string的每个元素, 结果中每个列表元素是一个字符型矩阵, 用来保存所有各个匹配以及匹配中的捕获, 每行是一个匹配的结果,首先是匹配结果,其次是各个捕获。 结果列表中每个作为列表元素的矩阵大小不一定相同。 当某个元素完全没有匹配时, 结果列表中对应元素是行数为0的矩阵。

比如,模式为19xx或者20xx的年份, 并将其分为前两位和后两位:

x <- c("1978-2000", "2011-2020-2099", "2100-2199")
pat <- "\\b(19|20)([0-9]{2})\\b"
str_view_all(x, pat)
mlist <- str_match_all(x,  pat); mlist
## [[1]]
##      [,1]   [,2] [,3]
## [1,] "1978" "19" "78"
## [2,] "2000" "20" "00"
## 
## [[2]]
##      [,1]   [,2] [,3]
## [1,] "2011" "20" "11"
## [2,] "2020" "20" "20"
## [3,] "2099" "20" "99"
## 
## [[3]]
##      [,1] [,2] [,3]

下面的程序合并上面提取的年份后两位为一个字符型向量:

ml <- Filter(function(m) nrow(m)>0, mlist)
ml <- Map(function(m) m[,3], ml)
ml <- Reduce(c, ml); ml
## [1] "78" "00" "11" "20" "99"

41.4.8 定位匹配位置

str_locate(string, pattern)对输入字符型向量string的每个元素返回首次匹配pattern的开始和结束位置。 输出结果是一个两列的矩阵,每行对应于输入的一个元素, 每行的两个元素分别是首次匹配的开始和结束字符序号(按字符计算)。如

x <- c("A falling ball", "Phone call.")
str_view_all(x, "all")
str_locate(x, "all")
##      start end
## [1,]     4   6
## [2,]     8  10

str_locate_all(string, pattern)则可以返回每个元素中所有匹配的开始和结束位置, 结果是一个列表, 每个列表元素对应于输入字符型向量的每个元素, 结果中每个列表元素是一个两列的数值型矩阵, 每行为一个匹配的开始和结束字符序号。如

x <- c("A falling ball", "Phone call.")
str_view_all(x, "all")
str_locate_all(x, "all")
## [[1]]
##      start end
## [1,]     4   6
## [2,]    12  14
## 
## [[2]]
##      start end
## [1,]     8  10

注意如果需要取出匹配的元素可以用str_subset(), 要取出匹配的子串可以用str_extract()str_extract_all(), 取出匹配的子串以及分组捕获可以用str_match()str_match_all()

41.5 利用基本R函数进行正则表达式处理

基本R函数grep, sub, gsub, regexpr, gregexpr, regexec中的 pattern参数可以是正则表达式, 这时应设参数 fixed=FALSEstrsplit函数中的参数split也可以是正则表达式。 regmatches函数从regexpr, gregexpr, regexec的结果中提取匹配的字符串。

以原样匹配为例。

x <- c("New theme", "Old times", "In the present theme")
regexpr("the", x, perl=TRUE)
## [1]  5 -1  4
## attr(,"match.length")
## [1]  3 -1  3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

这里使用了regexpr函数。 regexpr函数的一般用法为:

x <- c("New theme", "Old times", "In the present theme")
regexpr(pattern, text, ignore.case = FALSE, perl = FALSE,
        fixed = FALSE, useBytes = FALSE)

自变量为:

  • pattern 是一个正则表达式,如果用了fixed=TRUE选项,则当作普通原样文本来匹配;
  • text 是源字符串向量,要从其每个元素中查找pattern模式出现的位置;
  • ignore.case:是否要忽略大小写匹配;
  • perl 选择是否采用perl格式,如果不把pattern当作普通原样文本,应该选perl=TRUE,perl语言的正则表达式是事实上的标准,所以这样兼容性更好;
  • fixed 当fixed=TRUEpattern作为普通原样文本解释;
  • useBytes 为TRUE时逐字节进行匹配,否则逐字符进行匹配。之所以有这样的区别,是因为有些编码中一个字符由多个字节构成,BGK编码的汉字由两个字节组成,UTF-8编码的汉字也是由两个字节构成。

regexpr()函数返回一个整数值的向量, 长度与text向量长度相同, 结果的每个元素是在text的对应元素中pattern的首次匹配位置; 没有匹配时结果元素取-1。 结果会有一个match.length属性,表示每个匹配的长度, 无匹配时取-1。

如果仅关心源字符串向量text中哪些元素能匹配pattern, 可以用grep函数,如

x <- c("New theme", "Old times", "In the present theme")
grep("the", x, perl=TRUE)
## [1] 1 3

结果说明源字符串向量的三个元素中仅有第1、第3号元素能匹配。 如果都不匹配,返回integer(0)

grep可以使用与regexpr相同的自变量, 另外还可以加选项invert=TRUE,这时返回的是不匹配的元素的下标。

grep()如果添加选项value=TRUE, 则结果不是返回有匹配的元素的下标而是返回有匹配的元素本身(不是匹配的子串), 如

x <- c("New theme", "Old times", "In the present theme")
grep("the", x, perl=TRUE, value=TRUE)
## [1] "New theme"            "In the present theme"

grepl的作用与grep类似, 但是其返回值是一个长度与源字符串向量text等长的逻辑型向量, 每个元素的真假对应于源字符串向量中对应元素的匹配与否。如

x <- c("New theme", "Old times", "In the present theme")
grepl("the", x, perl=TRUE)
## [1]  TRUE FALSE  TRUE

就像grep()grepl()本质上给出相同的结果,只是结果的表示方式不同, regexec()regexpr()也给出仅在表示方式上有区别的结果。 regexpr()主要的结果是每个元素的匹配位置, 用一个统一的属性返回各个匹配长度; regexec()则返回一个与源字符串向量等长的列表, 列表的每个元素为匹配的位置,并且列表的每个元素有匹配长度作为属性。 所以,这两个函数只需要用其中一个就可以,下面仅使用regexpr()regexec()的使用效果如

x <- c("New theme", "Old times", "In the present theme")
regexec("the", x, perl=TRUE)
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## 
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## 
## [[3]]
## [1] 4
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"

grep(), grepl(), regexpr(), regexec()都只能找到源字符串向量的每个元素中模式的首次匹配, 不能找到所有匹配。 gregexpr()函数可以找到所有匹配。 如

x <- c("New theme", "Old times", "In the present theme")
gregexpr("the", x, perl=TRUE)
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## 
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## 
## [[3]]
## [1]  4 16
## attr(,"match.length")
## [1] 3 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

其结果是一个与源字符串向量等长的列表, 格式与regexec()的结果格式类似, 列表的每个元素对应于源字符串向量的相应元素, 列表元素值为匹配的位置, 并有属性match.length保存了匹配长度。 匹配位置和匹配长度包含了所有的匹配, 见上面例子中第三个元素的匹配结果。

函数grep, grepl结果仅给出每个元素能否匹配。 regexpr(), regexec(), gregexpr()则包含了匹配位置与匹配长度, 这时,可以用regmatches()函数取出具体的匹配字符串。 regmatches()一般格式为

regmatches(x, m, invert = FALSE)

其中x是源字符串向量, mregexpr()regexec()gregexpr()的匹配结果。 如

x <- c("New theme", "Old times", "In the present theme")
m <- regexpr("the", x, perl=TRUE)
regmatches(x, m)
## [1] "the" "the"

可以看出,regmatches()仅取出有匹配时的匹配内容, 无匹配的内容被忽略。

取出多处匹配的例子如:

x <- c("New theme", "Old times", "In the present theme")
m <- gregexpr("the", x, perl=TRUE)
regmatches(x, m)
## [[1]]
## [1] "the"
## 
## [[2]]
## character(0)
## 
## [[3]]
## [1] "the" "the"

regmatches()第二个自变量是gregexpr()的结果时, 其输出结果变成一个列表, 并且不再忽略无匹配的元素, 无匹配元素对应的列表元素为character(0), 即长度为零的字符型向量。 对有匹配的元素, 对应的列表元素为所有的匹配字符串组成的字符型向量。

实际上, 如果pattern中没有正则表达式, grep(), grepl(), regexpr(), gregexpr() 中都可以用fixed=TRUE参数取代perl=TRUE参数, 这时匹配总是解释为原样匹配, 即使pattern中包含特殊字符也是进行原样匹配。

41.5.1 不区分大小写匹配

在基本R中, 为了不区分大小写匹配, 可以在grep等函数调用时加选项ignore.case=TRUE; 如

grep("Dr", c("Dr. Wang", "DR. WANG", "dR. W.R."))
## [1] 1
grep("dr", c("Dr. Wang", "DR. WANG", "dR. W.R."), ignore.case=TRUE)
## [1] 1 2 3
grep("(?i)dr", c("Dr. Wang", "DR. WANG", "dR. W.R."))
## [1] 1 2 3

41.5.2 匹配单个字符

在模式中用“.”匹配任意一个字符(除了换行符"\n",能否匹配此字符与选项有关)。如

s <- c("abc", "cabs", "lab")
mres <- regexpr("ab.", s, perl=TRUE); mres
## [1]  1  2 -1
## attr(,"match.length")
## [1]  3  3 -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

regexpr仅给出每个元素中模式的首次匹配位置而不是给出匹配的内容。 regmatches函数以原始字符型向量和匹配结果为输入, 结果返回每个元素中匹配的各个子字符串(不是整个元素),如:

regmatches(s, mres)
## [1] "abc" "abs"

注意返回结果和输入字符型向量元素不是一一对应的,仅返回有匹配的结果。

像句点这样的字符称为元字符(meta characters), 在正则表达式中有特殊函数。 如果需要匹配句点本身,用“[.]”或者“\.”表示。 比如,要匹配a.txt这个文件名,如下做法有错误:

grep("a.txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1 2

结果连a0txt也匹配了。用“[.]”表示句点则将句点不做特殊解释:

grep("a[.]txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1
grep("a\\.txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1

注意在R语言字符型常量中一个\需要写成两个。

如果仅需按照原样进行查找, 也可以在grep(), grepl()regexpr()gregexpr()等函数中加选项fixed=TRUE, 这时不要再用perl=TRUE选项。 如

grep("a.txt", c("a.txt", "a0txt"), fixed=TRUE)
## [1] 1

41.5.3 匹配一组字符中的某一个

模式“[ns]a.[.]xls” 表示匹配的第一个字符是ns, 第二个字符是a,第三个字符任意,第四个字符是句点, 然后是xls。 例:

regexpr("[ns]a.[.]xls", c("sa1.xls", "dna2.xlss", "na3.xls"), perl=T)
## [1] 1 2 1
## attr(,"match.length")
## [1] 7 7 7
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

41.5.4 原样匹配元字符

例:

grep("int x\\[5\\]", c("int x;", "int x[5]"), perl=TRUE)
## [1] 2

也可以用“[[]”表示“[”, 用“[]]”表示“]”,如

grep("int x[[]5[]]", c("int x;", "int x[5]"), perl=TRUE)
## [1] 2

41.5.5 匹配数字

例:

grep("n\\d[.]xls", c("n1.xls", "na.xls"), perl=TRUE)
## [1] 1

41.5.6 匹配开头和末尾

例:

grep("^n\\d[.]xls$", c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), perl=TRUE)
## [1] 1
grep("\\An\\d[.]xls\\Z", c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), perl=TRUE)
## [1] 1

只匹配了第一个输入字符串。

41.5.7 匹配字母、数字、下划线

例:

m <- regexpr("s\\w[.]", c("file-s1.xls", "s#.xls"), perl=TRUE)
regmatches(c("file-s1.xls", "s#.xls"), m)
## [1] "s1."

可以看出,模式匹配了s1.而没有匹配s#.

41.5.8 十六进制和八进制数

例如

gregexpr("\\x0A", "abc\nefg\n")[[1]]
## [1] 4 8
## attr(,"match.length")
## [1] 1 1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

匹配了两个换行符。

41.5.9 POSIX字符类

例如:

grep("[[:alpha:]_.][[:alnum:]_.]", c("x1", "_x", ".x", ".1"))
## [1] 1 2 3 4

41.5.10 加号重复匹配

s <- c("sa1", "dsa123")
mres <- regexpr("sa[[:digit:]]+", s, perl=TRUE)
regmatches(s, mres)
## [1] "sa1"   "sa123"

例如:

p <- "^[[:alnum:]_]+@[[:alnum:]_]+[.][[:alnum:]_]+$"
x <- "abc123@efg.com"
m <- regexpr(p, x, perl=TRUE)
regmatches(x, m)
## [1] "abc123@efg.com"

匹配的电子邮件地址在@前面可以使用任意多个字母、数字、下划线, 在@后面由小数点分成两段, 每段可以使用任意多个字母、数字、下划线。 这里用了^$表示全字符串匹配。

41.5.11 星号和问号重复匹配

^https?://[[:alnum:]./]+$可以匹配http或https开始的网址。 如

s <- c("http://www.163.net", "https://123.456.")
grep("^https?://[[:alnum:]_./]+$", s, perl=TRUE)
## [1] 1 2

(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)

41.5.12 计数重复

例:

grep("[[:digit:]]{3}", c("1", "12", "123", "1234"))
## [1] 3 4

模式匹配的是三位的数字。

日期匹配例:

pat <- paste(
  c("[[:digit:]]{1,2}[-/]",
    "[[:digit:]]{1,2}[-/]",
    "[[:digit:]]{2,4}"), collapse="")
grep(pat, c("2/4/1998", "13/15/198"))
## [1] 1 2

41.5.13 贪婪匹配和懒惰匹配

例如:

s <- "<B>1st</B> other <B>2nd</B>"
p1 <- "<[Bb]>.*</[Bb]>"
m1 <- regexpr(p1, s, perl=TRUE)
regmatches(s, m1)[[1]]
## [1] "<B>1st</B> other <B>2nd</B>"

我们本来期望的是提取第一个“<B>……</B>”组合, 不料提取了两个“<B>……</B>”组合以及中间的部分。

比如,上例中模式修改后得到了期望的结果:

s <- "<B>1st</B> other <B>2nd</B>"
p2 <- "<[Bb]>.*?</[Bb]>"
m2 <- regexpr(p2, s, perl=TRUE)
regmatches(s, m2)[[1]]
## [1] "<B>1st</B>"

41.5.14 单词边界

例:

grep("\\bcat\\b", c("a cat meaos", "the category"))
## [1] 1

41.5.15 句点全匹配与多行模式

句点通配符一般不能匹配换行,如

s <- "<B>1st\n</B>\n"
grep("<[Bb]>.*?</[Bb]>", s, perl=TRUE)
## integer(0)

跨行匹配失败。而在HTML的规范中换行是正常的。 一种办法是预先用gsub把所有换行符替换为空格。 但是这只能解决部分问题。

另一方法是在Perl正则表达式开头添加(?s)选项, 这个选项使得句点通配符可以匹配换行符。 如

s <- "<B>1st\n</B>\n"
mres <- regexpr("(?s)<[Bb]>.*?</[Bb]>", s, perl=TRUE)
regmatches(s, mres)
## [1] "<B>1st\n</B>"

多行模式例:

s <- "<B>1st\n</B>\n"
mres1 <- gregexpr("^<.+?>", s, perl=TRUE)
mres2 <- gregexpr("(?m)^<.+?>", s, perl=TRUE)
regmatches(s, mres1)[[1]]
## [1] "<B>"
regmatches(s, mres2)[[1]]
## [1] "<B>"  "</B>"

字符串s包含两行内容,中间用\n分隔。 mres1的匹配模式没有打开多行选项, 所以模式中的^只能匹配s中整个字符串开头。 mres2的匹配模式打开了多行选项, 所以模式中的^可以匹配s中每行的开头。

41.5.16 备择模式

例如,某个人的名字用James和Jim都可以, 表示为James|Jim, 如

s <- c("James, Bond", "Jim boy")
pat <- "James|Jim"
mres <- gregexpr(pat, s, perl=TRUE)
regmatches(s, mres)
## [[1]]
## [1] "James"
## 
## [[2]]
## [1] "Jim"

41.5.17 分组与捕获

例: 希望把“<B>……</B”两边的“<B>”和“</B>”删除, 可以用如下的替换方法:

x <- "<B>1st</B> other <B>2nd</B>"
pat <- "(?s)<[Bb]>(.+?)</[Bb]>"
repl <- "\\1"
gsub(pat, repl, x, perl=TRUE)
## [1] "1st other 2nd"

替换模式中的\1(写成R字符型常量时\要写成\\)表示第一个圆括号匹配的内容, 但是表示选项的圆括号((?s))不算在内。

例:希望把带有前导零的数字的前导零删除,可以用如

x <- c("123", "0123", "00123")
pat <- "\\b0+([1-9][0-9]*)\\b"
repl <- "\\1"
gsub(pat, repl, x, perl=TRUE)
## [1] "123" "123" "123"

其中的\b模式表示单词边界, 这可以排除在一个没有用空格或标点分隔的字符串内部拆分出数字的情况。

例:为了交换横纵坐标,可以用如下替换

s <- "1st: (5,3.6), 2nd: (2.5, 1.1)"
pat <- paste0(
  "[(]([[:digit:].]+),",
  "[[:space:]]*([[:digit:].]+)[)]")
repl <- "(\\2, \\1)"
gsub(pat, repl, s, perl=TRUE)
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"

例如,要匹配yyyy-mm-dd这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:

pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})"
repl <- "\\2/\\3/\\1"
gsub(pat, repl, c("1998-05-31", "2017-01-14"))
## [1] "05/31/1998" "01/14/2017"

分组除了可以做替换外, 还可以用来表示模式中的重复出现内容。 例如,([a-z]{3})\1这样的模式可以匹配如abcabc, uxzuxz这样的三字母重复。如

grep("([a-z]{3})\\1", c("abcabc", "aabbcc"))
## [1] 1

又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:

x <- c("2008-08-08", "2017-01-18")
m <- regexpr("\\d\\d(\\d\\d)-\\1-\\1", x)
regmatches(x, m)
## [1] "2008-08-08"

下面是一个非捕获分组示例。 设需要把1921-2020之间的世纪号删去,可以用

pat <- "\\A(?:19|20)([0-9]{2})\\Z"
repl <- "\\1"
x <- c("1978", "2017", "2035")
gsub(pat, repl, x, perl=TRUE)
## [1] "78" "17" "35"

其中用了非捕获分组使得备择模式19|20优先匹配。 注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:

x <- c("1978", "2017", "2035")
p1 <- "\\A19(2[1-9]|[3-9][0-9])\\Z"
r1 <- "\\1"
p2 <- "\\A20([01][0-9]|20)\\Z"
x <- gsub(p1, r1, x, perl=TRUE)
x <- gsub(p2, r1, x, perl=TRUE)
x
## [1] "78"   "17"   "2035"

41.6 正则表达式应用例子

41.6.1 数据预处理

在原始数据中, 经常需要审核数据是否合法, 已经把一些常见错误输入自动更正。 这都可以用正则表达式实现。

41.6.1.1 除去字符串开头和结尾的空格

函数stringr::str_trim()trimws()可以除去字符串开头与结尾的空格, 也可以仅除去开头或仅除去结尾的空格。

这个任务如果用正则表达式字符串替换函数来编写,可以写成:

### 把字符串向量x的元素去除首尾的空白。
strip <- function(x){
  x <- str_replace_all(x, "^[[:space:]]+", "")
  x <- str_replace_all(x, "[[:space:]]+$", "")
  x
}

或者

### 把字符串向量x的元素去除首尾的空白。
strip <- function(x){
  x <- gsub("^[[:space:]]+", "", x, perl=TRUE)
  x <- gsub("[[:space:]]+$", "", x, perl=TRUE)
  x
}

这个版本可以除去包括空格在内的所有首尾空白字符。

41.6.1.2 除去字符串向量每个元素中所有空格

compress <- function(x){
  str_replace_all(x, " ", "")
}

或者

compress <- function(x){
  gsub(" ", "", x, fixed=TRUE)
}

这可以解决"李明""李 明"不相等这样的问题。 类似的程序也可以用来把中文的标点替换成英文的标点。

41.6.1.3 判断日期是否合法

设日期必须为yyyy-mm-dd格式, 年的数字可以是两位、三位、四位, 程序为:

is_yyyymmdd <- function(x){
  pyear <- "([0-9]{2}|[1-9][0-9]{2}|[1-2][0-9]{3})"
  pmon <- "([1-9]|0[1-9]|1[0-2])"
  pday <- "([1-9]|0[1-9]|1[0-9]|2[0-9]|3[01])"
  pat <- paste("\\A", pyear, "-", pmon, "-", pday, "\\Z", sep="")
  str_detect(x, pat)
}

is.yyyymmdd <- function(x){
  pyear <- "([0-9]{2}|[1-9][0-9]{2}|[1-2][0-9]{3})"
  pmon <- "([1-9]|0[1-9]|1[0-2])"
  pday <- "([1-9]|0[1-9]|1[0-9]|2[0-9]|3[01])"
  pat <- paste("\\A", pyear, "-", pmon, "-", pday, "\\Z", sep="")
  grepl(pat, x, perl=TRUE)
}

这样的规则还没有排除诸如9月31号、2月30号这样的错误。

例:

x <- c("49-10-1", "1949-10-01", "532-3-15", "2015-6-1", 
       "2017-02-30", "2017-13-11", "2017-1-32")
is_yyyymmdd(x)
## [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE

注意错误的2月30号没有识别出来。

41.6.1.4 把字符型日期变成yyyy-mm-dd格式。

make_date <- function(x){
  x %>%
    str_trim() %>%
    str_replace_all("[[:space:]]+", "-") %>%
    str_replace_all("/", "-") %>%
    str_replace_all("[.]", "-") %>%
    str_replace_all("^([0-9]{2})(-[0-9]{1,2}-[0-9]{1,2})$", "20\\1\\2") %>%
    str_replace_all("^([0-9]{4})-([0-9])-([0-9]{1,2})$", "\\1-0\\2-\\3") %>%
    str_replace_all("^([0-9]{4}-[0-9]{2})-([0-9])$", "\\1-0\\2")
}

或者

make.date <- function(x){
  x <- trimws(x)
  x <- gsub("[[:space:]]+", "-", x)
  x <- gsub("/", "-", x)
  x <- gsub("[.]", "-", x)
  x <- gsub("^([0-9]{2})(-[0-9]{1,2}-[0-9]{1,2})$", "20\\1\\2", x)
  x <- gsub("^([0-9]{4})-([0-9])-([0-9]{1,2})$", "\\1-0\\2-\\3", x)
  x <- gsub("^([0-9]{4}-[0-9]{2})-([0-9])$", "\\1-0\\2", x)

  x
}

另一办法是用strsplit()拆分出三个部分, 转换为整数, 再转换回字符型。

测试:

x <- c("49/10/1", "1949.10.01", "532 3 15", "2015/6.1", 
       "20170230", "2017.13/11", "2017 1 32")
make_date(x)
## [1] "2049-10-01" "1949-10-01" "532-3-15"   "2015-06-01" "20170230"  
## [6] "2017-13-11" "2017-01-32"

目前的函数还不能处理没有分隔符的情况, 也不能验证日期的合法性。

41.6.1.5 合并段落为一行

在某些纯文本格式中, 各段之间用空行分隔, 没有用空行分隔的各行看成同一段。 如下的函数把其中的不表示分段的换行删去从而合并这些段落。 函数以一个文件名作为输入, 合并段落后存回原文件。 注意, 这样修改文件的函数在调试时, 应该注意先备份文件, 等程序没有任何错误以后才可以忽略备份。

combine_paragraph <- function(fname){
  lines <- readLines(fname) 
  
  s <- str_flattern(lines)
  s <- s %>%
    str_replace_all("^[[:space:]]+\n", "\n") %>%
    str_replace_all("([^\n]+)\n", "\\1 ") %>%
    str_replace_all("([^\n]+)\n", "\\1\n\n")
  
  writeLines(str_split(s, "\n")[[1]],
             con=fname)
}

函数首先把仅有空格的行中的空格删除, 将有内容的行的行尾换行符替换成一个空格, 再把剩余的有内容的行的行尾换行符多加一个换行符。

上面的程序中特意用了基本R的readLines()函数而不是readr包的read_lines()函数, 因为readLines()使用操作系统的默认中文编码, 而read_lines()默认使用UTF-8编码, 需要用选项locale=locale(encoding="GB18030")才能在中文MS Windows中正确读取中文文件。

下面的版本不使用stringr:

combine.paragraph <- function(fname){
  lines <- readLines(fname)
  s <- paste(lines, collapse="\n")
  s <- gsub("^[[:space:]]+\n", "\n", s, perl=TRUE)
  s <- gsub("([^\n]+)\n", "\\1 ", s, perl=TRUE)
  s <- gsub("([^\n]+)\n", "\\1\n\n", s, perl=TRUE)
  writeLines(strsplit(s, "\n", fixed=TRUE)[[1]],
             con=fname)
}

41.6.2 不规则Excel文件处理

  • 作为字符型数据处理示例, 考察如下的一个Excel表格数据。

假设一个中学把所有课外小组的信息汇总到了Excel表的一个工作簿中。 每个课外小组占一块区域,各小组上下排列, 但不能作为一个数据框读取。 下图为这样的文件的一个简化样例:

不规则Excel文件样例图形

实际数据可能有很多个小组, 而且数据是随时更新的, 所以复制粘贴另存的方法不太可行, 需要用一个通用的程序处理。 Excel文件(.xls后缀或.xlsx后缀)不是文本型数据。 在Excel中,用“另存为”把文件保存为CSV格式, 内容如下:

XXX中学兴趣组情况总表,,,
,,,
组名:,物理,指导教师:,刘一心
姓名,性别,班级,
伊家宝,男,初二(3),
闻月,女,初二(5),
刘阳,男,初三(1),
宋佰霖,男,初三(2),
洪晓梅,女,初三(1),
,,,
组名:,生物,指导教师:,赵晓辉
姓名,性别,班级,
刘佳琦,女,初二(1),
李雨婷,女,初二(5),
张宠,男,初三(4),

生成测试用的数据文件:

demo.multitab.data <- function(){
s <- "
XXX中学兴趣组情况总表,,,
,,,
组名:,物理,指导教师:,刘一心
姓名,性别,班级,
伊家宝,男,初二(3),
闻月,女,初二(5),
刘阳,男,初三(1),
宋佰霖,男,初三(2),
洪晓梅,女,初三(1),
,,,
组名:,生物,指导教师:,赵晓辉
姓名,性别,班级,
刘佳琦,女,初二(1),
李雨婷,女,初二(5),
张宠,男,初三(4),
"
writeLines(s, "data/multitab.csv")
}
demo.multitab.data()

读入测试用的数据,转换为一整个数据框:

demo_multitab <- function(){
  ## 读入所有行
  lines <- readLines("data/multitab.csv")

  ## 去掉首尾空格
  lines <- str_trim(lines)
  
  ## 删去所有空行和只有逗号的行
  empty <- str_detect(lines, "^[[:space:],]*$")
  if(length(empty)>0){
    lines <- lines[!empty]
  }

  ## 找到所有包含 “组名:”的行对应的行号
  heads <- str_which(lines, "组名:")

  ## 定位每个表的开始行和结束行(不包括组名和表头所在的行)
  start <- heads + 2
  end <- c(heads[-1]-1, length(lines))
  ngroups <- length(heads)

  ## 先把数据读入一个列表。
  for(ii in seq(ngroups)){
    ## 组名和指导教师所在行:
    line <- lines[heads[ii]]
    v <- str_split(line, ",")[[1]]
    ## 组名:v[2] 指导教师: v[4]

    ## 将表格内容各行合并成一个用换行符分隔的长字符串,
    ## 然后变成可读取的文件
    s <- str_flatten(lines[start[ii]:end[ii]], collapse="\n")
    con <- textConnection(s, "rt")
    da1 <- read.csv(
      con, header=FALSE, 
      colClasses=c("character", "character", "character", "NULL"))
    close(con)
    names(da1) <- c("姓名", "性别", "班级")
    da1 <- cbind("组名"=v[2], "指导教师"=v[4], da1)

    if(ii==1) {
      da <- da1
    } else {
      da <- rbind(da, da1)
    }
  }

  da
}
da <- demo_multitab()
knitr::kable(da)
组名 指导教师 姓名 性别 班级
物理 刘一心 伊家宝 初二(3)
物理 刘一心 闻月 初二(5)
物理 刘一心 刘阳 初三(1)
物理 刘一心 宋佰霖 初三(2)
物理 刘一心 洪晓梅 初三(1)
生物 赵晓辉 刘佳琦 初二(1)
生物 赵晓辉 李雨婷 初二(5)
生物 赵晓辉 张宠 初三(4)

不使用stringr的版本:

demo.multitab <- function(){
  ## 读入所有行
  lines <- readLines("multitab.csv")

  ## 去掉首尾空格
  lines <- trimws(lines)
  
  ## 删去所有空行和只有逗号的行
  ## (1) 不用正则表达式做法
  #empty <- which(lines == "" | substring(lines, 1, 3)==",,,")
  ## (2) 用正则表达式做法:
  empty <- grep("^[[:space:],]*$", lines)
  if(length(empty)>0){
    lines <- lines[-empty]
  }

  ## 找到所有包含 “组名:”的行对应的行号
  heads <- grep("组名:", lines, fixed=TRUE)

  ## 定位每个表的开始行和结束行(不包括组名和表头所在的行)
  start <- heads + 2
  end <- c(heads[-1]-1, length(lines))
  ngroups <- length(heads)

  ## 先把数据读入一个列表。
  for(ii in seq(ngroups)){
    ## 组名和指导教师所在行:
    line <- lines[heads[ii]]
    v <- strsplit(line, ",")[[1]]
    ## 组名:v[2] 指导教师: v[4]

    ## 将表格内容各行合并成一个用换行符分隔的长字符串,
    ## 然后变成可读取的文件
    s <- paste(lines[start[ii]:end[ii]], collapse="\n")
    con <- textConnection(s, "rt")
    da1 <- read.csv(
      con, header=FALSE, 
      colClasses=c("character", "character", "character", "NULL"))
    close(con)
    names(da1) <- c("姓名", "性别", "班级")
    da1 <- cbind("组名"=v[2], "指导教师"=v[4], da1)

    if(ii==1) {
      da <- da1
    } else {
      da <- rbind(da, da1)
    }
  }

  da
}
da <- demo.multitab()
da

在程序中, 用readLines函数读取文本文件各行到一个字符型向量。 用grep可以找到每个小组开头的行(有“组名:”的行)。 然后可以找出每个小组学生名单的开始行号和结束行号。 各小组循环处理,读入后每个小组并入结果数据框中。 用strsplit函数拆分用逗号分开的数据项。 用textConnection函数可以把一个字符串当作文件读取, 这样read.csv函数可以从一个字符串读入数据。

41.6.3 字频统计

正则表达式中的字符类[:alpha:]指的是当前系统中的字母, 所以在中文环境中的中文字也是字母, 但中文标点不算。 下面是《红楼梦》中“秋窗风雨夕”的文本:

秋花惨淡秋草黄,耿耿秋灯秋夜长。
已觉秋窗秋不尽,那堪风雨助凄凉!
助秋风雨来何速!惊破秋窗秋梦绿。
抱得秋情不忍眠,自向秋屏移泪烛。
泪烛摇摇爇短檠,牵愁照恨动离情。
谁家秋院无风入?何处秋窗无雨声?
罗衾不奈秋风力,残漏声催秋雨急。
连宵脉脉复飕飕,灯前似伴离人泣。
寒烟小院转萧条,疏竹虚窗时滴沥。
不知风雨几时休,已教泪洒窗纱湿。

希望统计每个字的出现次数, 并显示频数前十的字。 设变量poem_autumnwindow中包含了上述诗词的文本。

首先用str_extract_all()提取每个中文字,组成一个字符型向量:

words_vec <- str_extract_all(poem_autumnwindow, "[[:alpha:]]")[[1]]
head(words_vec)
## [1] "秋" "花" "惨" "淡" "秋" "草"

table()函数计算频数,并按频数排序,输出前10结果:

words_freq <- sort(table(words_vec), decreasing=TRUE)
knitr::kable(head(words_freq, 10))
words_vec Freq
15
5
5
5
4
3
2
2
2
2

41.6.4 数字验证

41.6.4.1 整数

字符串完全为十进制正整数的模式,写成R字符型常量:

"\\A[0-9]+\\Z"

这个模式也允许正整数以0开始,如果不允许以零开始,可以写成

"\\A[1-9][0-9]*\\Z"

对于一般的整数,字符串完全为十进制整数,但是允许前后有空格, 正负号与数字之间允许有空格,模式可以写成:

"\\A[ ]*[+-]?[ ]*[1-9][0-9]*\\Z"

41.6.4.2 十六进制数字

字符串仅有十六进制数字,模式写成R字符型常量为

"\\A[0-9A-Fa-f]+\\Z"

在文中匹配带有0x前缀的十六进制数字,模式为

"\\b0x[0-9A-Fa-f]+\\b"

41.6.4.3 二进制数字

为了在文中匹配一个以Bb结尾的二进制非负整数,可以用

"\\b[01]+[Bb]\\b"

41.6.4.4 有范围的整数

1-12:

"\\b1[012]|[1-9]\\b"

1-24:

"\\b2[0-4]|1[0-9]|[1-9]\\b"

1-31:

"\\b3[01]|[12][0-9]|[1-9]\\b"

1900-2099:

"\\b(?:19|20)[0-9]{2}\\b"

这里的分组仅用于在19和20之间选择, 不需要捕获,所以用了(?:的非捕获分组格式。

41.6.4.5 判断字符型向量每个元素是否数值

如下的R函数用了多种数字的正则表达式来判断字符型向量每个元素是否合法数值。

all_numbers <- function(x){
  x <- x %>%
    str_replace_all("\\A[ ]+", "") %>%
    str_replace_all("[ ]+\\Z", "")

  pint <- "\\A[+-]?[0-9]+\\Z"  # 整数, 允许有前导零
  ## 浮点数1, 整数部分必须,小数部分可选,指数部分可选
  pf1 <- "\\A[+-]?[0-9]+([.][0-9]*)?([Ee][+-]?[0-9]+)?\\Z" 
  ## 浮点数2, 整数部分省略,小数部分必须,指数部分可选
  pf2 <- "\\A[+-]?[.][0-9]+([Ee][+-]?[0-9]+)?\\Z"
  pat <- str_c(pint, pf1, pf2, sep="|")
  
  str_detect(x, pat)
}

不使用stringr的版本:

all.numbers <- function(x){
  x <- gsub("\\A[ ]+", "", x, perl=TRUE)
  x <- gsub("[ ]+\\Z", "", x, perl=TRUE)

  pint <- "\\A[+-]?[0-9]+\\Z"  # 整数, 允许有前导零
  ## 浮点数1, 整数部分必须,小数部分可选,指数部分可选
  pf1 <- "\\A[+-]?[0-9]+([.][0-9]*)?([Ee][+-]?[0-9]+)?\\Z" 
  ## 浮点数2, 整数部分省略,小数部分必须,指数部分可选
  pf2 <- "\\A[+-]?[.][0-9]+([Ee][+-]?[0-9]+)?\\Z"
  pat <- paste(pint, pf1, pf2, sep="|")
  grepl(pat, x, perl=TRUE)
}

测试:

all_numbers(c("1", "12", "-12", "12.", "-12.",
              "123.45", "-123.45", ".45", "-.45",
              "1E3", "-12E-10", "1.1E3", "-1.1E-3",
              "1.0.0", "1.0-0.5"))
##  [1]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
## [13]  TRUE FALSE FALSE

41.6.5 文件名中的数字提取

设有如下的一些文件名:

s <- c("10-0.16-1700.0-42.csv", "12-0.22-1799.1.csv")

希望提取出每个文件名中用减号分隔开的数字, 如第一个文件名中的10, 0.16, 1700.0, 42, 第二个文件名中的12, 0.22, 1799.1, 数字的个数不需要相同。

先定义数字的模式, 注意长备择模式中长的模式要写在前面, 否则会被短的模式优先匹配:

pat <- "[0-9]+[.][0-9]+|[0-9]+"

stringr::str_match_all()提取其中的匹配数字:

s1 <- str_match_all(s, pat); s1
## [[1]]
##      [,1]    
## [1,] "10"    
## [2,] "0.16"  
## [3,] "1700.0"
## [4,] "42"    
## 
## [[2]]
##      [,1]    
## [1,] "12"    
## [2,] "0.22"  
## [3,] "1799.1"

每个列表元素是一个矩阵, 其中第一列的各行是对模式的多次匹配, 取出这些匹配为一个字符型向量:

s2 <- Map(function(x) x[,1], s1); s2
## [[1]]
## [1] "10"     "0.16"   "1700.0" "42"    
## 
## [[2]]
## [1] "12"     "0.22"   "1799.1"

如果需要,也可以将拆分出的字符型的数字结果转换成数值型:

s3 <- Map(as.numeric, s2); s3
## [[1]]
## [1]   10.00    0.16 1700.00   42.00
## 
## [[2]]
## [1]   12.00    0.22 1799.10

这个问题也可以用strsplit()或者stringr::str_split()解决。如:

s <- c("10-0.16-1700.0-42.csv", "12-0.22-1799.1.csv")
s1 <- substring(s, 1, nchar(s)-4) # 去掉".csv"
print(s1)
## [1] "10-0.16-1700.0-42" "12-0.22-1799.1"
s2 <- strsplit(s1, "[-]")  # 按减号分成几个部分,结果为列表
print(s2)
## [[1]]
## [1] "10"     "0.16"   "1700.0" "42"    
## 
## [[2]]
## [1] "12"     "0.22"   "1799.1"
s3 <- Map(as.numeric, s2)  # 转换为数值型
print(s3)
## [[1]]
## [1]   10.00    0.16 1700.00   42.00
## 
## [[2]]
## [1]   12.00    0.22 1799.10

41.7 网站数据获取

很多网站定期频繁发布数据, 所以传统的手工复制粘贴整理是不现实的。 有些网站提供了下载功能, 有些则仅能显示。

这些数据网页往往有固定模式, 如果网页不是依赖JavaScript来展示的话, 可以读取网页然后通过字符型数据处理方法获得数据。

R扩展包rvest可以对网页按照其中的网页构成节点路径(xpath)提取数据, 转换为R数据框。

以上海证券交易所的上证综指成份股列表为例。 使用Google Chrome浏览器打开如下的页面:

http://www.sse.com.cn/market/sseindex/indexlist/s/i000001/const_list.shtml

将显示上证综指成份股的名称与编码的列表页面。 利用Chrome浏览器的功能先获取表格所在页面部分的xpath, 办法是鼠标右键单击表格开头部分, 选择“检查”(inspect), 这时会在浏览器右边打开一个html源代码窗口, 当前加亮显示部分是表格开头内容的源代码, 将鼠标单击到上层的<table class="tablestyle">处, 右键单击选择“Copy-Copy XPath”, 得到如下的xpath地址:'//*[@id="content_ab"]/div[1]/table'

然后, 用rvest的html_nodes()函数提取页面中用xpath指定的成分, 用html_table()函数将HTML表格转换为数据框, 结果是一个数据框列表, 因为仅有一个, 所以取列表第一项即可。 程序如下:

library(rvest)

## 网页地址
urlb <- "http://www.sse.com.cn/market/sseindex/indexlist/s/i000001/const_list.shtml"
## 网页中数据表的xpath
xpath <- '//*[@id="content_ab"]/div[1]/table'

## 读入网页并提取其中的表格节点
nodes <- html_nodes(
  read_html(urlb), xpath=xpath)

## 从表格节点转换为表格列表
tables <- html_table(nodes)
restab <- tables[[1]]
head(restab)
##                                  X1                               X2
## 1 浦发银行\r\n            (600000) 白云机场\r\n            (600004)
## 2 中国国贸\r\n            (600007) 首创股份\r\n            (600008)
##                                 X3
## 1 东风汽车\r\n            (600006)
## 2 上海机场\r\n            (600009)

可见每一行有三个股票, 我们将数据中的\r\n和空格去掉, 然后转换成名称与代码分开的格式:

library(tidyverse)

pat1 <- "^(.*?)\\((.*?)\\)"
tab1 <- restab %>%
  ## 将三列合并为一列,结果为字符型向量
  reduce(c) %>% 
  ## 去掉空格和换行符,结果为字符型向量
  stringr::str_replace_all("[[:space:]]", "") %>%
  ## 提取公司简称和代码到一个矩阵行,结果为字符型矩阵
  stringr::str_match(pat1) 
tab <- tibble(
  name = tab1[,2],
  code = tab1[,3])
head(tab)
## # A tibble: 6 x 2
##   name     code  
##   <chr>    <chr> 
## 1 浦发银行 600000
## 2 中国国贸 600007
## 3 包钢股份 600010
## 4 华夏银行 600015
## 5 上港集团 600018
## 6 上海电力 600021
str(tab)
## Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    1551 obs. of  2 variables:
## $ name: chr  "浦发银行" "中国国贸" "包钢股份" "华夏银行" ...
##  $ code: chr  "600000" "600007" "600010" "600015" ...

对于不符合规则的网页, 可以用download.file()下载网页文件, 用str_replace_all()或者gsub()去掉不需要的成分。 用str_which()或者grep查找关键行。

有些网页是依靠JavaScript来显示数据的, 比如新浪财经的环球股指汇总网页:

http://finance.sina.com.cn/money/globalindex/

这样的网页很难用程序提取数据。

41.8 中文分词与词频

为了对中文文章进行分析, 需要将文章内容拆分为一个个单词。 R扩展包jiebaR可以进行中文分词。 用w <- worker()创建一个分词器, 用segment(txt, w)对字符串txt中的中文内容进行分词, 得到字符型向量,每个元素是一个词。

也可以调用segment(fname, w), 其中fname是输入文本文件名, 可以自动侦测其中的中文编码, 分词结果会自动保存为文件开头和文件扩展名与fname相同的一个文件, 词之间以空格分隔。

分词后, R可以很容易地进行词频统计, 如table()函数。

例如, 对金庸的《侠客行》分词:

library(jiebaR)
## 载入需要的程辑包:jiebaRD
wk <- worker()
txt <- readr::read_file("xkx.txt", locale=locale(encoding="GB18030"))
words <- segment(txt, wk)
tab <- table(words)
tab <- sort(tab, decreasing = TRUE)
## 去掉单个字的词语
tab2 <- tab[stringr::str_length(names(tab)) > 1]
knitr::kable(as.data.frame(head(tab2, 20)))
words Freq
石破天 1800
什么 697
说道 602
自己 570
雪山 451
白万剑 443
丁当 443
一个 421
帮主 387
武功 381
石清 372
丁不四 345
谢烟客 340
一声 321
不是 319
二人 319
不知 308
咱们 304
史婆婆 291
夫妇 284

词频可以用“词云”数据可视化方式表现。 在词云图形中, 词频大的词显示为较大的字体。 R扩展包wordcloud2可以输入词频统计表, 输出图形格式的词云显示, 以HTML5格式显示。 函数wordcloud2()可以输入table()的结果, 或者有词和词频构成的两列的数据框。

library(wordcloud2)
wordcloud2(data = head(tab2, 20))

注意,这个库支持图形在HTML结果中显示, 且具有一定交互性, 但不直接支持LaTeX转换的PDF输出, 所以需要进行设置。 可以在Rmd源文件开头运行命令:

is_html <- knitr::opts_knit$get("rmarkdown.pandoc.to") == "html"

这可以定义一个变量is_html, 仅在输出格式为HTML时才为TRUE, 然后在包含特殊HTML显示的代码段选项中, 加选项eval = is_html