1. regular expression 的设计
讲到 regular expression、grep,首先要从 unix 谈起。
在计算机界,unix 是一位有着悠久历史的“老革命”。随着其不断发展,应用范围扩大,影响的日益增大,形成了其独有的 unix 文化。而这一文化的一个重要特点就是:以命令行操作为核心。系统的管理与操作一切均依靠命令行完成。虽然随着其发展也开发出图形界面,但图形界面架构于底层命令行基础之上,图形界面上的操作本质上都转换为命令行来完成,图形界面与命令的耦合度不高,这一点与 windows 系统的图形界面与操作的高耦合的设计特点完全不同。
既然以命令行操作为核心,缺少 gui 元素的辅助,那么,显而易见,字符串操作是 unix 中的重点。在处理文本文件、命令执行后的结果时,要面临大量让人应接不暇的字符信息,此时,我们的核心需求无外乎两点:如何在众多的信息中找到我们需要的字符串,以及如何对这些字符串进行特定的编辑 (即更改替换操作)。要想高效使用命令行环境,上述需求就成了一个必须解决的问题。regular expression 的出现很好的应对了这一问题。
regular expression 本质上是一种文本模式,这种模式描述了一类字符串的特征。利用 regular expression 所描述的特征,以及支持 regular expression 的工具,我们可以找到一类字符串,然后对这一类字符串进行各种操作,比如替换、修改、输送至其他程序等。符合 regular expression 描述的字符串集合中的各个具体的字符串可能会分布在文本的各个地方。regular expression 最初起源于数学领域的研究,后来被贝尔实验室的研究人员引入计算机领域,迅速彰显出其在文本处理上的巨大威力。
1.1. 基本描述元素
regular expression 用于描述一类字符串的特征,这些特征是如何描述的呢?
除了可以使用字符串常量直接进行描述外,regular expression 提供了一系列带有 “变量” 特征的 “基本描述元素”。这些基本元素有各自的独特的功能,基本的 regular expression 就是由这些基本的描述元素“拼接”而成。因为这些基本元素具有的 “变量” 特征,才使得正则表达式具有广泛的描述能力。
那么,regular expression 都有哪些描述元素呢?
- 代表字符的拼接符号
- . : 该位置一定存在一个字符,该字符可以是任意一个字符
- [xxx] : 该位置一定存在一个字符,该字符选取范围由 bracket 中内容指出
- bracket 中内容可以有如下形式:
- [0123456789] 逐一枚举表示
- [0-9] 区间表示
- [^a-z] 反向选择表示
- 注:当 ^ 符号放在 bracket [ ] 内的时候,代表”非”,而不是行首
- bracket 中内容可以有如下形式:
- \ : 转义字符
- 代表位置的拼接符号(锚字符,anchor)
- ^ : 行首标志
- $ : 行尾标志
- 代表重复的拼接符号
顾名思义,代表重复的拼接符号的作用就是完成重复次数的描述,它不能独立存在,必须依附于某个被重复的字符而出现。
- * : 重复前一个字符 0 次或无穷多次
- + : 重复前一个字符 1 次或无穷多次
- ? : 重复前一个字符 0 次到 1 次
- {n} : 重复前一个字符 n 次(精准重复)
- {n,m} : 前一个字符重复次数在 n 次到 m 次之间
- brace 中内容可以有如下形式:
- {n,m} 重复次数在n到m次之间
- {n,} 重复次数大于n次
- {,m} 重复次数小于m次
- 注:若在 shell 中使用,由于 brace 在 shell 中有特殊意义,因此,使用 regular expression 时,需要用转义字符
\
对 brace 进行转义。例如,使用 \{ n \}。 - 注:
*
、+
、?
都可以用 brace 格式进行解释- * <--> {0,}
- + <--> {1,}
- ? <--> {0,1}
- {n} <--> {n,n}
- brace 中内容可以有如下形式:
当“代表重复”的拼接符号出现时,一定要注意观察其前一个被重复的目标是什么。
1.2. 对字符串的整体处理
正则表达式基本描述元素的设计是针对单个字符的匹配,通过字符常量和基本描述元素的 “拼接和组合”,进而来代表具有特定特征的一类字符串。此时,可以视为任务的出发点是单个字符,任务的目标是构建字符串。而有些情况下,任务的出发点就是字符串,需要将某些字符串作为基本操作对象,在各个字符串整体之间进行处理操作,如集合中取值选择、多次重复、后续引用等,正则表达式同样提供了相应的描述能力。将字符串整体视为一个基本操作单元,以满足后续匹配处理的需求,可以使用 “子表达式” 和 “或操作 |
”。
1.3. 子表达式 — 为复用而生
一般情况下,利用基本描述元素“拼接”了与特定字符串匹配的模式,即可开展 regular expression 的基本应用。而很多时候,还会有更进一步复用的需求:
- 对构建好的某些模式进行复用
- 对某些模式匹配后的精确结果进行复用
针对第一种情况,最直接的方式当然是在特定的复用位置重复输入相关的模式字符串,这种方式简单直接,但效率较低;针对第二种情况,基本的元素拼接组串的方式则无能为力。为应对上述需求,regular expression 提供了一种更为高级的方法 — 子表达式。
子表达式是一个更大表达式的一部分,其目的就是将子表达式本身作为一个独立元素,从整体上进行使用。从某种角度来看,子表达式可以视为 regular expression 中的一个“变量”,对这个“变量”,既可复用变量本身,也可以复用变量的具体取值,进行重复、引用等多种操作。
本质上,子表达式代表的是从整体字符串中划分出来的子字符串的正则表达式。具体展现形式上,子表达式必须用括号 (
和 )
括起来。一个 regular expression,既可以选取其一部分内容括起来,作为单独的子表达式,也可以直接将其划分为由几个子表达式共同”拼接”而成的大表达式,子表达式划分取决于具体应用需求。
针对前述的第一种模式复用的需求,利用 “子表达式 + 表示重复的拼接符号” 即可满足需求;针对第二种模式匹配结果精确复用的需求,regular expression 设计了专门的回溯引用来完成相关任务。
回溯引用 (backreference) — 基于精确匹配值的复用
在应用中,经常会碰到这样的情况,在一开始的时候,我们利用模式串匹配了某个具体的字符串,在后面还想继续引用前面已经匹配成功的那个具体的字符串,这种情况下,继续沿用前一个模式串是没有效果的,模式串标定的是一个范围(集合),而此时,我们需要匹配的是范围(集合)中的一个具体的元素,而这个具体元素的内容对我们是未知的,此时,传统的 regular expression 的模糊式描述方式是无法进行这种前后一致的精确性匹配的。而“回溯引用”针对这种需求而设计。
回溯引用,本质上就是“前后取值的一致性匹配”,它可以让正则表达式引用前面精确匹配的结果。具体应用上,须将整个模式串中的一部分或几部分单独划分成子表达式的形式,以便在后面精确引用。引用时,采用 \ + n (数字)
的形式,\1 表示引用第一个子表达式精确匹配的结果,\2 表示第二个子表达式精确匹配的结果,以此类推。而 \0 则用来代表这个 regular expression 匹配的具体结果。如果说子表达式模式串代表一个集合,则 \n 代表集合里的一个具体元素。
regular expression 常用于搜索和替换等操作,回溯引用在某些场景有不可替代的作用。
搜索下列重复单词的文本
This is a block of of text, several words here are are repeated.
采用的 regular expression为:
[]+(\w+)[]+\1
尤其是替换操作,更彰显回溯引用的威力。
替换操作需要用到两个 regular expression,一个用来给出搜索模式,另一个用来给出匹配文本的替换模式。回溯引用可以跨模式使用,即在搜索模式中被子表达式匹配的结果,可以直接在替换模式中使用。
将 313-555-1234
替换为
(313) 555-1234
搜索表达式:
(\d{3})(-)(\d{3})(-)(\d{4})
替换为:
(\1) \3-\5
1.4. 正则表达式应用总结
正则表达式中的括号
括号是正则表达式中的常用字符,各种 “括号” 都有各自独特的含义
- 大括号
{
}
:重复的次数 - 中括号
[
]
:集合中选取一个字符 - 小括号
(
)
:子表达式
只要正则表达式中出现 “小括号”,意味着存在子表达式,存在回溯引用,有子字符串处理的需求!
2. regular expression 的实现
2.1. grep — regular expression 的搜索利器
regular expression 描述了一种规范,一种标准,虽然描述能力强大,但为高效发挥其作用,必须依赖具体工具的实现支撑。当前,可以支撑 regular expression 的工具不少。grep 就是众多支撑工具中的最为典型的代表。
grep 全名 Globally search a Regular Expression and Print,最早在贝尔实验室 unix 平台上实现,至今已有40多岁的年纪了,像 unix 一样,也是计算机界的“老同志”了。随着 unix 文化及其系列系统的发展,grep 目前也已在多个平台上有相应的实现。从其名字不难看出 grep 的几个主要特征:
- 与regular expression联系紧密;
- 该工具的主要任务是完成搜索任务;(grep无法完成修改替换操作)
grep的使用模式与linux命令的基本使用方式一致:
grep主要完成搜索并打印的任务,其搜索及打印的最核心特点就是 “以行为单位” ,在文件 filename 中查找符合 string 模式的字符串,并进行格式化输出。其中,-option 决定着搜索操作方式与输出的格式。
主要选项参数有如下几项:
- 与搜索方式相关选项
- - i : 忽略大小写
- - v : 反向搜索,查找不包含 string 的那些行
- 与打印结果相关选项
- - n : 打印包含 string 结果行,同时输出行号
- - A + num : 打印包含 string 结果行,同时打印后(after) num 行
- - B + num : 打印包含 string 结果行,同时打印前(before) num 行
- 与软件状态相关选项
- - V : 查看当前 grep 的版本
2.2. zgrep — 压缩文件的搜索利器
grep 可以直接搜索并查看普通的文本文件的内容,针对压缩后文本文件,grep 则无能为力,此时,要用到处理压缩文件的专用工具集 — zcommands,工具集中的 zgrep,即是用于搜索压缩后文本文件的专用工具。
以处理 dpkg 命令的日志为例,
当软件包信息发生变更,会将信息存储在
/var/log/dpkg.log
日志文件中。因为变更信息是一个持续增加的量,因此 dpkg.log 也采用了 linux 中通常使用的 “日志轮转机制”,日志增加到一定程度的量以后,旧的日志自动成为 “轮转” 记录,即生成 log.1,log.2.gz,log.3.gz 的文件,以保持 .log 文件的 “最新状态”。这些 log 文件一般都由 logrotate 工具来控制,会根据配置好的要求,以固定周期对 log 文件进行拆分和压缩,并只保留最新的一部分 log,从而避免 log 文件占用太多的存储空间。
dpkg 的 log “轮转” 日志记录包含了各个软件包的安装与更新的记录,通过查看轮转记录,就可以查看各个软件包的安装与更新的信息。
搜索 dpkg 压缩日志,
zgrep installed/upgrade/remove /var/log/dpkg.log.5.gz
reference
- manpage – grep
- GNU grep
- 《Linux command line and shell scripting Bible》(3rd)
- 《bird private home cuisine》(3rd)
- 《Guide to Unix and Linux》