Shallow Dream

Keep It Simple and Stupid!

0%

正则表达式

拜托,简短的一行,就能在大部分软件里快速实现匹配和替换目标词,这谁不心动啊!

几个 tips

模式 pattern

一般把一个正则表达式叫做模式

模式定义了一种模板

会在文本中匹配符合模式的字符串

多个匹配结果

绝大多数正则表达式默认返回第 1 个匹配结果,如果需要在全部文本中查找,需要查阅对应工具对正则表达式如何进行全局匹配

大小写敏感

单词 ekko 并不会对模式 Ekko 有什么反应

痛点

把必须匹配的情况考虑周全并写出一个匹配结果符合预期的正则表达式很容易

但把不需要匹配的情况也考虑周全,并确保它们都将被排除在匹配结果以外,往往要困难得多

换言之

用正则表达式匹配出来目标的字符串很容易,但是正则表达式会不会匹配出来其他意想不到的字符串,就不好判断了

良好的编码习惯

[\r]? :匹配零个或一个回车符,功能等价于 \r?

创建一个字符集合,仅包含 \r 字符

这样做的目的是为了增加可读性,一眼就能看出哪个字符和哪个相关联

匹配

匹配纯文本

  • Ekko :匹配 Ekko

在整个文本这个长字符串中,查找匹配 Ekko 这个字符串

注意:

匹配的并不总是整个字符串,而是与某个模式相匹配的字符,即使它们只是整个字符串的一部分

对于上面这个模式

aEkko

Ekkooooohohohoh

165asdweEkkoa9df51wq9

这些词都会被匹配

匹配任意字符

  • . :(英文句号)匹配任意一个单个字符

. 可以匹配任何单个字符,字母,数字甚至是自己 .

匹配特殊字符

一些字符在正则表达式中有着特殊的含义

如果我们需要使用这些字符本来的意思,那么就需要对这些特殊字符进行转义

转义: 使用 \ 对特殊字符进行转义

  • Ekko\. :匹配 Ekko. 这个字符串

匹配多个字符中的某一个

有时候我们只需要匹配 na sa 这样的而不需要 ca

此时只使用 . 也会将 ca 匹配出来

使用 [] 两个元字符来定义一个字符集合,字符集合的匹配结果是能够与该集合里的任意一个成员相匹配的文本

  • [ns]a :匹配第一个字母是 ns ,第二个字母是 a 的所有字符串
  • [Cc]ar :不区分大小写的匹配 car

匹配数字和字母

同样的逻辑,创建一个集合 [0123456789]

  • arr[0123456789]:匹配 arr[0]arr[1] 或 ... 或 arr[9]

那么匹配小写字母就只需要敲出

