最简R语言教程(六)

正则表达式

R语言
正则表达式
regex
Author

Lee

Published

May 9, 2026

正则表达式(Regular Expression,简称regex)是一种用于匹配字符串的模式,它使用特定语法描述字符的规律。它可以帮助我们从看似杂乱的文本中查找、替换和提取特定的字符或字符串,从而实现文本的批量处理。正则表达式在数据清洗、文本处理和自然语言处理等领域非常有用。

例如,普通查找只能精确匹配”张三”,而正则表达式通过模式匹配,可以找出所有”姓张的人”

这篇文章 有助于我们更好的理解正则表达式。

正则表达式的示意图

主流编程语言都支持正则表达式,它广泛用于文本处理,例如:

正则表达式学习建议:

1 正则表达式的基本语法

正则表达式的本质是字符串,由普通字符(字母、数字、标点等)和特殊字符(元字符)组成。元字符具有特殊含义,用于定义匹配模式。

想要使用特殊字符的原本含义,需要先进行转义
Note

正则表达式作为表达式同样有运算优先级规则:

  • 括号分组:圆括号 () 内的表达式优先级最高,用于明确子表达式的边界。
  • 重复操作符:量词 *、+、{}(控制前一项的重复次数)次之。
  • 字符连接:多个字符连续排列时(如 abc),按顺序隐式连接匹配。
  • 或运算| 表示的分支选择(或运算)优先级最低。

stringr全面支持正则表达式,并且提供了更简洁的函数接口,推荐使用。以下是一些常用函数:

1.1 元字符和特殊字符

常用元字符

特殊字符类与反义

在了解元字符和特殊字符之后,我们就可以利用它们书写一些简单的正则表达式了:

  1. 匹配有abc开头的字符串:^abc

  2. 匹配8位数字的QQ号码:

    • ^\\d\\d\\d\\d\\d\\d\\d\\d$
    • ^\\d{8}$
  3. 匹配1开头11位数字的手机号码:

    • ^1\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d$
    • ^1\\d{10}$
  4. 匹配以a开头、0个或多个b结尾的字符串:^ab*b$

1.2 POSIX字符类

在模式中方括号内可以用[:alpha:] 表示任意一个字母。 比如,[[:alpha:]]匹配任意一个字母(外层的方括号表示字符集合, 内层的方括号是POSIX字符类的固有界定符)。

POSIX字符类

1.3 分组

上面的例4中,限定符 * 是作用在它左边最近的一个字符,如果想ab同时被*限定那怎么办呢?

正则表达式中用小括号()来做分组,也就是括号中的内容作为一个整体。

因此当我们要匹配多个ab时,我们可以这样写:^(ab)*$

1.4 转义

我们看到正则表达式用小括号来做分组,那么问题来了:

如果要匹配的字符串中本身就包含小括号,那是不是冲突?应该怎么办?

针对这种情况,正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简答,就是在要转义的字符前面加个斜杠,也就是\即可。值得注意的是,R语言中的转义符号为\\ 如:要匹配以(ab)开头,需要用如下表达式:^(\\(ab\\))$

1.5 条件-或

回到我们刚才的手机号匹配,我们都知道:国内号码都来自三大网,它们都有属于自己的号段,比如联通有130/131/132/155/156/185/186/145/176等号段,假如让我们匹配一个联通的号码,那按照我们目前所学到的正则,应该无从下手的,因为这里包含了一些并列的条件,也就是”或”,那么在正则中是如何表示”或”的呢?

正则表达式中用符号|来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。

那么我们可以使用或条件来处理这个问题:

^(130|131|132|155|156|185|186|145|176)\d{8}$

1.6 区间

看到上面的例子,我们发现还是有些复杂,是否可以再简化?

实际上,

正则表达式中提供了一个元字符[]表示区间,来匹配方括号内的任意字符。

  1. 限定0到9可以写成[0-9]
  2. 限定字母A到Z可以写成[A-Z]
  3. 限定某些数字[165]

那么上述的联通电话号码的正则表达式可以简化为:

^(13[0-2]|15[5-6]|18[5-6]|145|176)\d{8}$

以上就是正则表达式的基本用法。下面我们需要了解一些进阶的用法。

2 正则进阶知识点

2.1 直接匹配

当目标内容有明确规律时(例如提取数字),直接编写正则表达式描述模式即可实现匹配,如\\d+匹配连续的数字。

x <- c("CDK弱(+)10%+", "CDK(+)30%-", "CDK(-)0+", "CDK(++)60%*")
str_view(x, "\\d+%?") # 匹配数字后跟百分号的模式,第三个匹配不到0
[1] │ CDK弱(+)<10%>+
[2] │ CDK(+)<30%>-
[3] │ CDK(-)<0>+
[4] │ CDK(++)<60%>*

2.1.1 贪婪匹配和懒惰匹配

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

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

例如:

str_view("(1st) other (2nd)", "\\(.+\\)")
[1] │ <(1st) other (2nd)>

我们本来期望的是提取两个”(1st)“和”(2nd)“组合, 不料整个地提取了”(1st) other (2nd)“。 这就是因为.+的贪婪匹配。 如果要求尽可能短的匹配, 使用*?, +?, {3,}?等”懒惰型”重复模式。 在无上限重复标志后面加问号表示懒惰性重复。 那么上述的正则表达式可以改写为:

str_view_all("(1st) other (2nd)", "\\(.+?\\)")
[1] │ <(1st)> other <(2nd)>

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

2.2 零宽断言

当目标内容无规律,但被有规律的边界标志包裹时(如提取括号内的内容),用零宽断言匹配不含边界的部分

  • 肯定断言(内容紧邻指定边界):(?<=左标志)内容(?=右标志)
  • 否定断言(内容不紧邻指定边界):(?<!左标志)内容(?!右标志)

