R语言编程:基于tidyverse
上QQ阅读APP看书,第一时间看更新

1.5 正则表达式

正则表达式是根据字符串规律按一定法则,简洁地表达一组字符串的表达式。正则表达式通常就是从貌似无规律的字符串中发现规律,进而概括性地表达它们所共有的规律或模式,以便于操作和处理它们,这是真正的化繁为简,以简驭繁的典范。

几乎所有的高级编程语言都支持正则表达式,正则表达式广泛应用于文本挖掘、数据预处理,例如:

检查文本中是否含有指定的特征词;

找出文本中匹配特征词的位置;

从文本中提取信息;

修改文本。

正则表达式包括只能匹配自身的普通字符(如英文字母、数字、标点等)和被转义了的特殊字符(称为“元字符”)。

1.5.1 基本语法

1.常用的元字符

正则表达式中常用的元字符如表1.3所示。

表1.3 常用的元字符

其他编程语言中的转义字符一般是“\”。默认情况下,正则表达式区分大小写,要创建忽略大小写的正则表达式,代码如下:

pat = fixed(pattern, ignore_case = FALSE)

在多行模式下,^$就表示行的开始和结束,创建多行模式的正则表达式的代码如下:

pat = regex(“^\\(.+?\\)$”, multiline = TRUE)

2.特殊字符类及其反义

正则表达式中常用的特殊字符及其反义如表1.4所示。

表1.4 特殊字符类及其反义

\\S+:匹配不包含空白符的字符串。

\\d:匹配数字,同[0-9]

[a-zA-Z0-9]:匹配字母和数字。

[\u4e00-\u9fa5]匹配汉字。

[^aeiou]:匹配除aeiou之外的任意字符,即匹配辅音字母。

3.POSIX字符类

正则表达式中还可以使用POSIX字符类,如表1.5所示。

表1.5 POSIX字符类

4.运算优先级

圆括号括起来的表达式最优先,其次是表示重复次数的操作(即“*”“+”“{ }”);再次是连接运算(即几个字符放在一起,如abc);最后是或运算(|)。

另外,正则表达式还有若干高级用法,常用的有零宽断言分组捕获,这些将在后面的实例中进行演示。

1.5.2 若干实例

以上正则表达式语法组合起来使用,就能产生非常强大的匹配效果,对于匹配到的内容,根据需要可以提取它们,也可以替换它们。

正则表达式与stringr包连用

若只是调试和查看正则表达式的匹配效果,可用str_view()及其_all后缀版本,匹配结果将在RStudio的Viewer窗口显示,在原字符向量中高亮显示匹配内容,非常直观。

若要提取正则表达式匹配到的内容,则用str_extract()及其_all后缀版本。

若要替换正则表达式匹配到的内容,则用str_replace()及其_all后缀版本。

使用正则表达式关键在于能够从貌似没有规律的字符串中发现规律性,再将规律性用正则表达式语法表示出来。下面看几个正则表达式比较实用的实例。

例1.1 直接匹配

该方法适合想要匹配的内容具有一定规律性,该规律性可用正则表达式表示出来。比如,数据中包含字母、符号、数值,我们想提取其中的数值,可以按正则表达式语法规则直接把要提取的部分表示出来:

x = c(“CDK弱(+)10%+”, “CDK(+)30%-”, “CDK(-)0+”, “CDK(++)60%*”)
str_view(x, “\\d+%”)
str_view(x, “\\d+%?”)

str_view()常用于调试正则表达式,匹配结果显示在Viewer窗口,如图1.12所示。

图1.12 Viewer窗口显示匹配效果

\\d表示匹配一位数字,+表示前面数字重复1次或多次,%原样匹配%

若后面不加“?”则必须匹配到%才会成功,故第3个字符串就不能成功匹配;若后面加上“?”则表示匹配前面的%0次或1次,从而能成功匹配到第3个字符串。

例1.2 用零宽断言匹配两个标志之间的内容

该方法适合想要匹配的内容没有规律性,但该内容位于两个有规律性的标志之间,标志也可以是开始和结束。

通常想要匹配的内容不包含两边的“标志”,这就需要用零宽断言。简单来说,就是引导语法既要匹配到“标志”,但又不包含“标志”。左边标志的引导语法是(?<=标志),右边标志的引导语法是(?=标志),而把真正要匹配的内容放在它们中间。

比如,来自问卷星“来自IP”数据,想要提取IP和地址信息。

