跳到主要内容

随时随地手写正则表达式

· 阅读需 10 分钟
Dylan Li

正则表达式常常被认为是一种现用现取的工具,开发者会忽视其潜在的场景和应用。实际上,几乎所有的开发语言均对其进行了差异化的实现,基于这一点,它被广泛应用于数据校验、 数据处理等场景。 从某种意义上说,正则表达式已经不是一种现用现取的工具,而是每个开发者必须要具备的一项基础素质。

应用场景

在开发API时判断请求参数中的手机号是否合法,使用数据爬虫爬取网页中某个标签下的数据,对敏感数据进行检测与处理,采集日志时根据格式提取关键信息等,这些场景都需要使用正则表达式来提高 处理的效率。概括来说主要分为两方面:字符串格式校验字符串提取

字符串格式校验

一次性判断一整个字符串是否符合某种格式。比如用^[0-9]+$来校验字符串是否只包含数字,用^[a-zA-Z0-9]+@gmail\.com$来校验是否是谷歌邮箱等。

字符串提取

根据某种格式从一段字符串中提取出符合要求的字串。比如使用[a-z]+hey, welcome to 中国里提取出所有的英文单词:hey、welcome和to。

细节

元字符(Meta Characters)

正则表达式主要依赖于元字符。元字符不代表他们本身的字面意思,他们都有特殊的含义,可以类比开发语言中的关键字。一些元字符写在方括号[]中的时候有一些特殊的意思。

元字符描述
.匹配任意字符,除了换行符
[ ]匹配方括号内的任意字符
[^ ]匹配除了方括号里的任意字符
*连续匹配 >=0 个重复的在 * 号之前的字符
+连续匹配 >=1 个重复的在 + 号之前的字符
{ }连续匹配 num 个大括号之前的字符或字符集,写法包括{n,m}、{n}、{n,},分别匹配 [n,m]、n、[n,+∞) 个
?标记 ? 之前的字符为可选,或者惰性匹配时使用
( )分组,按组匹配符合括号内正则表达式的子字符集
|或运算符,匹配符号前或后的字符
\ 转义字符,用于匹配一些保留的字符 [ ] ( ) { } . * + ? ^ $ \ |
^前定界符,从第一个字符开始匹配
$后定界符,匹配到最后一个字符为止

简写字符集(Shorthand Character Sets)

一些常用的字符集可以通过简写的方式进行匹配。

简写描述
\w匹配字母、数字和下划线,等价于:[a-zA-Z0-9_]
\W匹配非字母、非数字和非下划线的字符,即符号,等价于:a-zA-Z0-9_\w
\s匹配空格字符,包括制表符、换行符、换页符、回车符号等,等价于:[\t\n\f\r\p{Z}]
\S匹配非空格字符,等价于:\t\n\f\r\p{Z}\s
\d匹配数字,等价于:[0-9]
\D匹配非数字:0-9\d
\f匹配换页符
\n匹配换行符
\r匹配回车符
\t匹配制表符
\v匹配垂直制表符
\p匹配 CR/LF(等价于 \r\n),用来匹配 DOS 行终止符
.虽然属于元字符,但严格意义上也属于一种简写字符集

补充:\p{Z} 或 \p{Separator},指任何类型的空格或不可见的分隔符,极少使用,了解即可

断言(Lookarounds)

断言主要分为两种:

  • 先行断言:断言部分位于某个表达式之前,用来修饰后者的判断条件,在某个子字符串满足正则部分的前提下,该子字符串紧邻前面的字符另需满足断言部分。
  • 后发断言:断言部分位于某个表达式之后,用来修饰后者的判断条件,在某个子字符串满足正则部分的前提下,该子字符串紧邻后面的字符另需满足断言部分。

捕获与非捕获:

  • 捕获:捕获文本,且对组合(同"分组"概念)进行计数。形象点说,就是会提取出匹配某个正则表达式的字符串,该表达式既可以是普通表达式,也可以是断言表达式。
  • 非捕获:不捕获文本,也不针对组合进行计数。形象解释,指不会提取出匹配某个正则表达式的字符串,这个正则表达式往往属于断言表达式。

常用断言一览:

断言类型断言匹配的字符是否被捕获举例描述
?=后发断言a(?=bc)字符a后紧邻的字符串是bc
?!后发断言a(?!bc)字符a后紧邻的字符串不是bc
?<=先行断言(?<=bc)a字符a前紧邻的字符串是bc
?<!先行断言(?<!bc)a字符a前紧邻的字符串不是bc
\b后发断言、先行断言a\b、\ba边界断言,指断言位置存在边界,边界:各类空格、整个待匹配字符串的头尾边界
\B后发断言、先行断言a\B、\Ba非边界断言,与 \b 相反,指断言位置不存在边界,而是其他字符
?:后发断言、先行断言a(?:bc)、(?:bc)a约等价于 ?= 和 ?<= ,不同是断言匹配的字符会被捕获

标志(Flags)

标志也叫模式修饰符,因为它可以用来修饰表达式,进而影响匹配结果。这些标志可以任意地互相组合起来使用,也可以单独使用, 在格式上遵循 /{regex}/{flags} ,例如 /[abc]+/gm。一般情况下,不需要开发者在 regex 串中设置,而是在方法的调用入参或者调用实例的属性中设置, 例如 Java 中 java.util.regex.Pattern 类的 compile 方法,就有一种支持 flags 参数的多态化实现。

/**
* Compiles the given regular expression into a pattern with the given
* flags.
*
* @param regex
* The expression to be compiled
*
* @param flags
* Match flags, a bit mask that may include
* {@link #CASE_INSENSITIVE}, {@link #MULTILINE}, {@link #DOTALL},
* {@link #UNICODE_CASE}, {@link #CANON_EQ}, {@link #UNIX_LINES},
* {@link #LITERAL}, {@link #UNICODE_CHARACTER_CLASS}
* and {@link #COMMENTS}
*
* @return the given regular expression compiled into a pattern with the given flags
* .......
*/
public static Pattern compile(String regex, int flags) {
return new Pattern(regex, flags);
}

常见的标志有这些:

标志描述
i忽略大小写
g全局搜索
m多行修饰符:元字符 ^ $ 工作范围在每行的起始
s单行修饰符
x忽律空格符
u忽律大小写,同时对 Unicode 编码字符集生效。不使用该标志时,默认只匹配 US-ASCII 字符集中的字符
U启用对预定义类的 Unicode 支持

贪婪匹配与惰性匹配(Greedy Matching & Lazy Matching)

正则表达式默认采用贪婪匹配模式,在该模式下意味着会匹配尽可能长的子串。我们也可以使用 ? 将贪婪匹配模式转化为惰性匹配模式。两者的不同可以通过下面的例子了解:

输入字符串正则表达式捕获到的子串
hahahaha(ha)+hahahaha
hahahaha(ha)+?ha、ha、ha、ha

Java 中的差异化使用

  • \转义字符必须替换为\\使用。
  • 在支持匿名捕获的基础上,还支持命名式捕获,格式如 (?<name>:asd),可以通过指定 name 的方式提取数据,类似于 K-V 数据中的 K。