零宽断言可划分为两个词:零宽和断言。

  1. 断言:就是我断定什么什么,即指明在指定的内容前面或后面会出现满足指定规则的内容。例如,ss1aa2bb3,正则可以用断言找出aa2后面有bb3,也可以找出aa2前面有ss1
  2. 零宽:就是没有宽度 ,表明断言只是匹配位置,不占字符,即匹配结果不会返回断言本身。

了解了定义后,我们还是通过例子来简单说明。

假设我们要用爬虫抓取csdn里的文章阅读量。通过查看源代码可以看到文章阅读量这个内容是这样的结构:

“<span class=”read-count”>阅读数:641</span>”

其中,641这个是变量,对应不同文章的不同值。当我们拿到这个字符串时,需要获得其中641的部分的正则表达式该如何匹配呢?

2.2.1 先行断言

  1. 语法:?=pattern
  2. 作用:匹配pattern表达式 前面的内容,不返回本身。

回到例子本身,我们要匹配641这个数字,意味着我们要匹配</span>前面的数字内容。

string_test <- "<span class=\"read-count\">阅读数:641</span>"
str_extract(string_test, ".+(?=</span>)")
[1] "<span class=\"read-count\">阅读数:641"

好像不对,我们只想要641的部分。

str_extract(string_test, "\\d+(?=</span>)")
[1] "641"

2.2.2 后行断言

有先行就有后行,先行是匹配前面的内容,那后行就是匹配后面的内容啦。 我们注意到,641也可以是表达式之后的内容,同样也可以用后行断言来处理。

  1. 语法:(?<=pattern)
  2. 作用:匹配pattern表达式之后的内容,同样不返回本身。
str_extract(string_test, "(?<=<span class=\"read-count\">阅读数:)\\d+")
[1] "641"

2.2.3 零断宽言实例

x1 <- c(
  "175.10.237.40(湖南-长沙)",
  "114.243.12.168(北京-北京)",
  "125.211.78.251(黑龙江-哈尔滨)"
)

# 提取省份
str_extract(x1, "(?<=\\().*?(?=-)")
[1] "湖南"   "北京"   "黑龙江"
# 提取IP地址
str_extract(x1, "^.*(?=\\()")
[1] "175.10.237.40"  "114.243.12.168" "125.211.78.251"
# 提取专业
x2 <- c("18级能源动力工程2班", "19级统计学1班")
str_extract(x2, "(?<=级).*?(?=[0-9])")
[1] "能源动力工程" "统计学"      
# 提取最后一个词
x3 <- c("I am a teacher", "She is a beautiful girl")
str_extract(x3, "(?<= )[^ ]+$")
[1] "teacher" "girl"   
# 提取以“kc/”为左标志, 直到第3个下划线之前的内容
x4 <- "D:/paper/1.65_kc_ndvi/kc/forest_kc_historical_ACCESS-ESM1-5_west_1981_2014.tif"
str_extract(x4, "(?<=kc/)([^._$]+_){2}[^_]+")
[1] "forest_kc_historical"

上述提取IP地址的代码中:

  • ^ 表示从字符串开头开始,
  • .* 表示尽可能多地匹配任意字符,
  • 后面的 (?=\\() 是一个正向的先行断言,意思是 后面必须紧跟一个左括号,但这个左括号不被真正包含在结果里。因为在 R 字符串里,\\( 才表示正则中的字面量 (,所以需要双重转义。

上述提取kc/后内容的代码中:

  • (?<=kc/)零宽后行断言,要求匹配位置前面紧跟 kc/ 但不包含它;
  • ([^_]+_) 匹配一个或多个非下划线字符后跟一个下划线;
  • {2} 表示前面的括号组恰好重复两次(即前两段带下划线的部分);
  • [^_]+ 则匹配第三段的非下划线字符直到下一个下划线或字符串结束。
^ 出现在字符类 [] 外部时,表示匹配字符串的开头(或行首,在多行模式下);当 ^ 出现在字符类 [] 内的起始位置时,表示否定——匹配除所列字符之外的任何字符。

2.3 分组与捕获

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

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

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

2.3.1 确定优先级

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

正则表达式有比较复杂的优先级规则, 所以在不确定那些模式组合优先采纳时, 应该使用圆括号分组, 确定运算优先级。

2.3.2 组成整体

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

str_view(c("x01x02x", "_x11x9"), "(x[[:digit:]]{2})+")
[1] │ <x01x02>x
[2] │ _<x11>x9
str_extract(c("x01x02x", "_x11x9"), "(x[[:digit:]]{2})+")
[1] "x01x02" "x11"   

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

2.4 捕获与向后引用

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

# 分组捕获型号(字母或数字)添加空格替换原内容, 后续可接按空格拆分列
x <- c("宝马X3 2016款", "大众 速腾2017款", "宝马3系2012款")
str_replace(x, "([a-zA-Z0-9])", " \\1") # 替换部分 " \\1" 中的 \\1 表示引用第 1 个捕获组,也就是把之前匹配到的那个字符原样放回去
[1] "宝马 X3 2016款"   "大众 速腾 2017款" "宝马 3系2012款"  
# 时分秒中间插入冒号, 后续可接hms()解析
x <- c("194631", "174223") 
str_replace_all(x, "(\\d{2})", " \\1:")
[1] " 19: 46: 31:" " 17: 42: 23:"
# 纠正年份和国别顺序不一致问题, 年份在国别前则交换
x <- c("独行月球2022_Chinese","蜘蛛侠USA_2021","人生大事2022_Chinese")
str_replace_all(x, "(\\d+)_(.+)", "\\2_\\1")
[1] "独行月球Chinese_2022" "蜘蛛侠USA_2021"       "人生大事Chinese_2022"