深入浅出正则表达式

273 阅读3分钟

背景

最近主要做一些数据抽取/清洗相关的事情,所以正则表达式在字符串的处理上比较多,以前对正则这块的学习还是比较浅层,有些地方是没有学习全面的,所以借这次写文章的机会,把正则整体的梳理一遍。

文章脉络

  • 正则表达式
    • 匹配(一般用于查找)
      • 普通元字符
      • 特殊元字符
        • 位置限定符
        • 逻辑限定符
        • 重复限定符
        • 范围限定符
    • 捕获(一般用于取值/替换)
      • 分组
      • 引用

第一部分:匹配(一般用于查找)

正则表达式由普通元字符特殊元字符构成,普通元字符也就是我们日常的字母数字。特殊元字符则是具备特殊含义的字符,用于匹配一类字符。另外还有一类比较特殊的元字符叫做限定符,用于限定匹配位置或者特定区间的字符再或者是字符重复次数。

1.普通元字符

元字符说明
.匹配除换行符以外的任意字符
\w匹配字母 数字 下划线 汉字
\s匹配任意空白字符
\d匹配数字
\f匹配一个换页符
\n匹配一个换行符
\r匹配一个回车符
pattern匹配pattern 字符,这里的pattern代表的就是普通的字符
这类字符主要用于匹配一类字符
  • 如果匹配的字符跟正则的元字符冲突,那么需要转义处理加个\号就可以了(js只要加一个但是java需要2个,具体看语言实现的正则引擎处理方式)。
  • 如果要进行反义操作,一般是把元字符改成大写。如:\s是任意空白字符,那么\S是非空白字符,\d匹配数字\D匹配非数字。当然也可以通过[^\s]或者[^\d]进行操作,这里的[]区间限定符和^逻辑限定符是接下来要说的。

2.特殊元字符

特殊元字符我这边按具体用途做了分类主要为:

  • 位置限定符
  • 逻辑限定符
  • 重复限定符
  • 范围限定符
2.1 位置限定符
限定符说明
^匹配行的首部
$匹配行的尾部
\b匹配一个单词的边界,也就是指单词和空格间的位置
2.2 逻辑限定符
限定符说明
|逻辑或
逻辑非

一般逻辑限定符要跟范围限定符[]一起使用,如:

  • [x|y]表示匹配x或者y字符
  • [^\d]表示非数字

位置限定符^跟逻辑限定符^虽然符号相同,但是逻辑限定符^一般与范围限定符[]一起使用,位置限定符一般是在范围限定符号[]外使用。如:

  • ^[x|y]表示匹配x或者y开头的字符
  • ^[^\d]表示匹配非数字开头的字符

2.3 重复限定符

由于普通的元字符只适合描述单个字符,并没有次数的概念,所以一下的重复限定符用于描述匹配单词的次数。这里存在贪婪匹配模式和非贪婪匹配模式。

限定符说明
*重复0次或者多次
+重复1次或者多次
重复0次或1次
{n}重复n次
{n,m}重复n次到m次
{n,}重复n次或者多次
举个例子如匹配两个数字(表达式为\d{2}),那么结果如下:

接下来要讲重复限定符匹配中贪婪匹配和非贪婪匹配。如字面上的意思:

  • 贪婪匹配:尽可能多的匹配
  • 非贪婪匹配:尽可能少的匹配

默认情况下,所有的重复限定符都是贪婪匹配的。例子如下:

\d+会匹配1234567890所有的字符,匹配的结果就是["1234567890"],这个匹配模式是贪婪的。有的时候我们想尽可能少范围地匹配,那么就是以非贪婪的模式匹配。*非贪婪匹配只需要在重复限定符 ,+,?,{n},{n,},{n,m} 后面加多个?号,即匹配就是非贪婪模式。如\d+?匹配1234567890出来的结果就是1,总共匹配10次结果(尽可能少的匹配,那么能够匹配到的结果越多)。

2.4 范围限定符

范围限定符主要跟范围有关系,主要限定在某些字符或者某些词前或者词后。那么就需要用到范围限定符。

限定符说明例子
-用于配合字母或者数字匹配a到z或者A到Z或者0到9区间字符a-z A-Z 0-9
[pattern]匹配[]内的每一个字符[\d|a-z] 匹配小写字母或者数字
还有一种比较特殊的范围限定符称之为零宽断言(lookaround/zero-width assersion)
限定符说明简单理解
------------
(?=pattern)零宽正向先行断言匹配 pattern 表达式的前面内容(左边),不返回本身
(?!pattern)零宽负向先行断言匹配非 pattern 表达式的前面内容(左边)内容,不返回本身。
(?<=pattern)零宽正回顾后发断言匹配 pattern 表达式的后面内容(右边)的内容,不返回本身。
(?<!pattern)零宽负回顾后发断言匹配非 pattern 表达式的后面内容(右边)内容,不返回本身。

为了便于理解这里我先简单列个文本:

爱我中华大地,美丽的中华儿女

零宽正向先行断言(?=pattern):中华(?=大地) 匹配"大地"前的“中华”

零宽负向先行断言(?!pattern):中华(?!大地) 匹配不是"大地"前面的“中华”(也就是儿女前的中华)

零宽正回顾后发断言(?<=pattern):(?<=爱我)中华 匹配"爱我"后面的“中华”

零宽负回顾后发断言(?<!pattern): (?<!爱我)中华 匹配不是"爱我"后面的“中华”

第二部分:捕获(一般用于取值/替换)

如果说第一部分讲的是用来查找查找匹配的文本,那么第二部分讲的就是怎么从匹配结果中捕获需要的内容,这里就不得提下分组以及分组完后怎么取引用分组的值。

1.分组

元字符说明
(pattern)匹配pattern并获取这一匹配,所获取的匹配将会放入匹配结果集合中.
(?:pattern)匹配pattern并获取这一匹配,所获取的匹配不会放入匹配结果集合中.
(?pattern)匹配pattern并获取这一匹配,所获取的匹配将会放入匹配结果集合中,并且命名该分组为name。

2.引用

引用是基于分组捕获结果而言的,如下两种表达式,匹配到的分组结果不同

表达式引用值
(pattern)匹配结果为数字编号组,正则表达式里引用方式:\k 或\number
(?pattern)匹配结果为命名编号组,正则表达式里引用方式:\k 或者\name

(1)正则表达式内反向引用场景:

aabbbbgbddesddfiid

目的:查找字符串里成对的字母 正则表达式:(\w)\1

(2)替换引用分组值场景:

爱我中华大地,美丽的中华儿女

正则:(爱我中华)(大地)(,)(美丽的)(中华儿女) 替换:44233155 (符号为vscode的取匹配结果的方式)

替换前:

替换后:

总结

整体来说正则学习还是相对容易,主要日常过程中处理字符串这块应该经常用正则来处理,并且灵活结合这块各种元字符来满足自己的匹配结果,最后要了解怎么利用分组来进行对结果的内容进行抽取。另外本文的只考虑目前JavaScript使用的正则引擎,所以对正则表达式的支持也建立在这之上。不同正则引擎对正则的实现不同,所以不同语言使用的正则引擎不同也会影响最终的结果,但是对日常使用的部分应该是相同的。引擎以及语言的支持程度可以google查询。