资讯专栏INFORMATION COLUMN

正则表达式基本知识(php)

lunaticf / 2410人阅读

摘要:字符组字符组就是一组字符,在正则表达式中,它表示在同一个位置可能出现的各种字符。放在正则表达式的开头,表示定位到字符串的起始位置用在正则表达式的末尾,表示定位到字符串的结束位置。

这里的知识点基本上是《正则指引》的读书笔记,只是每个知识点的示例代码用php来实现。

1. 字符组

字符组(Character Class)就是一组字符,在正则表达式中,它表示“在同一个位置可能出现的各种字符”。
写法:[ab]、[314]、[#.?]

字符组的基本用法

[...]

preg_match("/[0123456]/", "5");    // => 1
preg_match("/[abc123]/", "5");    // => 0
范围表示法(range)

[x-y]表示xy整个范围内的字符。如,[0123456789]表示为[0-9][abcdefghijk]表示为[a-k]

为什么是[0-9],而不是[9-0]
因为-表示的范围一般是根据字符对应的码值(Code Point)来确定的。典型地有ACSⅡ编码。在ACSⅡ编码中,0~9的码值是48~57,a~z的码值是97~122,A~Z的码值是65~90。

preg_match("/[0-9]/", "5");    // => 1
preg_match("/[a-z]/", "5");    // => 0
preg_match("/[0-9a-fA-F]/", "0");    // 16进制
字符组简记法(shorthands)

提供比范围表示法更简洁的表示方法,如d表示[0-9]w表示[0-9a-zA-z_]

php中支持的字符组简记:

d 所有的数字,即[0-9]

D 所有的非数字,与d互斥

w 所有的单词字符(字符、数字、下划线),即[0-9a-zA-Z_]

W 所有的非单词字符,与W互斥

s 所有的空白字符,包括空格、制表符、回车符、换行符等空白字符

S 所有的非空白字符,与s互斥

preg_match("/d/", "8");    // => 1
preg_match("/d/", "a");    // => 0
preg_match("/d[a-z]/", "a");    // => 1

preg_match("/w/", "a");    // => 1
preg_match("/w/", "6");    // => 1
preg_match("/w/", "_");    // => 1

preg_match("/s/", " ");    // => 1
preg_match("/s/", "	");    // => 1
preg_match("/s/", "
");    // => 1
元字符与转义

在范围表示法中,字符组中的横线-不能匹配横线字符,而是用来表示范围,这类字符叫做元字符(meta-character)。元字符除了-还有开方括号[、闭刚括号]^$等,它们都有特殊的意义。
当元字符想要表示普通字符的含义时(如-就只想表示横线字符),就需要转义处理(在元字符前加反斜线字符)。对于-,有一个例外情况,就是当它紧跟着字符组中的开括号[时,它就表示普通横线字符,此时不用转义。

preg_match("/[0-9]/", "-");    // => 1
preg_match("/[0-9]/", "8");    // => 0
preg_match("/[0-9]/", "0");    // => 1

preg_match("/[-09]/", "-");    // => 1

preg_match("/[0-9]/", "-");    // => 1
preg_match("/[0-9]/", "-");    // => 1

仔细看上面第一个表达式和最后两个表示式。这里要注意:

在php中,字符串既可以用单引号标注也可以用双引号标注。两者的主要区别在于,双引号字符串可以插值,二单引号字符串不能;另外,双引号字符串会处理字符串转义,二单引号字符串不会

正则表达式是以字符串的方式提供的。在php中,双引号字符串本身也有关于转义的规定(如""" "" "等),因此"0-9""0-9"是等价的。

那么最后一个表达式为什么也可以匹配呢?这是因为,尽管php的正则表达式用字符串文字给出,但它与常见的字符串不完全一样——如果某个转义序列可以有字符串识别,则对其进行转义处理;否则,将整个转义序列“原封不动”地保存下来。

因此,在正则表达式中转义要小心,在php中,使用单引号字符串来构建正则表达式会比双引号字符串更简单明了。

排除型字符组(Negated Character Class)

在方括号[…]中列出希望匹配的所有字符叫做“普通字符组”。在开方括号[之后紧跟一个脱字符^,写作[^…],表示“在当前位置,匹配一个没有列出的字符”。例如,[^0-9]匹配非数字字符。

preg_match("/[^0-9][0-9]/", "A8");    // => 1

排除型字符组中的紧跟着开方括号[的脱字符^也是元字符,如果要匹配尖括号字符,需要进行转义处理。但是,不紧跟着开方括号[^就是普通字符,不需要转义。

preg_match("/[^0-9]/", "0");    // => 0
preg_match("/[^0-9]/", "0");    // => 1
preg_match("/[^0-9]/", "^");    // => 1
preg_match("/[0-9^]/", "^");    // => 1
POSIX字符组

之前介绍的字符组,都属于Perl衍生出来的正则表达式流派(Flavor),这个流派叫做PCRE(Per Compatible Regular Expression)。正则表达式还有其他流派,比如POSIX(Portable Operating System Interface for unix),它是一系列规范,定义了UNIX操作系统应当支持的功能,其中也包括了正则表达式的规范。

常见的[a-z]形式的字符组,在POSIX规范中仍然获得支持,称作POSIX方括号表达式。POSIX方括号表达式中的不是用来转义的,如[d]就只能匹配d两个字符。这里涉及到]-这两个特殊字符,在POSIX规范中,紧接在开方括号[之后的]才表示闭方括号字符,紧挨在闭方括号]之前的-才表示横线字符。

对于PCRE规范中的dws等字符组简记法,POSIX中有类似的东西,叫做POSIX字符组。在ASCⅡ语言环境(locale)中,常见的POSIX字符组及其含义如下:

POSIX字符组 说明 ACSⅡ字符组 等价的PCRE简记法
[:alnum:] 字母和数字 [0-9a-zA-Z]
[:alpha:] 字母 [a-zA-Z]
[:ASCⅡ] ASCⅡ字符 [x00-x7F]
[:blank:] 空格字符和制表字符 [ ]
[:cntrl:] 控制字符 [x00-x1Fx7F]
[:digit:] 数字字符 [0-9] d
[:graph:] 空白字符之外的字符 [x21-x7E]
[:lower:] 小写字母字符 [a-z]
[:print:] 类似[:graph:],但包括空白字符 [x20-x7E]
[:punct:] 标点符号 [][!"#$%&"()*+,./:;<=>?@^_`{ }~-]
[:space:] 空白字符 [ vf] s
[:upper:] 大写字母 [A-Z]
[:word:] 字母字符 [A-Za-z0-9_] w
[:xdigit:] 十六进制字符 [A-Fa-f0-9]

php中有专门处理POSIX正则的函数,但从5.3.0开始已经废弃了。这里只是了解一下相关知识。

2. 量词

这里首先介绍一下^$两个特殊字符,在上一章的元字符与转义一节提到过这两个特殊字符。
^放在正则表达式的开头,表示“定位到字符串的起始位置”;$用在正则表达式的末尾,表示“定位到字符串的结束位置”。

preg_match("/wd/", "1a2b");   // => 1
preg_match("/^wd/", "1a2b");  // => 0 必须以字母开头
preg_match("/wd$/", "1a2b");  // => 0 必须以数字结尾
preg_match("/^wd/", "a2b");   // => 1
preg_match("/wd$/", "1a2");   // => 1
preg_match("/^wd$/", "1a2");  // => 0 开头必须是字母,结尾必须是数字
preg_match("/^wd$/", "a2");   // => 1
量词的一般形式

如果要匹配一个邮政编码(6位数字),目前能写出来的正则表达式是^ffffdffffd$

preg_match("/^ffffdffffd$/", "100010");   // => 1
preg_match("/^ffffdffffd$/", "10001035"); // => 0
preg_match("/^ffffdffffd$/", "10a010");   // => 0

d重复6次的写法很不科学,正则表达式肯定会有更方便的写法,也就是量词(quantifier)。量词的通用形式是{m,n}(注意,,后面不能有空格),它限定之前的元素能够出现的次数,m是下限,n是上限。其他常见的量词形式有:

量词 说明
{n} 之前的元素必须出现n次
{m,n} 之前的元素最少出现m次,最多出现n次
{m,} 之前的元素最少出现m次,出现次数无上限
{0,n} 之前的元素可以不出现,也可以出现,最多出现n次
preg_match("/^d{6}$/", "100010");      // => 1

preg_match("/^d{4,6}$/", "123");       // => 0
preg_match("/^d{4,6}$/", "1234");      // => 1
preg_match("/^d{4,6}$/", "123456");    // => 1
preg_match("/^d{4,6}$/", "1234567");   // => 0
常用量词

正则表达式还有三个常用的量词,分别是+?*

常用量词 {m,n}等价形式 说明
* {0,} 可能出现,也可能不出现,出现次数没有上限
+ {1,} 至少出现1次,出现次数没有上限
? {0,1} 出现0次或1次

这三种量词在实际中使用的非常多。

例如,匹配url的时候,有可能是http,也有可能是https,这个时候用?就很方便:

preg_match("/^https?://www.baidu.com/", "http://www.baidu.com");    // => 1
preg_match("/^https?://www.baidu.com/", "https://www.baidu.com");   // => 1

在匹配html的tag(如

等),同时,也不能匹配一个空的标签<>
直接使用<[^/]+>/字符排除是不对的,因为有些标签的属性中可能会含有/字符。例如。这里只是在开尖括号<之后的第一个字符和闭尖括号>之前的第一个字符不能为/
然而<[^/][^>]*[^/]>也是不行的,因为在<>之间会至少匹配两个字符,像
这样的标签是无法匹配到的。这里就要用到环视了。

<(?!/).*+(?

上面的正则表达式中有两个环视结构,一个在开尖括号<之后,表示在尖括号<后向右看看,右边的第一个字符不能为/(正则表达式中进行了转义);另外一个在闭尖括号>之前,表示在闭尖括号>之前向左看看,左边挨着的字符不能为/

上面的正则表达式已经解决了匹配html中开标签的主要问题,只是其中的.*?还需要优化一下。需要解决的问题是:

有可能会有单引号"或双引号",它们都得成对出现

单引号对或双引号对之内可以有>字符,但是它们的外面不能有>字符

利用正则表达式的选择结构,可以写出下面的表达式,用于完善上面的问题。

<(?!/)(?:"[^"]*"|"[^"]*"|[^"">])+(?

5.匹配模式

前面的内容中已经出现介过了单行模式多行模式非贪婪模式匹配模式是指匹配时使用的规则。常用的匹配模式还有不区分大小写模式注释模式

在开始介绍具体的模式之前,先介绍php中模式的两种具体实现/.../{modifier}...(?{modifier})...

.*/s(?s).*
模式修饰符 /.../{modifier} ...(?{modifier})...
示例 /
名称(php手册) 模式修饰符 模式内修饰符
名称(《正则指引》) 预定义常量 模式修饰符
作用范围 整个正则表达式 不在分组(子表达式)中时,对它后面的全部正则表达式起作用;如果在分组(子表达式)中,则对它分组中的剩余部分起作用。在没有分组,且放在整个正则表达式最前面的时候相当于/.../{modifier}
支持程度 支持所有模式修饰符 支持部分模式修饰符
其他编程语言 可能不支持 一般都支持
不区分大小写模式

在html中是不区分大小写的,例如的作用是一样的。如果要从网页中提取,不使用匹配模式的表达式应该是这样:

<[tT][dD]>

由于标签只有两个字符,所以上面的写法还可以接受。但是如果标签是

阅读需要支付1元查看
<