我理解的正则表达式

一、前言

当我们想以特定的规则从字符串中匹配出想要的子串时,正则表达式非常有用。而且,大部分编程语言都集成了正则表达式,我们学会它的语法规则后,可以直接在各种编程语言中运用。

正则表达式大致有以下应用:

  • 数据验证(例如:检查时间字符串是否格式正确)
  • 数据抓取(例如:网页抓取,以特定顺序查找包含特定单词集的所有页面)
  • 数据整理(例如:将原始数据转换为另一种格式)
  • 字符串解析(例如:捕获所有 URL GET 参数,捕获一组括号内的文本)
  • 字符串替换、语法高亮、文件重命名、数据包嗅探

在实际场景中,被匹配的字符串内容杂乱无章,但稍加分析,会发现其实质要么是 ASCII 码,要么就是实现了 unicode 的各种编码。unicode 兼容 ASCII,而这些编码有个特点,在特定的二进制范围中表示的字符有着相同的特性。比如 ASCII 中的 0-31 位表示一些控制字符,32-127 位则是一些可打印出来的字符,不一而足。

由此不难想见,我们可以针对不同类别字符的特性,制定一些规则。而正则表达式正是使用一些特殊的元字符构造出了自己的一套匹配规则,从而匹配出任意形式的数据。这些元字符本来没有什么特殊,但经过正则表达式引擎的编译,便具有了特殊的功能,看似不知所云,但实际记录了很多的信息。

正则通常用 // 包裹,后面跟上若干个匹配标志,比如:/a[bc]+/gm

一个正则表达式,大致遵循这样的编写思路:什么位置,什么样的关键词,有几个,怎么做,那么上面的这个正则可以这样解读:「以全局、多行的模式匹配出任意位置的这样一个字符串:a 后面跟着一个或多个 bc」。

下面按照这个思路分层次讲解,其中给出的用例可以使用这个工具测试。

二、编写思路

1. 什么位置

