《正则指引》-读书笔记

105 阅读13分钟

第一章 字符组

字符组

顾名思义,字符组就是“一组”字符,在正则表达式中,它表示“在同一个位置可能出现的 各种字符”,其写法是在一对方括号[和]之间列出所有可能出现的字符,简单的字符组包括[ab]、 [314]、[#.?]等。

image.png

字符组 [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)

字符组简记法中的“单词字符”不只有大小写单词,还包括数字字符和下画线_。

image.png

这些简记法能匹配的字符是互补的:\s能匹配的字符,\S 一定不能匹配;\w 能匹配的字符,\W 一定不能匹配;\d 能匹配的字符,\D一定不能匹配。

字符组运算

image.png

POSIX 字符组

如果只使用常用的编程语言,可以忽略文档中的 POSIX 字符组,也可以忽略本节;如果想了解 POSIX 字符组,或者需要在 Linux/UNIX 下的各种工具(sed、awk、grep 等)中使用正则表达式,最好阅读本节。

image.png

image.png

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 个字符。

image.png

常用量词

image.png

量词也广泛应用于解析HTML代码。HTML是一种“标签语言”,包含各种各样的tag。从<开始,到>结束,在<和>之间有若干字符,“若干”的意思是长度不确定,但不能为 0(<>并不是合法的tag),也不能是>字符。用[^>]+匹配中间的“若干字符”,整个正则表达式就是<[^>]+>。

使用正则表达式匹配双引号字符串。""是一个完全合法的字符串。"[^"]*"

image.png

数据提取

re.findall(pattern, string)。其中 pattern 是正则表达式,string是字符串。这个方法会返回一个数组,其中的元素是在 string 中依次寻找 pattern 能匹配的文本。

点号

点号.可以匹配“任意字符”,常见的数字、字母、各种符号都可以。有一个字符不能由点号匹配,就是换行符\n。

如果非要匹配“任意字符”,有两种办法:可以指定使用单行匹配模式,在这种模式下,点号可以匹配换行符。“自制”通配字符组[\s\S](也可以使用[\d\D]或[\w\W]),正好涵盖了所有字符。

image.png

匹配优先量词,也叫贪婪量词。

忽略优先量词(懒惰量词)

如果不确定是否要匹配,忽略优先量词会选择“不匹配”的状态,尝试表达式中之后的元素,如果尝试失败,再回溯,选择之前保存的“匹配”的状态。

image.png

转义

image.png

image.png

括号

分组

() 具有分组的功能 后面跟量词

多选结构

