正则表达式入门
正则表达式入门
简单地说,正则表达式(regular expression)是一些用来匹配和处理文本的字符串。它是内置于其他语言或软件产品里的“迷你”语言。语法是正则表达式最容易掌握的部分,真正的挑战在于如何运用语法把实际问题分解为可由正则表达式解决的子问题。
正则在线测试工具:regexper、regulex、regex101
匹配一个或多个字符
.
字符可以匹配任意单个字符、字母、数字甚至是.
字符本身。
正则表达式使用字符串内容来匹配模式,匹配到的未必总是整个字符串,也可能是与某个模式相匹配的子串。
文本
sales.xls
sales1.xls
orders3.xls
sales2.xls
sales3.xls
apac1.xls
正则表达式
sales.
结果
sales.xls
sales1.xls
orders3.xls
sales2.xls
sales3.xls
apac1.xls
.
并不是可以匹配所有字符,在大多数的正则表达式实现里,.
就不能匹配换行符。而如果要匹配.
本身,可以对其进行转义,即\.
可以匹配字符.
。
匹配一组字符
正则表达式[ns]
将匹配字符n
或s
,[
和]
不匹配任何字符,它们只负责定义一个字符集合。
类似地,[0123456789]
将匹配从0
到9
任意一个数字。[0123456789]
可简写为[0-9]
,二者功能完全等价。当然,字符区间并不仅限于数字,以下都是合法的字符区间:
A-Z
,匹配从A
到Z
的所有大写字母。a-z
,匹配从a
到z
的所有小写字母。A-z
,匹配从ASCII字符A
到ASCII字符z
的所有字母。这个模式不常用,因为它还包含[
和^
等。
注意,要保证区间的首字符小于尾字符,否则往往会让整个模式失效。 -
(连字符)只有出现在[
和]
之间的时候才是元字符。在字符集合以外的时候只是一个普通字符,不需要被转义。
文本
europe2.xls
sam.xls
na1.xls
na2.xls
sa1.xls
ca1.xls
正则表达式
[ns]a[0-9]\.xls
结果
europe2.xls
sam.xls
na1.xls
na2.xls
sa1.xls
ca1.xls
注意,在同一个字符集合里可以给出多个字符区间。比如说,下面这个模式可以匹配任何一个字母(无论大小写)或数字:[A-Za-z0-9]
字符集合通常用来指定一组必须匹配其中之一的字符,但在某些场合,我们需要反过来做,即指定一组不需要匹配的字符,即排除字符集合里指定的那些字符。可以使用元字符 ^
来排除某个字符集合。前面说[0-9]
只匹配数字,而这里[^0-9]
匹配的是任何不是数字的字符。
文本
europe2.xls
sam.xls
na1.xls
na2.xls
sa1.xls
ca1.xls
正则表达式
[ns]a[^0-9]\.xls
结果
europe2.xls
sam.xls
na1.xls
na2.xls
sa1.xls
ca1.xls
注意,^
的效果将作用于给定字符集合里的所有字符或字符区间,而不是仅限于紧跟在^
字符后面的那一个字符或字符区间。
关于转义
元字符 | 说明 |
---|---|
[\b] | 回退(并删除)一个字符(Backspace键) |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符(Tab键) |
\v | 垂直制表符 |
\r\n
匹配一个“回车(carriage)+换行(line feed)”组合,Windows把这个组合用作文本行的结束标记。而在Unix/Linux系统以及Mac OSX系统上匹配空白行只使用\n
即可。
元字符 | 说明 |
---|---|
\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] ) |
注意,用来匹配退格字符的[\b]
不在\s
的覆盖范围内,\S
也没有将其排除。
在正则表达式里,十六进制值要用前缀\x
来给出,比如说,\x0A
(对应于ASCII字符10,也就是换行符)等价于\n
。同样地,八进制值要用前缀\0
来给出。
POSIX是一种特殊的标准字符类集,是许多正则表达式实现都支持的一种简写形式,JavaScript不支持在正则表达式里使用POSIX字符类。
字符类 | 说明 |
---|---|
[:alnum:] | 任何一个字母或数字(等价于[a-zA-Z0-9]) |
[:alpha:] | 任何一个字母(等价于[a-zA-Z]) |
[:blank:] | 空格或制表符(等价于[\t ])。字母t后面有一个空格 |
[:cntrl:] | ASCII控制字符(ASCII 0到31,再加上ASCII127) |
[:digit:] | 任何一个数字(等价于[0-9]) |
[:graph:] | 和[:print:]一样,但不包括空格 |
[:lower:] | 任何一个小写字母(等价于[a-z]) |
[:print:] | 任何一个可打印字符 |
[:punct:] | 既不属于[:alnum:],也不属于[:cntrl:]的任何一个字符 |
[:space:] | 任何一个空白字符,包括空格(等价于[\f\n\r\t\v ])。字母v后有一个空格 |
[:upper:] | 任何一个大写字母(等价于[A-Z]) |
[:xdigit:] | 任何一个十六进制数字(等价于[a-fA-F0-9]) |
重复匹配
+
匹配一个或多个字符(至少一个,不匹配零个字符的情况)。比如a
匹配a
本身,a+
匹配一个或多个连续出现的a
。类似地,[0-9]
匹配任意单个数字,[0-9]+
匹配一个或多个连续的数字。
[\w.]+
匹配字母数字字符、下划线和.
的一次或多次重复出现。当在字符集合里使用像.
和+
这样的元字符时,不需要转义,但转义了也没有坏处。
+
最少也要匹配一个字符。如果想匹配一个可有可无的字符,需要配合*
元字符来完成。只要把*
放在某个字符(或字符集合)的后面,就可以匹配该字符(或字符集合)出现零次或多次的情况。比如说,模式B.*Forta
将匹配B Forta
、B. Forta
、Ben Forta
以及其他组合。可以把*
理解为一种“使其可选”的元字符。+
需要最少匹配一次,而*
可以匹配多次,也可以一次都不匹配。
另一个非常有用的元字符是?
。和+
一样,它能够匹配可选文本,但只能匹配某个字符(或字符集合)的零次或一次出现,最多不超过一次。?
非常适合匹配一段文本中某个特定的可选字符。
要想设置具体的匹配次数,把数字写在{
和}
之间。比如说,{3}
意味着匹配前一个字符(或字符集合)3次。{}
语法还可以用来为重复匹配次数设定一个区间范围,也就是匹配的最小次数和最大次数。{0,4}
表示最少重复0次,最多重复4次。{3,}
表示至少重复3次。
贪婪型量词 | 懒惰型量词 |
---|---|
* | *? |
+ | +? |
{n,}? | {n,}? |
*
和+
是所谓的“贪婪型”元字符,它们会尽可能地从一段文本的开头一直匹配到末尾,而不是碰到第一个匹配时就停止。需要时可以使用“懒惰型”版本。
位置匹配
匹配单词边界用到\b
,它匹配的是字符之间的一个位置:一边是单词(能够被\w
匹配的字母数字字符和下划线),另一边是其他内容(能够被\w
匹配的字符)。要想匹配一个完整的单词,必须要在匹配的文本的前后都加上\b
。
文本
The cat scattered his food all over the room.
正则表达式
\bcat\b
结果
The cat scattered his food all over the room.
注意\b
匹配的是一个位置,上述例子中匹配到的字符串的长度是3个字符,不是5个字符。
\B
将匹配一个前后都不是单词边界的连字符。nine-digit
和pass-key
中的连字符不能与之匹配,但color - coded
中的连字符可以与之匹配。
字符串边界元字符有两个:^
代表字符串开头,$
代表字符串结尾。正则表达式^\s*<\?xml.*\?>
可以匹配XML文件第一行的起始标签。^
匹配一个字符串的开头位置,所以^\s*
匹配字符串的开头和随后的零个或多个空白字符(这解决了<?xml>
标签前允许出现的空格、制表符、换行符的问题)注意贪婪型量词的问题,用.*?
代替.*
。$
的用法也差不多。它可以用来检查Web
页面结尾的</html>
标签的后面没有任何内容:</[Hh][Tt][Mm][Ll]>\s*$
。
再如^\s*\/\/.*$
可以匹配代码中的注释内容,但它只能匹配第一条注释,并认为这条注释将一直延续到代码的末尾。(?m)
可以启用多行模式(Javascript不支持),多行模式迫使正则表达式引擎将换行符视为字符串分隔符,这样一来^
既可以匹配字符串开头,也可以匹配换行符之后的起始位置(新行);$
不仅能匹配字符串结尾,还能匹配字符串结尾,还能匹配换行符之后的结束位置。在使用时,(?m)
必须出现在整个模式的最前面,例如(?m)^\s*\/\/.*$
。
使用子表达式
用来表示重复次数的元字符?
、*
、{2}
只作用于紧挨着它的前一个字符或元字符。要用多个字符实现重复次数的匹配可以用元字符 (
和 )
划分子表达式。子表达式允许嵌套。
例如IP地址的匹配:
文本
Pinging cumt campus network [10.2.5.251]
正则表达式
(\d{1,3}\.){3}\d{1,3}
结果
Pinging cumt campus network [10.2.5.251]
反向引用
比如\1
、\2
分别匹配前面模式中所使用的第一、第二个子表达式,且只能用来引用括号里的子表达式。反向引用匹配通常从1开始计数。在许多实现里,第0个匹配(\0)可以用来代表整个正则表达式。(.NET支持命名捕获功能)。
假设要把某个Web页面里的所有标题文字全都查找出来,不管是几级标题。
正则表达式:<[hH]([1-6])>.*?<\/[hH]\1>
遗憾的是,在不同的正则表达式实现中,反向引用的语法差异不小。Javascript使用\来标识反向引用。在替换操作中用的是$。
已知\w+[\w\.]*@[\w\.]+\.\w+
可以找出文本中的电子邮件地址。现在要把文本中的电子邮件地址全部替换为可点击的链接,于是可以考虑替换操作。在HTML文档中需要使用<a href="mailto:user@address.com">user@address.com</a>
这样的语法来创建一个可点击的电子邮件地址。
替换操作需要用到两个正则表达式:一个用来指定搜索模式,另一个用来指定替换模式。反向引用可以跨模式使用,在第一个模式里匹配的子表达式可以用在第二个模式里。
文本
Hello, ben@forta.com is my email address.
正则表达式
(\w+[\w\.]*@[\w\.]+\.\w+)
替换
<a href="mailto:$1">$1</a>
结果
Hello, <a href="mailto:ben @forta.com">ben@forta.com</a> is my email address.
有些正则表达式允许我们使用元字符对字母进行大小写转换。
元字符 | 说明 |
---|---|
\E | 结束\L或\U转换 |
\l | 把下一个字符转换为小写 |
\L | 把\L到\E之间的字符全部转换为小写 |
\u | 把下一个字符转换为大写 |
\U | 把\U到\E之间的字符全部转换为大写 |
\l
和\u
可以放置在字符(或子表达式)之前,转换下一个字符的大小写。L
和\U
可以转换其与\U
之间所有字符的大小写。
文本
<h1>Welcome to my Homepage</h1>
正则表达式
(<[Hh]1>)(.*?)(<\/[Hh]1>)
替换
$1\U$2\E$3
结果
<h1>WELCOME TO MY HOMEPAGE</h1>
环视
向前查看指定了一个必须匹配但不用在结果中返回的模式。向前查看模式的语法是一个以?=
开头的子表达式,需要匹配的文本跟在=
的后面。有些正则表达式文档使用术语“消耗”(consume)来表述“匹配和返回文本”的含义。向前查看“不消耗”(not consume)所匹配的文本。
文本
http: //www .forta.com/
https: //mail .forta.com/
ftp: //ftp .forta.com/
正则表达式
.+(?=:)
结果
http: //www .forta.com/
https: //mail .forta.com/
ftp: //ftp .forta.com/
向后查看即查看出现在已匹配文本之前的内容。向后查看操作符是?<=
,(Java、.NET、PHP、Python、Perl都支持向后查看)。?<=
的用法和?=
一样,它必须出现在一个表达式里,后面跟随要匹配的文本。
结合向前查看和向后查看。要把一个Web页面的页面标题提取出来。HTML页面标题是出现在<title>
和</title>
标签之间的文字,而这对标签又必须位于HTML代码的<head>
部分里。能不能只返回文字部分呢?
文本
<head>
<title>Ben Forta's Homepage</title>
</head>
正则表达式
(?<=<[tT][iI][tT][lL][eE]>).*(?=</[tT][iI][tT][lL][eE>)
结果
<head>
<title>Ben Forta's Homepage</title>
</head>
为避免歧义,在上面这个例子里,应该对<
(需要匹配的第一个字符)进行转义,也就是把(?<=<
替换为(?<=\<
。
否定式环视会查看不匹配指定模式的文本。一般来说,凡是支持向前查看的正则表达式实现也都支持肯定式向前查看和否定式向前查看。向后查看类似。
种类 | 说明 |
---|---|
(?=) | 肯定式向前查看 |
(?!) | 否定式向前查看 |
(?<=) | 肯定式向后查看 |
(?<!) | 否定式向后查看 |
文本
I paid $30 for 100 apples,
50 oranges, and 60 pears.
I saved $5 on this order.
正则表达式
\b(?<!\$)\d+\b
结果
I paid $ 30 for 100 apples,
50 oranges, and 60 pears.
I saved $ 5 on this order.
注意,上面的模式中使用了\b
指定了单词边界。因为不使用单词边界,$30
里的0
也会出现在最终的匹配结果里。因为那个字符0
的前一个字符是3而不是$
字符,它完全符合模式(?<!\\$)\d+
的匹配要求。把整个模式放进单词边界中就可以解决这个问题了。
嵌入式条件
嵌入式条件有反向引用条件和环视条件。
反向引用条件的语法是(?(backreference)true|false)
,其中?
表明这是一个条件,括号里的backreference
是一个反向引用,此语法接受一个条件和两个分别在符合/不符合该条件时执行的表达式。
(123)456-7890和123-456-7890都是可接受的北美电话号码格式,而1234567890、(123)-456-7890和(123-456-7890)虽然都包含数目正确的数字字符,但格式都不对。
文本
123-456-7890
(123)456-7890
(123)-456-7890
(123-456-7890
1234567890
123 456 7890
正则表达式
(\ ()?\d{3}(?(1)\ )|-)\d{3}-\d{4}
结果
123-456-7890
(123)456-7890
(123)-456-7890
(123-456-7890
1234567890
123 456 7890
分析
(\()?
负责检查左括号,放在一个子表达式中。随后的\d{3}
匹配3位数字的区号。依赖于是否满足条件,(?(1)\)|-
匹配)
或-
。如果(1)
存在(也就是找到了一个左括号),必须匹配\)
;否则必须匹配-
。这样一来,括号就只能成对出现。如果没有使用括号,电话区号和其余数字之间的-
分隔符必须被匹配。为什么没有匹配第4行?因为左括号没有与之匹配的右括号,所以嵌入条件被视为无关文本,完全被忽略了。
嵌入了条件的模式一眼看上去非常复杂,这意味着调试工作会变得非常困难。比较好的办法是,先构建和测试整个模式的各个组成部分,再把它们组合到一起。
环视条件允许根据向前或向后查看操作是否成功来决定要不要执行表达式。作为一个例子,考虑怎样匹配美国邮政编码。美国邮政编码有两种格式,一种是12345形式的ZIP编码,另一种是12345-6789形式的ZIP+4编码。只有后者才必须使用连字符。
文本
11111
22222
33333-
44444-4444
正则表达式
\d{5}(?(?=-)-\d{4})
结果
11111
22222
33333-
44444-4444
其中,使用了?=-
匹配(但不消耗)一个连字符,如果连字符存在,那么-\d{4}
将匹配该连字符和随后的4位数字。这样一来,33333-就被排除在最终的匹配结果之外了。向前查看和向后查看(肯定式和否定式皆可)都可作为条件,也可使用可选的else表达式(语法和之前看到的一样,即|expression)。
环视条件用的不是很多,因为使用更简单的方法往往可以实现差不多的结果。
有用的表达式
Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?
InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)
帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z0-9]{8,10}$
强密码(必须包含大小写字母和数字的组合,可以使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
日期格式:^\d{4}-\d{1,2}-\d{1,2}
一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
中文字符的正则表达式:[\u4e00-\u9fa5]
双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
空白行的正则表达式:\n\s*\r (可以用来删除空白行)
HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> ( 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
IPv4地址:((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}