x = c(“175.10.237.40(湖南-长沙)”, “114.243.12.168(北京-北京)”,
"125.211.78.251(黑龙江-哈尔滨)”)
# 提取省份
str_extract(x, “\\(.*-”)           # 此处作为对比,不用零宽断言
## [1] “(湖南-”   “(北京-”   “(黑龙江-”
str_extract(x, “(?<=\\().*(?=-)”)  # 用零宽断言
## [1] “湖南”   “北京”   “黑龙江”
# 提取IP
# str_extract(x, “\\d.*\\d”)       # 直接匹配
str_extract(x, “^.*(?=\\()”)       # 用零宽断言
## [1] “175.10.237.40”  “114.243.12.168” “125.211.78.251”

省份(或直辖市)位于两个标志“(”和“-”之间,但又不包含该标志,这就需要用到零宽断言。

IP位于两个标志“开始”和“(”之间,左边用开始符号^,右边用零宽断言。

再比如,用零宽断言提取专业信息(位于“级”和数字之间):

x = c(“18级能源动力工程2班”, “19级统计学1班”)
str_extract(x, “(?<=级).*?(?=[0-9])”)
## [1] “能源动力工程” “统计学”

再看两个的复杂的零宽断言,涉及出现次数。例如,提取句子中的最后一个单词:

x = c(“I am a teacher”, “She is a beautiful girl”)
str_extract(x, “(?<= )[^ ]+$”)
## [1] “teacher” “girl”  

零宽断言以空格为左标志,匹配内容是非空格出现1次或多次直到结尾,结果就是作为左标志的空格只能是句子中的最后一个空格。

再比如,提取以“kc/”为左标志,直到第3个下划线之前的内容:

x = “D:/paper/1.65_kc_ndvi/kc/forest_kc_historical_ACCESS-ESM1-5_west_1981_2014.tif”
str_extract(x, “(?<=kc/)([^_]+_){2}[^_]+”)
## [1] “forest_kc_historical”

匹配内容是:非下划线出现1次或多次(即1个单词)接1个下划线,上述部分重复2次,再接一个非下划线出现1次或多次(即1个单词),结果就是恰好匹配到第3个下划线出现之前。

关于懒惰匹配

正则表达式通常都是贪婪匹配,即重复直到文本中能匹配的最长范围,例如匹配小括号:

str_extract(“(1st) other (2nd)”, “\\(.+\\)”)
## [1] “(1st) other (2nd)”

若想只匹配到第1个右小括号,则需要懒惰匹配,在重复匹配后面加上“?”即可:

str_extract(“(1st) other (2nd)”, “\\(.+?\\)”)
## [1] “(1st)”

例1.3 分组捕获

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

确定优先规则;

组成一个整体;

拆分出整个匹配中的部分内容(称为捕获);

捕获内容供后续引用或者替换。

比如,来自瓜子二手车的数据:若汽车型号是中文,则品牌与型号中间有空格;若汽车型号为英文或数字,则品牌与型号中间没有空格。

若用正则表达式匹配“字母或数字”并分组,然后捕获该分组内容并添加空格以替换原内容,代码如下所示:

x = c(“宝马X3 2016款”, “大众 速腾2017款”, “宝马3系2012款”)
str_replace(x, “([a-zA-Z0-9])”, “ \\1”)
## [1] “宝马 X3 2016款” “大众 速腾 2017款” “宝马 3系 2012款”

后续操作就可以用空格拆分列(见2.4.4节)。

现有6位数字表示的时分秒数据,想用lubridate::hms()解析成时间类型,但是在时分秒之间用冒号或空格分隔才能正确解析。下面分组捕获两组数字,并分别替换为该两位数字加冒号,然后再解析成时间类型:

x = c(“194631”, “174223”) #数值型也可以
x = str_replace_all(x, “(\\d{2})”, “ \\1:”)
x
## [1] “19:46:31:” “17:42:23:”
hms(x)
## [1] “19H 46M 31S” “17H 42M 23S”

更多分组的引用还有\\2\\3等。例如,纠正电影的年份和国别出现顺序不一致的情况,可以通过代码统一将信息转换成“国别_年份”,代码如下所示:

x = c(“独行月球2022_Chinese”,”蜘蛛侠USA_2021”,”人生大事2022_Chinese”)
str_replace(x, “(\\d+)_(.+)”,”\\2_\\1”)
## [1] “独行月球Chinese_2022” “蜘蛛侠USA_2021” “人生大事Chinese_2022”

最后,再推荐一个来自GitHub的包inferregex,该包可以推断正则表达式,用函数infer_regex()可根据字符串推断正则表达式。