多选结构的形式是(…|…),在括号内以竖线|分隔开多个子表达式,这些子表达式也叫多选分支。([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 就是如此

image.png

有些正则表达式里可能包含嵌套的括号,无论括号如何嵌套,分组的编号都是根据开括号出现顺序来计数的;开括号是从左向右数起第多少个开括号,整个括号分组的编号就是多少。

image.png

image.png

image.png

re.sub(pattern, replacement, string)

在 replacement 中也可以引用分组,形式是\num,其中的 num 是对应分组的编号。必须指定 replacement为原生字符串,即r" "

反向引用

在日常开发中,我们可能经常需要反向引用来建立前后联系。它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是左侧),其形式也是\num,其中 num 表示所引用分组的编号。

反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用是一种“引用”,对应的是由之前表达式决定的具体文本,它本身并不规定文本的特征。

各种引用的记法

image.png

image.png

image.png

命名分组

在 Python 中用(?P<name>regex)来分组的,其中的 name 是赋予这个分组的名字,regex 则是分组内的正则表达式。

image.png

非捕获分组

正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟一个问号和冒号(?:…),这样的括号叫作非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组。

image.png

断言

常见的断言有三类:单词边界、行起始/结束位置、环视。

单词边界

image.png 单词边界要求一侧必须出现单词字符

行起始/结束位置

单词边界匹配的是某个位置而不是文本,在正则表达式中,这类匹配位置的元素叫作锚点(anchor),它用来“定位”到某个位置。除了刚才介绍的\b,常用的锚点还有^和$。通常来说,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断“整个字符串能否由表达式匹配”。

image.png

类似的还有两个特殊标记Z\z,它们不受多行模式的影响,在任何情况下都匹配整个字符串的结束位置。Z\的主要差别在于:Z等价于默认模式(非多行模式)下的类似的还有两个特殊标记\Z 和\z,它们不受多行模式的影响,在任何情况下都匹配整个字符串的结束位置。\Z 和\的主要差别在于:\Z 等价于默认模式(非多行模式)下的,如果字符串的末尾有行终止符,则它匹配换行符之前的位置;\z 则不管行终止符,只匹配整个字符串的结束位置。

image.png

image.png

环视

正则表达式专门提供了环视(look-around)用来“停在原地,四处张望”。环视类似单词边界,在它旁边的文本需要满足某种条件,而且本身不匹配任何字符。

image.png

image.png

TODO: 环视的补充

匹配模式

所谓匹配模式(match mode) ,指的是匹配时遵循的规则。设置特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有 4 种:不区分大小写模式、单行模式、多行模式、注释模式。

不区分大小写模式与模式的指定方式

不区分大小写的匹配模式对应的模式修饰符是 i(case Insensitive),对 the 指定此模式,完整的正则表达式就是(?i)the。

image.png

image.png

image.png

单行模式

单行模式对应的模式修饰符是 s(Single line),所以如果用模式修饰符,可以在表达式的开头用(?s)指定。

image.png

image.png

在 Java 和 Python 中叫作 DOTALL(也就是点号通配)。如果不想使用“点号+单行模式”的组合(比如 JavaScript 完全不支持这种模式,想用也没办法),也可以使用[\s\S]之类的字符组,它的确可以匹配任何字符。

多行模式

单行模式影响的是点号的匹配规则:在默认模式下,点号.可以匹配除换行符之外的任何字

符,在单行模式下,点号.可以匹配包括换行符在内的任何字符;多行模式影响的是^和的匹配规则:在默认模式下的匹配规则:在默认模式下,^和匹配的是整个字符串的起始位置和结束位置,但在多行模式下,它们也能匹配字符串内部某一行文本的起始位置和结束位置。

多行模式的模式修饰符是 m(Multiline),所以在表达式的开头用(?m)指定多行模式,这样^可以定位到字符串内部每一行的起始位置;匹配数字字符的表达式是\d,因为没有指定单行模式,点号.不能匹配换行符,.*可以匹配“之后的整行文本”,整个表达式就是(?m)^\d.*。

image.png

注释模式

许多语言支持使用(?#comment)的记法添加注释,comment 就是注释的内容。

image.png

image.png

失效修饰符,它用来“终止”某种模式的作用范围,其形式是(?-modifier),类似(?modifier),只是问号?之后多了一个减号-,表示“取消模式”,也就是某个模式生效到此处为止。

image.png

其他

转义

image.png

image.png

image.png

image.png

image.png

元字符的转义

image.png

image.png

image.png

正则表达式的处理形式

Python是函数式处理的

image.png

Java正则表达式处理之前,必须生成专门的正则表达式对象,再调用此对象的成员函数。

image.png

根据应用场合的不同,采取不同的处理方式:如果正则表达式只是单次使用,则选择函数式处理;如果正则表达式需要重复使用,则选择面向对象式处理。

image.png

Java 中也可以使用函数式处理

Pattern.matches("\\d+", "123 45 6"); //它等价于 Pattern.compile("\\d+").matcher("123 45 6").matches();

线程安全性

image.png

正则表达式对象本身基本没有什么状态可言,所以这个对象总是线程安全的,可以由多个线程共享。匹配结果对象一般不是线程安全的,也不应由多个线程共享。因此,最理想的办法是:多个线程可以共享同一个正则表达式对象,节省时间;但操作不同文本时,应当针对各个线程生成专属的匹配结果对象。多个线程共享同一个 Matcher是不正确的处理模式。

image.png

表达式中的优先级

正则表达式的元素之间的组合关系只有 4 种。

image.png

image.png

image.png

Unicode

基础知识

image.png

image.png

UCS-2 是 UTF-16 的子集,在 UTF-16 编码格式下,用于表示一个字符的字节数可能是变化的,或者是 2 个字符,或者是 4 个字符。

image.png

关于编码

ASCII 字符,也就是码值在 0~127 之间的字符,常见的英文字符和半角标点符号,都属于 ASCII 字符。

尽量使用 Unicode 编码

GBK(实际是GB18030)是Windows环境下默认的中文编码环境 2 ,也可以笼统地说,Windows下的默认编码就是GBK,在大多数场合使用也一切正常。

image.png

匹配原理

有穷自动机

正则表达式能迅速进行复杂处理的秘密在于,它采用了一种特殊的理论模型:有穷自动机(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,环视功能(?!…)、(?=…),忽略优先量词+?、*?、??……

image.png

常见问题的解决思路

关于元素的三种逻辑

按照元素(单个字符、字符组、多选分支等)的出现情况,可称为三种逻辑:必须出现、可能出现、不能出现。

image.png

正则表达式的常见操作

正则表达式执行的操作,可以粗略分为三大类:匹配、替换、切分。其中,匹配是最基本的操作—使用正则表达式无非是“用正则表达式匹配文本”,这种说法没错,但太笼统,细究起来,广义的“匹配”又可以分为两类:提取和验证。

操作方法
提取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 包中

正则功能详解

image.png

image.png

正则 API 简介

主要用到的是这两个类:java.util.regex.Pattern(以下简称 Pattern)和 java.util.regex.Matcher

Pattern 是 Java 语言中的正则表达式对象,要使用正则表达式,必须首先从字符串“编译”出 Pattern 对象,这需要用到 Pattern.compile(String regex)方法。

Matcher 可以理解为“某次具体匹配的结果对象”:把编译好的 Pattern 对象“应用”到某个 String 对象上,就获得了作为“本次匹配结果”的 Matcher 对象。之后,就可以通过它获得关于匹配的信息。