第一章 字符组
字符组
顾名思义,字符组就是“一组”字符,在正则表达式中,它表示“在同一个位置可能出现的 各种字符”,其写法是在一对方括号[和]之间列出所有可能出现的字符,简单的字符组包括[ab]、 [314]、[#.?]等。
字符组 [0-9a-fA-F]可以用来验证十六进制字符
元字符与转义
字符组中的横线-并不能匹配横线字符,而是用来表示范围,这类字符叫作元字符(meta-character)。
字符组中的-,如果它紧邻着字符组中的开方括号[,那么它就是普通字符,其他情况下都是元字符;而对于其他元字符,取消特殊含义的做法都是转义,也就是在正则表达式中的元字符前加上反斜线字符\。[-0-9]则是由“-范围表示法”0-9 和横线-共同组成的字符组
正则表达式是用来处理字符串的,但它又不完全等于字符串,正则表达式中的每个反斜线字符\,在字符串中(也就是正则表达式之外)还必须转义为\。Python 提供了原生字符串(Raw String),原生字符串的形式是 r"string"
r"^[0\-9]$" == "^[0\\-9]$"
排除型字符组
^
排除型字符组必须匹配一个字符,在当前位置,匹配一个没有列出的字符
在排除型字符组中,紧跟在^之后的-不是元字符
在排除型字符组中,^是一个元字符,但只有它紧跟在[之后时才是元字符,如果想表示“这个字符组中可以出现^字符”,不要让它紧挨着[即可,否则就要转义。
字符组简记法
- \d [0-9] 数字(digit)
- \w [0-9a-zA-Z_] 单词(word)
- \s [ \t\r\n\v\f] 空格字符、制表符\t、回车符\r、换行符\n 空白字符(space)
字符组简记法中的“单词字符”不只有大小写单词,还包括数字字符和下画线_。
这些简记法能匹配的字符是互补的:\s能匹配的字符,\S 一定不能匹配;\w 能匹配的字符,\W 一定不能匹配;\d 能匹配的字符,\D一定不能匹配。
字符组运算
POSIX 字符组
如果只使用常用的编程语言,可以忽略文档中的 POSIX 字符组,也可以忽略本节;如果想了解 POSIX 字符组,或者需要在 Linux/UNIX 下的各种工具(sed、awk、grep 等)中使用正则表达式,最好阅读本节。
Java、PHP、Ruby、Golang 支持使用 POSIX 字符组。
Java 中,POSIX 字符组[[:name:]]必须使用\p{name}的形式,其中 name为 POSIX 字符组对应的名字,比如[:space:]就应当写作\p{Space},请注意第一个字母要大写,其他 POSIX 字符组都是这样,只有[:xdigit:]要写作\p{XDigit}。还需要指出的是,Java 中的 POSIX 字符组只能匹配 ASCII 字符。
量词
一般形式
\d{6}
\d{4,6},表示这个数字字符串的长度最短是 4 个字符(“单个数字字符”至少出现 4 次),最长是 6 个字符。
常用量词
量词也广泛应用于解析HTML代码。HTML是一种“标签语言”,包含各种各样的tag。从<开始,到>结束,在<和>之间有若干字符,“若干”的意思是长度不确定,但不能为 0(<>并不是合法的tag),也不能是>字符。用[^>]+匹配中间的“若干字符”,整个正则表达式就是<[^>]+>。
使用正则表达式匹配双引号字符串。""是一个完全合法的字符串。"[^"]*"
数据提取
re.findall(pattern, string)。其中 pattern 是正则表达式,string是字符串。这个方法会返回一个数组,其中的元素是在 string 中依次寻找 pattern 能匹配的文本。
点号
点号.可以匹配“任意字符”,常见的数字、字母、各种符号都可以。有一个字符不能由点号匹配,就是换行符\n。
如果非要匹配“任意字符”,有两种办法:可以指定使用单行匹配模式,在这种模式下,点号可以匹配换行符。“自制”通配字符组[\s\S](也可以使用[\d\D]或[\w\W]),正好涵盖了所有字符。
匹配优先量词,也叫贪婪量词。
忽略优先量词(懒惰量词)
如果不确定是否要匹配,忽略优先量词会选择“不匹配”的状态,尝试表达式中之后的元素,如果尝试失败,再回溯,选择之前保存的“匹配”的状态。
转义
括号
分组
() 具有分组的功能 后面跟量词
多选结构
多选结构的形式是(…|…),在括号内以竖线|分隔开多个子表达式,这些子表达式也叫多选分支。([1-9]\d{14}|[1-9]\d{14}\d{2}[0-9x])
第一,多选结构的一般表示法是(option1|option2)(其中 option1 和 option2 是两个作为多选分支的正则表达式),在多选结构中一般会同时使用括号()和竖线|;但是如果没有括号(),只出现竖线|,仍然是多选结构。 竖线|的优先级很低
第二,多选分支并不等于字符组。多选分支看起来类似字符组,如[abc]能匹配的字符串和(a|b|c)一样,[0-9]能匹配的字符串(0|1|2|3|4|5|6|7|8|9)一样。从理论上说,可以完全用多选结构来替换字符组,但这种做法并不推荐,理由在于:首先,[abc]比(a|b|c)要简洁许多,在多选结构中的每个分支都必须明确写出,不能使用 - 范围表示法。
第三,多选分支的排列是有讲究的。多选结构都会优先选择最左侧的分支。如果出现多选结构,应当尽量避免多选分支中存在重复匹配,因为这样会大大增加回溯的计算量。
引用分组
捕获分组,这种括号叫作捕获型括号。在正则表达式中,每个捕获分组都有一个编号。编号为 0 的分组,它是默认存在的,对应整个表达式匹配的文本。在许多语言中,如果调用 group()方法,不给出参数 num,默认就等于调用group(0),比如 Python 就是如此
有些正则表达式里可能包含嵌套的括号,无论括号如何嵌套,分组的编号都是根据开括号出现顺序来计数的;开括号是从左向右数起第多少个开括号,整个括号分组的编号就是多少。
re.sub(pattern, replacement, string)
在 replacement 中也可以引用分组,形式是\num,其中的 num 是对应分组的编号。必须指定 replacement为原生字符串,即r" "
反向引用
在日常开发中,我们可能经常需要反向引用来建立前后联系。它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是左侧),其形式也是\num,其中 num 表示所引用分组的编号。
反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用是一种“引用”,对应的是由之前表达式决定的具体文本,它本身并不规定文本的特征。
各种引用的记法
命名分组
在 Python 中用(?P<name>regex)来分组的,其中的 name 是赋予这个分组的名字,regex 则是分组内的正则表达式。
非捕获分组
正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟一个问号和冒号(?:…),这样的括号叫作非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组。
断言
常见的断言有三类:单词边界、行起始/结束位置、环视。
单词边界
单词边界要求一侧必须出现单词字符
行起始/结束位置
单词边界匹配的是某个位置而不是文本,在正则表达式中,这类匹配位置的元素叫作锚点(anchor),它用来“定位”到某个位置。除了刚才介绍的\b,常用的锚点还有^和$。通常来说,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断“整个字符串能否由表达式匹配”。
与,如果字符串的末尾有行终止符,则它匹配换行符之前的位置;\z 则不管行终止符,只匹配整个字符串的结束位置。
环视
正则表达式专门提供了环视(look-around)用来“停在原地,四处张望”。环视类似单词边界,在它旁边的文本需要满足某种条件,而且本身不匹配任何字符。
TODO: 环视的补充
匹配模式
所谓匹配模式(match mode) ,指的是匹配时遵循的规则。设置特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有 4 种:不区分大小写模式、单行模式、多行模式、注释模式。
不区分大小写模式与模式的指定方式
不区分大小写的匹配模式对应的模式修饰符是 i(case Insensitive),对 the 指定此模式,完整的正则表达式就是(?i)the。
单行模式
单行模式对应的模式修饰符是 s(Single line),所以如果用模式修饰符,可以在表达式的开头用(?s)指定。
在 Java 和 Python 中叫作 DOTALL(也就是点号通配)。如果不想使用“点号+单行模式”的组合(比如 JavaScript 完全不支持这种模式,想用也没办法),也可以使用[\s\S]之类的字符组,它的确可以匹配任何字符。
多行模式
单行模式影响的是点号的匹配规则:在默认模式下,点号.可以匹配除换行符之外的任何字
符,在单行模式下,点号.可以匹配包括换行符在内的任何字符;多行模式影响的是^和匹配的是整个字符串的起始位置和结束位置,但在多行模式下,它们也能匹配字符串内部某一行文本的起始位置和结束位置。
多行模式的模式修饰符是 m(Multiline),所以在表达式的开头用(?m)指定多行模式,这样^可以定位到字符串内部每一行的起始位置;匹配数字字符的表达式是\d,因为没有指定单行模式,点号.不能匹配换行符,.*可以匹配“之后的整行文本”,整个表达式就是(?m)^\d.*。
注释模式
许多语言支持使用(?#comment)的记法添加注释,comment 就是注释的内容。
失效修饰符,它用来“终止”某种模式的作用范围,其形式是(?-modifier),类似(?modifier),只是问号?之后多了一个减号-,表示“取消模式”,也就是某个模式生效到此处为止。
其他
转义
元字符的转义
正则表达式的处理形式
Python是函数式处理的
Java正则表达式处理之前,必须生成专门的正则表达式对象,再调用此对象的成员函数。
根据应用场合的不同,采取不同的处理方式:如果正则表达式只是单次使用,则选择函数式处理;如果正则表达式需要重复使用,则选择面向对象式处理。
Java 中也可以使用函数式处理
Pattern.matches("\\d+", "123 45 6"); //它等价于 Pattern.compile("\\d+").matcher("123 45 6").matches();
线程安全性
正则表达式对象本身基本没有什么状态可言,所以这个对象总是线程安全的,可以由多个线程共享。匹配结果对象一般不是线程安全的,也不应由多个线程共享。因此,最理想的办法是:多个线程可以共享同一个正则表达式对象,节省时间;但操作不同文本时,应当针对各个线程生成专属的匹配结果对象。多个线程共享同一个 Matcher是不正确的处理模式。
表达式中的优先级
正则表达式的元素之间的组合关系只有 4 种。
Unicode
基础知识
UCS-2 是 UTF-16 的子集,在 UTF-16 编码格式下,用于表示一个字符的字节数可能是变化的,或者是 2 个字符,或者是 4 个字符。
关于编码
ASCII 字符,也就是码值在 0~127 之间的字符,常见的英文字符和半角标点符号,都属于 ASCII 字符。
尽量使用 Unicode 编码
GBK(实际是GB18030)是Windows环境下默认的中文编码环境 2 ,也可以笼统地说,Windows下的默认编码就是GBK,在大多数场合使用也一切正常。
匹配原理
有穷自动机
正则表达式能迅速进行复杂处理的秘密在于,它采用了一种特殊的理论模型:有穷自动机(Finite Automata,也叫有穷状态自动机,finite-state machine)。
正则表达式的匹配过程
正则表达式所使用的理论模型就是有穷自动机,其具体实现称为正则引擎(Regex Engine)。
根据状态的确定与否,一般我们会把有穷自动机(正则引擎)分为两类:一类是确定型有穷自动机(Definite Finite Automata,简称 DFA),在任何时刻,它所处的状态是确定无疑的;另一类是非确定型有穷自动机(Nondefinite Finite Automata,简称 NFA),在某个时刻,它所处的状态可能是不确定的。
回溯
在实际应用中,不只要注意自己写的正则表达式,还需要防范外界的恶意程序,它们刻意使用会造成大量回溯的表达式,将计算机的资源消耗殆尽,这种攻击有一个专门的名词,叫作正则表达式拒绝服务攻击(Regular Expression Denial of Service)。
NFA 和 DFA
DFA 不需要回溯,也就不需要保存状态,再反复尝试。NFA 确实更慢,但 NFA 也有自己的优势:如果正则表达式比较复杂,构建 NFA 的时间比DFA 的时间短。
NFA 的匹配性质决定了它必须在匹配过程中保存可能的状态,需要“停下来四处看看”,所以也能够“回顾一路走来的历程”;相比之下,DFA 不会两次测试同一个字符,所以不需要保存状态。因此,NFA 具有许多 DFA 无法提供的功能:比如捕获型括号(…),反向引用\num,环视功能(?!…)、(?=…),忽略优先量词+?、*?、??……
常见问题的解决思路
关于元素的三种逻辑
按照元素(单个字符、字符组、多选分支等)的出现情况,可称为三种逻辑:必须出现、可能出现、不能出现。
正则表达式的常见操作
正则表达式执行的操作,可以粗略分为三大类:匹配、替换、切分。其中,匹配是最基本的操作—使用正则表达式无非是“用正则表达式匹配文本”,这种说法没错,但太笼统,细究起来,广义的“匹配”又可以分为两类:提取和验证。
| 操作 | 方法 |
|---|---|
| 提取 | Matcher.find() |
| 验证 | String.matches(regex) |
| 替换 | String.replaceAll(regex, replacement)、String.replaceFirst(regex, replacement)、Matcher.replaceAll(replacement)、Matcher.replaceFirst(replacement) |
| 切分 | String.split(regex)、Pattern.split(input) |
正则表达式的优化建议
不可滥用
Java
Java 语言中的正则表达式的相关类都存于 java.util.regex 包中
正则功能详解
正则 API 简介
主要用到的是这两个类:java.util.regex.Pattern(以下简称 Pattern)和 java.util.regex.Matcher
Pattern 是 Java 语言中的正则表达式对象,要使用正则表达式,必须首先从字符串“编译”出 Pattern 对象,这需要用到 Pattern.compile(String regex)方法。
Matcher 可以理解为“某次具体匹配的结果对象”:把编译好的 Pattern 对象“应用”到某个 String 对象上,就获得了作为“本次匹配结果”的 Matcher 对象。之后,就可以通过它获得关于匹配的信息。