1.1 关键词位于行开头(^)或行结尾($

^ 习惯上读作 Cat$ 读作 Dollar

正则表达式含义说明
ok匹配字符串 ok
^ok匹配字符串 ok,而且 ok 前面是行开头。
ok$匹配字符串 ok,而且 ok 后面是行结尾。
^ok$匹配字符串 ok,而且 ok 前面是行开头,ok 后面是行结尾。

1.2 指定关键词前后的字符内容 (?=)(?<=)

正则表达式含义说明
(?<=r)okok 前面 r,结果不捕获 r
ok(?=r)ok 后面 r,结果不捕获 r

这里可以将 = 替换为 !,表示否定。

正则表达式含义说明
ok(?!r)ok 后面不是 r,但结果不捕获 r
(?<!r)okok 前面不是 r,但结果不捕获 r

2. 什么样的关键词

2.1 单字符关键词

2.1.1 限定单字符关键词的备选值:用 [] 包裹

正则表达式含义说明
[abc]包含 abc 中的一个,等价于 a|b|c
[a-c]同上。
[a-fA-F0-9]单个十六进制数字的字符串,且不区分大小写。
[0-9]%% 前面是一个数字。
[^a-zA-Z]不在 a~Z 的范围内。在这个例子中,^ 表示否定。

2.1.2 元字符转义

看到这里,我们知道 ^$<>()[]{}|/\.+*?:=! 这些元字符在正则中可能有着特殊的作用。那么这里会有个问题,正则表达式是用字符串来描述匹配规则,进而去匹配字符串。如果我们需要匹配这些元字符本身该怎么办呢?

这时可以在这些字符前添加转义符号 \,使其还原为字符本身,不再具备限定含义。

正则表达式含义说明
\$\d$ 后面跟着一个数字字符。

2.2 多字符关键词

2.2.1 构造关键词元组:用 () 包裹

之前的例子中,只能对单字符关键词进行限定,如果要对多字符关键词进行限定,可以用 () 括起来,构造成一个关键词元组,看下面的例子。

正则表达式含义说明
a(bc)a 后面跟着一个 bc
a(?:bc)*a 后面不能跟着一个 bc
a(?<foo>bc)a 后面跟着一个 bc,并将 bc 这个元组命名为 foo

当我们自己的编程语言从字符串或文本数据中匹配信息时,像这样构造关键词元组非常有用。这样的匹配结果会返回一个列表,从而方便我们根据下标去取值。

而如果使用的是上表的第三种,给关键词元组命名的方式,返回的结果则是字典。键的名称是组名 foo,值是匹配结果列表。

2.2.2 引用关键词元组:\1

正则表达式含义说明
([abc])\1引用第一个关键词元组 ([abc]) 匹配的相同文本。
([abc])([de])\2\1\2 引用第二个关键词元组 ([de]) 匹配的相同文本。
(?<foo>[abc])\k<foo>\k<foo> 引用前面名为 foo 的关键词元组 (?<foo>[abc]) 匹配的相同文本。结果与 ([abc])\1 相同。

2.3 特定类型关键词

正则表达式含义说明
.匹配任意字符,当匹配标志不是 s 时,不匹配 \n
\d匹配数字
\D匹配非数字
\b表示它的一边是单词字符,一边不是单词字符。如 \bok\bok 前面是 \n空格 这样的非单词字符,后面也是非单词字符。
\B匹配 \b 所有不匹配的内容。如 \Bok\Bok 前面是单词字符,后面也是单词字符。
\w匹配字母数字下划线
\W匹配非字母非数字非下划线
\s匹配空白字符,包括空格( 水平制表符(\t换行符(\n换页符(\f回车符(\r垂直制表符(\v空字符(\0
\S匹配非空白字符

类似 \d 这样的限定方法,当 \ 后面的字母为大写时,表示的限定规则恰好与小写相反。

3. 有几个

3.1 指定关键词的数量:*+?{}

符号含义数学区间
*匹配零个或多个x ∈ Zx = 1x ∈ [0, +∞)
+匹配一个或多个x ∈ Zx = 1x ∈ [1, +∞)
?匹配零个或一个x ∈ Zx = 0x = 1
{a,b}匹配 a~b 个。x ∈ Zx ∈ [a, b]

限定数量时,限定条件须写在关键词的后面,看下面的例子:

正则表达式含义说明
abc*ab 后面跟着零个或多个 c
abc+ab 后面跟着一个或多个 c
abc?ab 后面跟着零个或一个 c
abc{2}ab 后面跟着两个 c
abc{2,}ab 后面跟着两个或两个以上c
abc{2,5}ab 后面跟着两个到五个 c
a(bc)*ab 后面跟着零个或多个 bc
a(bc){2,5}ab 后面跟着两个到五个 bc

这里还可以加入操作符 |[],更灵活地指定数量。

正则表达式含义说明
a(b|c)匹配 a。要求 a 后面跟着 bc
a[bc]匹配 abac。要求 a 后面跟着 bc

这两者都要求 a 后面是备选词 bc,区别在于,前者不捕获备选词,后者捕获。

4. 怎么做

4.1 一些匹配标志码

在执行匹配时,可以通过设置标志码来改变匹配的模式:

标志码模式含义备注
g全局模式查找所有的匹配项。
m多行模式使边界字符 ^$ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
i忽略大小写将匹配设置为不区分大小写,搜索时不区分大小写: Aa 没有区别。
s单行模式在其他模式中,. 不匹配类似 \n 的控制字符,在这种模式下匹配
U非贪婪模式使用非贪婪模式匹配。

当未指定非贪婪模式时,上述的数量限定符号* + {},匹配时都是贪婪模式,即它们会尽可能地在提供的文本中匹配目标值。

例如,在 This is a <div> simple div </div> test 这个例子中,<.+> 匹配到 <div>simple div</div>

.+ 意为:一个或多个任意字符,这里它匹配了 div>simple div</div,但我们的目的明显是匹配出 div</div>

为了只捕获 div,可以在 .+ 之后添加 ?,让它用非贪婪模式执行:

正则表达式含义说明
<.+?> 匹配一次或多次 <> 中的内容,根据需要扩展。

在上述例子中,会匹配出 <div<> 这样的结果,为了更加严谨,应该尽量减少 . 的使用,可以优化为:

正则表达式含义说明
<[^<>]+>匹配一次或多次这样的字符:<> 中不含 <>

各种编程语言中的正则使用方法

这里列举 Python、Java、JavaScript、Golang 的正则使用方式,使用时替换正则表达式即可。

Python

import re
pattern = re.compile(ur'^[0-9]*$')
str = u'12345'
print(pattern.search(str))

Java

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexMatches {
	
	public static void main(String args[]) {
		String str = "";
		String pattern = "^[0-9]*$";

		Pattern r = Pattern.compile(pattern);
		Matcher m = r.matcher(str);
		System.out.println(m.matches());
	}

}

JavaScript

var pattern = /^[0-9]*$/g,
	str = '12345';
console.log(pattern.test(str));``

Golang

package main

import (
	"fmt"
	"regexp"
)

func main() {
	str := ""
	matched, err := regexp.MatchString("^[0-9]*$", str)
	fmt.Println(matched, err)
}

参考文章

点赞