Skip to content

Latest commit

 

History

History
567 lines (411 loc) · 25.9 KB

chap03-tidyverse03-stringr.md

File metadata and controls

567 lines (411 loc) · 25.9 KB

文本数据分析

传统数据分析的对象多是数字而非文字。这主要是受制于分析技术与相关工具的发展水平。实际上,人类行为的许多内容是以文字、语音、表情等难以完全数字化的方式存在。如何扩大数据分析的范畴,将分析对象拓展至文本数据、音频数据和视频数据,是近些年来数据分析领域的热门议题。这里仅限对利用 R 语言进行文本数据的基础处理和统计分析进行简单介绍,但很多内容也适用于其他软件。

R 内置字符串函数

R 基础安装包中内置了一些字符串处理函数,能在很大程度上满足基本使用需求,也是进一步学习其他 R 包中相关字符串函数的基础。这里择要进行简单介绍。

函数名称 功能
nchar(x) 计算向量 x 中的字符数、字节数或宽度
substr(x, start, end) 指定起始位置截取子字符串
grep(pattern, x) 检索向量 x 中的元素是否包含指定的模式,并返回其在 x 中的序位
sub(pattern, replacement, x) 替换向量x 中的指定模式
strsplit(x, split) 指定位置切割字符串
paste(x, y, sep = "") 拼接向量 xy
toupper(x) x 中的所有英文字母转为大写(upper case)
tolower(x) x 中的所有英文字母转为小写(lower case)
cat(x, y) 连接向量并定制化输出至屏幕或文件, cat 是英文 concatenate(串联、合并)的缩写

以下通过一些实例说明上述函数的使用方式。诸多演示将使用如下的简单汉语字符向量。

x <- c("北京大学社会学系", "清华大学心理学系", "南开大学社会心理学系")

先看nchar()

nchar(x)

显示为各元素中包含的字符个数。这看似简单,实则不然。nchar()的用法为:

nchar(x, type = "")

其中,type参数有三个选项:

  • chars(字符数),1个英文单词、数字或汉字计1字符
  • bytes(字节),1个英文单词或数字为计1字节,1个汉字计2字节
  • width(宽度),将字符串转为等宽字体 ^[等宽字体(monospaced font)是指字符宽度相同的电脑字体,字符宽度不尽相同的电脑字体则称为比例字体(proportional font)。编程代码常使用的是等宽字体。]后所占的列数,通常与bytes的计算方式相同

所以,如果输入

nchar(x, type = "bytes")

即可看到不同结果。

substr()函数的结果一目了然:

substr(x, 1, 4)

grep()函数实际上通常要匹配其他函数来用,这里只做简单示例:

grep("大学", x)

这表示 x 向量中的前3个元素均包含“大学”这一字符。

sub()的作用是替换,其中sub 是英文 substitute(替换)的缩写。

sub("大学", "University", x)

paste()可将若干向量的横向拼接,其用法为:

paste (A, B, sep = " ", collapse = NULL)

其中,

  • AB表示等拼接的向量,可拓展至多个向量
  • sep 表示连接各向量中各元素的连接符
  • collapse 表示将各向量拼接为一个向量的连接符

只看解释很难让人明白这两个参数的功能。细看以下示例:

paste("南开大学", "社会心理学系", sep = "-")
paste("南开大学", "社会心理学系", sep = "**")
paste("南开", c("社会学系", "社会心理学系", "经济学系"), sep = "大学")
paste(LETTERS[1:5], letters[1:5], sep = "-")
paste(LETTERS[1:5], letters[1:5], sep = "-", collapse = " | ")

前三例非常直观,值得注意的是后两例,请体会sepcollapse这两个参数的不同功能。

从示例可看出,sep参数用于横向拼接向量,即把paste(A, B, sep = "")中的第一个向量和第二个向量按元素顺序逐对拼起来,而collapse是把一个向量内部所有元素按一个分隔符拼接为单个字符串。按 R 的自动扩展原则,若两个向量长度不一,短向量会被扩展到长向量的长度,再去拼接。同时,sep返回的仍然是一个向量,每一元素是所拼接向量中的相应位置上的元素拼接而成;而collapse把所有字符向量“坍缩”为一个字符串,这正是 collapse 的英文本意。

