正则表达式的基础

regex
stringr
Author

Lee

Published

March 30, 2023

1 正则基础知识

1.1 元字符和特殊字符

R中的元字符与其他编程语言有些许的不同,常用的元字符参见 表 1表 2

表 1: R中常用的元字符
符号 描述
. 匹配除换行符”/n”以外的任意字符
\ Z换义字符,匹配元字符时使用”\元字符\
^ 匹配字符串的开始
$ 匹配字符串的结束
( ) 提取匹配的字符串,即括号内的字符串看作一个整体
[ ] 匹配方括号内的任意一个字符
{ } 大括号前面的字符或表达式重复的次数:{n}表示重复n次,{n, }表示重复n到更多次,{n,m}表示重复n~m次
* 前面的字符或表达式重复0次或更多次
+ 前面的字符或表达式重复1次或更多次
? 前面的字符或表达式重复0次或1次
表 2: R中常用的特殊字符及反义
符号 描述
\d与\D 匹配数字,匹配非数字
\s与\S 匹配空白符,匹配非空白符
\w与\W 匹配字母或数字或下划线或汉字,匹配非\w的字符
\b与\B 匹配单词的开头或结束的位置,匹配非\b的位置
\h与\H 匹配水平间隔,匹配非水平间隔
\v与\V 匹配垂直间隔,匹配非垂直间隔
[^…] 匹配除……以外的任意字符

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

  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字符类的固有界定符)。

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 零宽断言

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

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

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

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

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

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

2.1.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.1.2 后行断言

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

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

2.2 贪婪匹配和懒惰匹配

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

例如:

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

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

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

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

2.3 分组与捕获

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

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

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

2.3.1 确定优先级

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

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

2.3.2 组成整体

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

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

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

2.4 捕获与向后引用

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