[abcdefghijkl...

等等,你认真的嘛?

当然不是,对于这种字符区间,为了简化,正则表达式提供了一个特殊的元字符 -

  • a-z :匹配一个字符,是小写字母
  • A-Z :匹配一个字符,是大写字母
  • 0-9 :匹配一个字符,是数字
  • a-f :匹配一个字符,是 af 这些字符中的字符
  • a-zA-Z0-9 :匹配一个字符,是大小写字母或者数字

注意:

这种字符区间,首字符 > 尾字符可能会匹配失败

取反匹配

达成匹配一个字符这件事有两种办法,一种是匹配合法字符,一种是排除所有不合法字符

  • [^0-9]:匹配一个字符,不能是数字

^ 效果作用于字符集合,表示取反的意思

元字符 metacharacter

转义

元字符表示这个字符有特殊含义,而不是字符本身含义

元字符如果需要表示本身含义,需要使用 \ 进行转义

  • array\[0\] :匹配 array[0] 字符串
  • array[0] :匹配 array0 字符串

需要转义使用的元字符

. ? [ ]
\ + *
{ }
( )

\\ 匹配 \,如果只写一个 \ ,可能会看到一个出错信息

正则表达式分析器会认为这样的正则表达式不完整

在一个完整的正则表达式里,字符 \ 后面永远跟着另一个字符

匹配空白字符

空白元字符

元字符 说明
[\b] 回退(并删除)一个字符 (Backspace 键)
\f 换页符
\n 换行符
\r 回车符
\t 制表符(Tab 键)
\v 垂直制表符

回车之所以叫回车,是因为以前的打字机,在打印到行末,需要将打印头移回行首

所以“回车 + 换行”

回车实现了将打印头移回行首

换行实现了将打印头移到下一行

现在往往用 \n 一个命令即可实现换行,并将光标移到行首的操作

  • \r\n :匹配一个 “回车 + 换行” 组合 (Windows 所使用的文本行结束标签)

匹配特定的字符类别

一些常用的字符集合可以用特殊的元字符来代替,从而简化我们的正则表达式

匹配数字

  • \d:匹配任何一个数字字符,等价于 [0-9]
  • \D:匹配任何一个非数字字符,等价于 [^0-9]

匹配字母和数字

  • \w :匹配任何一个数字或字母(不区分大小写)或下划线字符,等价于 [a-zA-Z0-9_]
  • \W :匹配任何一个非字母(不区分大小写)或数字或下划线字符,等价于 [^a-zA-Z0-9]

匹配空白字符

  • \s :匹配任何一个空白字符,等价于 [\f\n\r\t\v]
  • \S :匹配任何一个非空白字符,等价于 [^\f\n\r\t\v]

匹配十六进制或八进制

  • 添加 \x\x0A 对应于 10 ,代表 ASCII 码中 \n ,效果等价
  • 添加 \0\011 对应于 9 ,代表 ASCII 码中 \t ,效果等价

POSIX 字符类

许多(不是所有)正则表达式都支持的一种简写方式

字符类 说明
[:alnum:] 匹配任何一个字母或数字,等价于 [a-zA-Z0-9]
[:alpha:] 匹配任何一个字母,等价于 [a-zA-Z]
[:blank:] 匹配空格或制表符,等价于 [\t ]
[:cntrl:] 匹配任何一个 ASCII 控制字符(ASCII 0到31,再加 127)
[:digit:] 匹配任何一个数字,等价于 [0-9]
[:print:] 匹配任何一个可打印字符
[:graph:] [:print:] 一样,但是不包括空格
[:upper:] 匹配任何一个大写字母,等价于 [A-Z]
[:lower:] 匹配任何一个小写字母,等价于 [a-z]
[:punct:] 匹配既不属于 [:alnum:] 也不属于 [:cntrl:] 的任何一个字符
[:space:] 匹配任何一个空白字符,包括空格,等价于 [\f\n\r\t\v ]
[:xdigit:] 匹配一个十六进制数字,等价于 [a-fA-F0-9]

上面这些字符类,你应该看成是一个整体,也就是字符类中写的就是一个整体的字符

如果你需要用他们表示一个十六进制数的集合

你应该表示为 [[:xdigit:]][] 表示集合,里面的 [:xdigit:] 表示一个十六进制数

而不应该写成 [:xdigit:]

重复匹配

前面介绍了匹配操作,但都是对于一个字符的,如果我们希望匹配模式作用多次,就需要一些操作进行重复匹配

匹配一个或多个字符 +

  • [0-9]+ :匹配一个或多个数字字符

匹配零个或多个字符 *

匹配一个可有可无的字符该怎么办呢?

  • B.*Forta :匹配任何,第一个字母为 B,后面有或没有若干字符,后接 Forta 的字符串

匹配零个或一个字符 ?

匹配模式,出现 0 次或 1 次

  • https? :匹配 httphttps

匹配的重复次数

+*? ,能够解决很多问题,但是一些情况下,我们需要更加精确的重复次数

为重复匹配次数设定一个精确的值

  • a{6} :重复匹配 6 次 a 字符,等价于 aaaaaa
  • #[[:xdigit:]]{6} :匹配合法 RGB 值

为重复匹配次数设定一个区间

  • {2,4} :最少 2 次,最多 4 次
  • \d{1,2}[-\/]\d{1,2}[-\/]\d{2,4} :匹配 “日月年” 类型合法日期

建议使用 / 的时候,进行转义

匹配至少重复多少次

省略 {} 中的最大值部分即可

  • {3,} :匹配至少 3 次

小心,不要遗漏 ,

{3}{3,} 是完全不同的意思

防止过度匹配

+* 是一种贪婪型匹配,他会在满足要求的时候一直匹配,直到行末

为了限制“贪婪行为”,可以使用这些字符的“懒惰版本”(尽可能匹配少的字符

贪婪型元字符 懒惰型元字符
* *?
+ +?
{n,} {n,}?

位置匹配

字符可以出现在文本的任意位置

某些场合只需要对某段文本的特定位置进行匹配,这就需要位置匹配了

边界

边界限定符,也就是在正则表达式里,用一些特殊的元字符来表明我忙呢想让匹配操作在什么位置(边界)发生

单词边界

  • \b :匹配一个单词的开始或结尾
  • \bpro :匹配以 pro 前缀开头的单词
  • ous\b :匹配以 ous 后缀结尾的单词
  • \bdictionary\b :匹配一个完整单词 dictionary

\b 匹配的是单词边界,

一个用来构成单词的字符和一个不能用来构成单词的字符之间

\b 匹配且只匹配一个位置,不匹配任何字符

例如 \bcat\b 匹配的是 cat 3 个字符,而不是 5 个字符

  • \B :匹配一个不是单词边界
  • \B - \B :匹配一个左右两边不是单词边界的 - (连字符)

上面这个例子

nine-digitpass-key 中的 - (连字符)不会被匹配

只会匹配 color - coded 这里的 - (连字符)

字符串边界

字符串边界用来进行字符串有关的位置匹配(字符串开头,字符串结束,整个字符串)

  • ^ :匹配字符串开头
  • $ :匹配字符串结尾

^ 在之前能做到对字符集合取反,现在也能匹配字符串开头

这种元字符有多种用途

只有 ^ 出现在一个字符集合里 (被放在 [] 之间),表示求非的用途

如果是在一个字符集合的外面,并位于一个模式的开头, ^ 将匹配字符串的开头

  • ^\s*<\?xml.*?\?> :匹配合法 XML 文档开头

^ :匹配一个字符串的开头位置

^\s* :匹配一个字符串的开头位置和随后的零个或多个空白字符

.*? :匹配零个或多个任意字符,且是懒惰型

\s*$ :匹配一个字符串结尾处的零个或多个空白字符

分行匹配模式 multiline mode

^ 匹配一个字符串的开头,$ 匹配一个字符串的结尾

许多正则表达式支持使用一些特殊的元字符去改变另外一些元字符行为的做法

(?m) :记号就是一个能够改变其他元字符行为的元字符序列,启用分行匹配模式

(?m)^\s*//.*$ :匹配 // 注释后面的所有内容

(?m) 必须出现在整个模式的最前面

分行匹配模式将使得正则表达式引擎把行分隔符当做一个字符串分隔符来对待(目前大多数语言和编辑器默认启用多行模式)

该模式下:

^ 不仅匹配正常的字符串开头,还匹配行分隔符(换行符)后面的开始位置(这个位置不可见)

$ 不仅匹配正常的字符串结尾,还将匹配行分隔符(换行符)后面的结束位置

子表达式 subexpression

有些短语,虽然由多个单词构成,但其实是一个整体

为了让这些单词显示在同一行上,在这些短语的单词之间使用非换行型空格&nbsp;

如果我们现在想把 &nbsp; 连续两次或更多次的重复出现找出来

&nbsp;{2,} :这样的正则表达式,不能正确实现目标

因为 {2,} 前紧跟着 ;

所以这个模式只能匹配 ;;;;; 类似的字符串

为了解决这个问题,使用子表达式,将子表达式当作一个独立元素来使用

使用 () 括起来子表达式

  • (&nbsp;){2,} :匹配两个或更多个 &nbsp; 字符串
  • (\d{1,3}\.){3}\d{1,3} :匹配 IP 地址(不一定合法)
  • (19|20)\d{2} :匹配 19 开头或 20 开头的 4 位数年份

| 字符表示的关系

子表达式嵌套

子表达式允许多重嵌套

理论没有嵌套层数限制,但是过多的嵌套就意味着降低了正则表达式的可读性

对于 (\d{1,3}\.){3}\d{1,3} 这个正则表达式

他能够找出正确的 IP 地址

但是,同样能够找出 999.12.255.1 这样非法的 IP 地址

可惜,我们无法使用数学运算符

对于合法 IP 地址(每一组数都在 0 - 255 之间)

拆分匹配规则

  • 任何一个 1 位或 2 位数字(0 - 9,10 - 99)
  • 任何一个以 1 开头的 3 位数字(100 - 199)
  • 任何一个以 2 开头,第 2 位数字在 0 ~ 4 之间的 3 位数字(200 - 249)
  • 任何一个以 25 开头,第 3 位数字在 0 ~ 5 之间的 3 位数字 (250 - 255)

利用子表达式嵌套

  • (((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])) :匹配合法 IP 地址

回溯引用

前后一致匹配

回溯引用 backreference

对于文本中重复打错的部分

“... are are repeated, and and ...”

  • [ ]+(\w+)[ ]+\1 :匹配打错的重复的单词

[ ]+ :匹配多个空格

(\w+) :匹配一个单词,用子表达式表示,目的是为了后面方便引用

[ ]+ :匹配多个空格

\1 :回溯引用,引用的就是前面划分出来的子表达式 (\w+),就像是一个变量

  • <[hH]([1-6])>.*?</[hH]\1> :匹配合法 HTML 标签,<H1> </H1>

回溯引用匹配通常从 1 开始计数

许多实现里,第 0 个匹配(\0)可以用来代表整个正则表达式

替换操作

替换操作需要两个正则表达式

一个用来给出搜索模式

一个用来给出匹配文本的替换模式

回溯引用可以跨模式使用,在第一个模式里被匹配的子表达式可以用在第二个模式里

回溯引用语法在不同的正则表达式中语法有很大差异,例如

JavaScript 需要用 $ 代替 \

typora 也需要将 $ 代替 \

  • (\d{3})(-)(\d{3})(-)(\d{4}) :匹配类似于 555-333-6594 电话号码的字符串
  • ($1) $3-$5 :将上面的匹配结果,替换为这类形式 (555) 333-6594

大小写转换

元字符 说明
\E 结束 \L\U 转换(结束转换标记)
\l 把下一个字符转换为小写
\u 把下一个字符转换为大写
\L \L\E 之间的字符全部转换为小写(之后一直到结束标记前的字符)
\U \U\E 之间的字符全部转换为大写(之后一直到结束标记前的字符)

前后查找 lookaround

上面的正则表达式都是用来匹配文本的,但是有时候我们还需要用正则表达式标记要匹配的文本的位置(而不仅仅是文本本身)

这就引出了前后查找:对某一位置的前、后内容进行查找

向前查找 ?=

对于一系列网址

http://www.123.com/

https://mail.123.com/

ftp://ftp.123.com/

  • .+(:) :匹配 http:https:ftp:
  • .+(?=:) :只在 : 字符,向前匹配任意字符,匹配 httphttpsftp

并不会“消费”前后查找匹配的字符

向后查找 ?<=

对于一些价格

$23.45

$0.45

$23

  • [0-9.]+ :匹配所有的数字和小数点
  • (?<=\$)[0-9.]+ :只在 $ 字符,向后匹配数字和小数点

前后结合

你当然可以对一个特定字符向后查找,也可以对一个特定字符向前查找

<TITLE>abcdefg. Ekko. Com</TITLE>

  • (?<=(<[tT][iI][tT][lL][eE]>)).*(?=(</[tT][iI][tT][lL][eE]>)) :匹配位于 <TITLE></TITLE> 中的字符串

对前后查找取非

同样,我也可以不匹配某个字符后的数字

下面这个例子,我只希望匹配数量,而不是金额

I paid $40 for 100 apples, 50 oranges. I saved $5

  • \b(?<!\$)\d+\b :匹配所有不是在 $ 字符后的数字

注意这里的 \b 如果没有这个边界限制,那么 $400 也会被匹配

操作符 说明
?= 正向前查找
?! 负向前查找
?<= 正向后查找
?<! 负向后查找

嵌入条件

威力强大,但不经常用到

而且并不是所有正则表达式都支持

回溯引用条件

只在前一个子表达式搜索取得成功的情况下才允许使用一个表达式