paste()还经常与cat()配合使用,以定制输出形式,例如:

paste("我是爱南开的", "周恩来", sep = "\n")
cat(paste("我是爱南开的", "周恩来", sep = "\n"))

paste()只执行拼接功能,而显示结果还需要通过cat()来完成。

touppper()tolower()的功能也非常直观:

toupper(letters[1:5])
tolower(LETTERS[1:5])

这里不再细述。

R 内置的字符串函数还有很多,功能也较齐全。其缺点在于函数命名比较分散,没有统一风格,不利于记忆。另外,在一些复杂字符串的处理上仍有一定欠缺。为此,可调动 stringr 等包中的相关函数加以更为灵活便捷的处理。

stringr 包中的字符串处理函数

绝大多数 stringr 包中的函数都以str_开头(表示string),下划线后跟着一个动词或其缩写,以指明其功能。以下按字母序列出stringr 包中的重要函数及其功能。

函数名称 功能
str_c() 拼接多个字符串为单个字符串
str_conv() 更改字符串的编码类型
str_count() 计算字符串中指定模式的字符个数
str_detect() 判断字符串是否包含指定模式
str_dup() 将向量中的各字符串重复自身 n
str_extract() 提取字符串中包含的指定模式(匹配一次)
str_extract_all() 提取字符串中包含的指定模式(匹配所有)
str_length() 计算字符串的长度(即所包含的字符个数)
str_locate() 给出指定模式在字符串中的起始位置(输出矩阵)
str_locate_all() 给出指定模式在字符串中的起始位置(输出列表)
str_match() 判断字符串中是否包含指定模式(输出矩阵)
str_match_all() 判断字符串中是否包含指定模式(输出列表)
str_order() 给出字符串在所处向量中的位置(默认首字母升序)
str_pad() 用指定字符填充字符串至指定字符长度
str_replace() 替换字符串中的指定模式(匹配一次)
str_replace_all() 替换字符串中的指定模式(匹配所有)
str_replace_na() 将表示缺失值的 NA 替换为字符串 "NA"
str_sort() 按字符串在所处向量中的位置排序(默认首字母升序)
str_split() 根据指定模式切割字符串(输出列表)
str_split_fixed() 根据指定模式切割字符串(输出矩阵)
str_sub() 指定起始位置截取子字符串
str_subset() 给出所有包含指定模式的字符串
str_trim() 删去字符串首尾处的空白符
str_trunc() 删除部分内容使字符串截为指定宽度
str_which() 给出所有包含指定模式的字符串的位置(输出整数)
str_wrap() 设定字符串的段落输出形式
str_view() 网页格式下对指定模式阴影显示(匹配一次)
str_view_all() 网页格式下对指定模式阴影显示(匹配所有)
str_to_upper() 将字符串中的所有英文字母转为大写(upper case)
str_to_lower() 将字符串中的所有英文字母转为小写(lower case)
str_to_title() 将字符串中的英文单词转为首字母大写
word() 提取句子中指定位置的单词

许多函数通过示例可一目了然,不再细述。少数复杂的函数会细加解释。同时请注意 stringr 字符串函数与 R 基础包中的字符串函数的比较。

展开分析前,请确保已载入 stringr 包。

library(stringr)

str_c()函数用于横向拼接字符串。

str_c("南开", "大学")
str_c("南开", "大学", sep = "")
str_c("南开", "大学", sep = "-")

可见默认不加空格拼接,也可指定用于拼接的字符。但str_C()的功能并不局限于此。试看下面的命令:

str_c(LETTERS[1:5], letters[1:5], sep = "-")
str_c(LETTERS[1:5], letters[1:5], collapse = "-")
str_c(LETTERS[1:5], letters[1:5], sep = "-", collapse = " | ")

请结合此例再次明确sepcollapse参数的功能。

str_replace_na() 函数将表示缺失值的 NA 替换为字符串 "NA"。试比较以下结果:

na <- c(NA, "A", "B")
na
str_replace_na(na)
str_trunc(string,
          width,
          side = c("right", "left", "center"),
          ellipsis = "...")

trunc是英文 truncate 的缩写,其中

  • string:待操纵的字符串
  • width:截断后的最大宽度
  • side:指定截断的方向
  • ellipis:指明被移除位置的替代符号,默认 ...(宽度为3)
