48 R语言的文本处理

48.1 介绍

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

48.2 字符型常量与字符型向量

字符串常量写在两个双撇号或者两个单撇号中间, 建议仅使用双撇号, 因为这是大多数常见程序语言的做法。 如果内容中有单撇号或者双撇号, 可以在前面加反斜杠\。 为了在字符串中写一个反斜杠, 需要写成两个, 比如路径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)

48.3 字符串连接、重复

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" "长江长江"

48.4 格式化输出

48.4.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()还有许多选项, 详见函数的帮助。

48.4.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"

48.4.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

需要插入原样的大括号时, 可以双写,如:"{{...}}"

48.5 字符串长度

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 6

48.6 取子串

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是标量。

48.7 字符串变换

48.7.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()转小写。

48.7.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] "昌平区,大兴区;固安区."

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

48.7.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"

48.7.4 截短或分行

stringr::str_trunc(x, width)可以截短字符串。

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

48.7.5 排序

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

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

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

48.8 简单匹配与查找

48.8.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)判断是否以给定模式结尾。

48.8.2 中间匹配

函数grep(), grepl()等可以用于查找子字符串, 位置不限于开头和结尾, 详见§49.1

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

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

48.9 字符串替换

stringr包的str_replace_all(string, fixed(pattern), replacement)在字符型向量string的每个元素中查找子串pattern, 并将所有匹配按照replacement进行替换。

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

gsub(pattern, replacement, x, fixed=TRUE) 把字符型向量x中每个元素中出现的子串 pattern都替换为replacement(注意与str_replace_all的自变量次序区别)。 如

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

48.10 字符串拆分

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"

48.11 文本文件读写

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