str_trunc(x, 4, side = "left")
str_trunc(x, 2, side = "right", ellipsis = "-")
str_trunc(x, 6, side = "center", ellipsis = "XX")

str_dup()函数用于复制向量中的字符串,dup 是英文 duplicate 的缩写。请观察以下示例。

n <- "南开"
k <- "大学"
nk1 <- c(n, k)
nk1
nk2 <- str_c(n, k)
nk2
str_dup(nk1, 2)
str_dup(nk2, 2)

word()函数用于提取句子中指定位置的单词,一般要求各单词之间应有空格划分。这一功能在中文领域目前较难有用武之地,因为中文句子的各个单字之间并无英文单词之间的空格,除非人为加入。

Chi <- c("南 开 大 学", "北 京 大 学")
word(Chi, 1, 2)
Eng <- c("He likes cats.", "She hates rats.")
word(Eng, 2)

str_count()用于计算字符串中包含的字符数。

str_count(x)
str_count(x, "心理学")

str_count()的功能类似于 R 基础包中的nchar()函数(number of characters),但nchar()的功能更全;前者只能用于计算字符数,后者还可用于计算字节数或宽度。

str_detect(x, "心理学")
str_extract(x, "心理学")
str_extract_all(x, "心理学")
str_extract_all(x, "心理学", simplify = TRUE)
str_locate(x, "心理学")
str_locate_all(x, "心理学")
str_replace(x, "学", "xue")
str_replace_all(x, "学", "xue")
str_match(x, "心理学")
str_match_all(x, "心理学")
str_split(x, "大学", n = 2)
str_split(x, "大学", n = 2, simplify = TRUE)
str_split_fixed(x, "大学", n = 2)
str_sub(x, start = 1, end = 4)
str_sub(x, -4, -1)
str_subset(x, "心理学")
str_which(x, "心理")
str_view(x, "学")
str_view_all(x, "学")

str_pad()函数用于填充字符串,格式如下:

str_pad(string,
        width,
        side = c("left", "right", "both"),
        pad = " ")

其中,

  • string:待填充的字符串
  • width:待填充的最大宽度,长度以字节(byte)计
  • side:填充方向
  • pad:用于填充的符号(限1个字节宽度),默认为空格
str_pad(x, 20, side = "left")

注意左边出现的空格。一个汉字占两个字节,故“南开大学社会心理学系”正好20字节,无须填充。再看下例。

str_pad(x, 20, side = "right", pad = "X")

str_order()str_sort()分别用于给出字符串的序位及根据这一序位排序。

str_order(x)
str_sort(x)
str_sort(x, decreasing = TRUE)

str_to_upper()str_to_lower()str_to_title()用于英语字母的大小写转换。

Eng <- c("He likes cats.", "She hates rats.")
str_to_lower(Eng)
str_to_upper(Eng)
str_to_title(Eng)
paragraph <- c("R是一个免费、自由且跨平台通用的统计计算与绘图软件,它有Windows、Mac、Linux等版本,均可免费下载使用。R项目(The R Project for Statistical Computing)最早由新西兰奥克兰大学(Auckland University)的Robert Gentleman(1959-) 和 Ross Ihaka(1954-) 开发,故软件取两人名字的首字母命名为R。")
str_wrap(paragraph, width = 30, indent = 4)

此时可见每40个字节即被切分为一行(增加了换行符\n),但并未分行显示;indent = 4表示首先缩进4字节。在str_wap()的基础上使用基础命令cat()即可达到合并成段落。

cat(str_wrap(paragraph, width = 40, indent = 4))

空白符

空白符(white space)并不仅仅指空格(Space ),还包括制表符、回车符、换行等。

常用空白符类型

名称 符号
空格符(space)
水平制表符(horizontal tab) \t
垂直制表符(vertical tab) \v
换行符(line feed) \n
回车符(carriage return) \r

注意表格空格符对应的符号为空,这并不表示没有符号,而是表示空格符号。请参考此两者的区别:""" ",前一对双引号中无任何符号,后一对双引号中有一个空格。

同时注意换行与回车的本质并不相同,它们源自传统的电传打字机。所谓回车是让打字机把打印头“回归”(return)到最初始位置(左边界处),换行则指让打字机把纸向下移一行。Unix系统中每行结尾只有“换行”,Windows系统里每行结尾是“回车 + 换行”,Mac系统里每行结尾是“回车”。这会造成不同系统下的文本文档在跨系统读取时出现格式变异问题。Word软件中的“回车”,其实也是回车 + 换行的组合。

为更好地说明str_trim()的功能,下面结合一个初步的网页爬虫实例进行示例。爬虫时经常会遇到要爬取多个链接中的相关内容,通常的做法是将所有链接保存为一个文件,然后再使用特定的循环语句,对每一个链接执行相同的操作。这里的第一步就是如何简洁高效地获取相关链接。这里以我国中央人民政府门户网站(即通常所谓“中国政府网”网站)上提供的政府工作报告页面作为示例页面。在

http://www.gov.cn/guowuyuan/baogao.htm

这个链接中,给出自1954年以来的历届政府工作报告。每个报告均以单独链接的方式存在,点击后即可阅读相关内容。若要一次性爬取所有报告内容,首先需要获得每年政府工作报告的链接地址。网络爬虫涉及基础网页结构知识和相关软件包的学习,这里暂不展开,只结合 rvest 等 R 包的相关函数展示相关说明,以说明str_trim()的功能(请确保联网以执行相关功能,并确定已安装 rvest 包)。

library(xml2)
library(rvest)
library(stringr)
url <- "http://www.gov.cn/guowuyuan/baogao.htm"
reports <- read_html(url)
links <- reports %>%
  html_nodes(".history_report a") %>%
  html_attr("href")
head(links)

这里的 links 对象已储存了所有49个政府工作报告(1954--2017)的详细链接。然后细分析每个“链接”,发现其结尾都有两个空白符:\r\n。这其实正是“回车 +换行”的意思。要让 R 准确读取链接,这两个空白符应当删去。此时即可使用str_trim()函数如下:

links <- str_trim(links)
head(links)

此时即可显示“纯净”的链接地址,方便进行下一步的工作。

最后,stringr 提供了三个文本数据,以供练习相关命令:sentences, fruit, words。加载 stringr 包后直接输入以上单词,回车后即可见到,例如:

stringr::fruit[1:5]

上述示例只展示了 stringr 中的基础函数用法,并未全面展示字符串处理的所有功能与应用。要更有效率地使用这些函数进行文本数据分析,需要进一步了解正则表达式。

R语言中的正则表达式

正则表达式(regular expression,简称 regex)是用于匹配、搜索和替换文本的字符串 ^[正则表达式听起来很陌生,实际上人们在文件夹中搜索类似包含“会议记录”字段的所有文件、或是在Word软件中一次性替换所有小写的“car”为首字母大写的“Car”单词、一次性把所有数字都转换为Times New Roman字体等操作时,就已在使用类似正则表达式的功能。]。它内置于多种程序语言或软件产品,是一种非严格意义上的“迷你语言”,其功能在于搜索特定模式(pattern)的文字并加以处理(如截取、替换等)。正式表达式在不同程序语言中有着不同的细节规定,但多数规则可跨语言通用。这里就这些通用规则并结合 R 语言的特征进行介绍。想要精通正则表达式需要较长时间,但了解基本的规则并不困难,细节内容可在实际工作需要时再去深究。

元字符

正则表达式本身就是由字符构成,但为了达到匹配其他字符的功能,需要对其中一些特定字符的功能加以限定。元字符(metacharacter)就是在正则表达式有特殊含义的字符,每一个元字符能匹配一个位置或一个字符。

基础元字符

正则表达式中有些基本字符被保留用作特殊用途,它们的含义不能从“字面意思”进行理解,且常与其他字符匹配成字符集合,以表达不同的字符类别。

正则表达式中的基础元字符

符号 功能
^ 匹配字符串的开始位置;放在[]中则表示反义,即[^aeiou]表示匹配除了[]中给定字符集合外的字符
$ 匹配字符串的开始位置
. 匹配除换行符之外的所有字符
` `
?*+ 限定重复匹配次数,后面详细说明
\ 对下一字符转义;或放在其他字符开头,组合特定的元字符
[] 方括号配对使用,匹配括号内的任意字符
() 圆括号配对使用表示字符组,括号内的字符串作为整体被匹配

转义符

由于元字符已被赋予特殊含义,因此,如果确实要匹配作为文本字符本身的上述符号,就需要通过转义符来实现。转义符(escape character)其实是一种特殊的元字符,“转义”就是“转换含义”的意思。转义符本身不被当成实体字符,而是起到提醒作用,使其后面的字符表达出特定的含义。不同程序使用的转义符并不相同,通常使用的转义符是反斜杠(捺斜杠)\

然而,由于\本身是元字符,因此在实际使用中会有诸多麻烦。例如,要匹配英文句号.本身,就不能写成.,也不能写成\.。这是因为\本身会被理解元字符而不是普通的文本字符。只有写成\\.才能达到匹配目的。试比较:

a <- c("a.\a123", "b.\a456", "c.\a789a", "d.\a")
a
str_view(a, ".")
str_view(a, "\\.")

str_view(a, ".")时突出显示的是第一个字符,这正是因为.号起到了元字符的作用。只有转义后才能突出显示.号本身。而如果输入str_view(a, "\.")则会出现错误信息。类似地,\\(会匹配(\\)会匹配),其他可类推。

细心的读者应该已经发现,刚才在执行str_view()命令时,\以及它后面的第一个字符并没有显示出来。这其实正是因为在\是一个转义符而不是普通字符,而跟在它后面的字母a因为并不是有意义的转义符号组合,所以被忽略掉,这其实正是英文 escape(逃离) 的本义。要正确地显示文本数据的内容,可使用 R 基础安装包中的writeLines()函数。

a0 <- c("a.\a123", "b.\a456", "c.\a789c", "d.\a")
a0
writeLines(a0)
a1 <- c("a.\\a123", "b.\\a456", "c.\\a789c", "d.\\a")
writeLines(a1)

请比较a0a1的结果。如果想要真正呈现带有\符号的文本,a1才是正确的写入方式。

那么,如何匹配\自身呢?思路如下:\是元字符,需要加以转义,故加上转义符\,变成\\;创建\\这一正则表达式,仍需要前面用\加以申明;最后,\\\\才能表示出作为普通文本符号的\

str_view(a0, "\\\\")
str_view(a1, "\\\\")

这可能是个特例。好在如此复杂的表达并不多见。

特殊元字符

与多数程序语言一样,R 支持预定义的POSIX字符类 ^[POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,是国际电气和电子工程师协会(IEEE)为在各种UNIX操作系统上运行的软件而定义的一系列标准的总称。],即通过一些特定字符组合表示一类字符。

符号 功能
[:alnum:] 任意英文字母及数字,等价于[a-zA-Z0-9],其中连字符-号表示范围区间
[:alpha:] 任意英文字母,等价于[a-zA-Z]
[:blank:] 空格(Space)或制表符(Tab),等价于[\t ],注意字母t后有一个空格
[:cntrl:] 任意ASCII控制字符
[:digit:] 任意数字,等价于[0-9]
[:graph:] 任意图形字符,包括标点、字母及数字,即[:alnum:]和[:punct:]的集合
[:lower:] 任意小写字母,等价于[a-z]
[:print:] 任意可打印字符,即[:alnum:],[:punct:]和[:space:]的集合
[:punct:] 任意英文标点符号,包括 ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ { | } ~ ` .
[:space:] 任意空白符,包括水平制表符(\t)、垂直制表符(\v)、空格符(space)、换行符(\n)、回车符(\r)、换页符(\f
[:upper:] 任意大写字母,等价于[A-Z]
[:xdigit:] 任意十六进制数字,即0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f,等价于[0-9a-fA-F]

正则表达式中也可用一些固定的字符串表达特殊含义,它们均以\符号开头,通用的模式如下:

符号 功能
\< 匹配单词的开头位置
\> 匹配单词的结束位置
\b 匹配单词的边界(boundary),即单词开头或结束的位置
\B 匹配非单词开头或结束的位置
\d 匹配任意数字,等价于[:digit:]
\D 匹配任意非数字,等价于[^[:digit:]]
\s 匹配空白符,等价于[:blank:]
\S 匹配非空白符,等价于[^[:blank:]]
\w 匹配字符串,等价于[:alnum:]
\W 匹配非字符串,等价于[^[:alnum:]]
\x 匹配十六进制数字,等价于[:xdigit:]

当然,由于\号本身是元字符,因此在 R 中需要加以转义,所以上述模式需要写成形如\\d的模式。例如:

str_replace_all(a, "\\d", "x")

重复匹配

正则表达式的元字符一次一般只能匹配一个位置或一个字符。若想匹配零个、一个或多个字符时,需要使用限定符,用于指定允许特定字符或字符集重复出现的次数。

符号 功能
{n} 重复n
{n,} 重复至少n
{n,m} 重复至少n次、至多m
? 重复零次或1次
* 重复零次或多次
+ 重复1次或多次

查找顺序

符号 功能
() 定义子表达式,可使复杂的正则表达式更适合阅读
(?=) ?=号后的字符为起点(不包含该字符本身),向前查找匹配的模式
(?<=) ?<=号后的字符为起点(不包含该字符本身),向后查找匹配的模式
(?!) ?<=号后的字符为起点(不包含该字符本身),向前查找指定模式之外的字符,称为负向前查找
(?<!) ?<=号后的字符为起点(不包含该字符本身),向前查找指定模式之外的字符,称为负向后查找

负向前查找(negative lookahead)和负向后查找中的“负”,均表示逻辑上的“否”,即对向前查找和向后查找取反义,但查找顺序相同。这也是为什么负查找使用了!号的原因,它是 R 及许多程序语言中表示“否”的逻辑符号。

library(tibble)
library(rvest)
library(dplyr)
library(stringr)
url <-
  "http://www.moe.gov.cn/jyb_zzjg/moe_347/201508/t20150824_202647.html" 
cast <- read_html(url, encoding = "UTF-8") %>%  html_nodes(".pt105") 
http_links <- html_attr(cast, "href") 
universitiy_names <- html_text(cast)
unme <- cbind(universitiy_names, http_links) %>% as_tibble() 
write.csv("unme.csv")
str_count(unme$universitiy_names, "学院") %>% sum()
unme <- mutate(unme, collage = as.numeric(str_detect(universitiy_names, "学院")), 
               china = as.numeric(str_detect(universitiy_names, "中国")), 
               brother = as.numeric(str_detect(unme$universitiy_names, "[()]")), 
               abbrevs = str_extract(unme$http_links, "(?<=\\.).*(?=\\.e)"))

上述命令中的最后一行,str_extract(unme$http_links, "(?<=\\.).*(?=\\.e)")表示提取第一个.号之后、第一个.e之前的内容。这显然是因为所有教育部直属院校的域名均以.edu.cn结尾。这是一种“投机取巧”的做法。更为普遍的解法思路应是:如何提取某两个特定位置的.号之间的内容?就此例而言,就是如何提取第一个.号与第二个.号之间的内容(不包括.号本身)?

实现的方式并不统一。这里提供一种思路。

str_extract(unme$http_links[1:5], "(?<=\\.)([^\\.]+)(?=\\.)")

其中,

  • ?<=\\. 表示从第一个.号之后进行匹配
  • [^\\.]+ 表示至少匹配一个非.号的字符
  • ?=\\. 表示在第二个.号之前进行匹配
  • () 仅表示运算的优先性,以便阅读

请思考如下语句的结果:

str_extract(unme$http_links[1:5], "(?<=\\.)([^\\.]+)(?=\\.)")

此外,对于形如"http://www.pku.edu.cn/"这种结构清晰的字符串,也可先将其按规则切割成若干子字符串,然后再提取所需部分。此处按3个.号将http_links列切割为4列,再提取第2列即可。示例如下:

str_split_fixed(unme$http_links, "\\.", n = 4)[, 2][1:5]

其中,()外的第一个方括号[, 2]表示提取前述结果的第2列,第二个方括号[1:5]表示只列示前5列结果,以节省空间。

如果一个字符串有若干个相同符号(如这里的.号),提取第一个和第二个符号之间的内容,直接采用定位匹配的方式是比较方便的。而当所提取内容存在一系列符号中的某两个之间时,可能先切割再提取的方式更容易操作。