Coding Poet, Coding Science

正则表达式实践

本部分内容摘抄自原来的linux笔记第三册,而原来的笔记实质是一本书的内容。书名忘了记了,反正是很详细的一本介绍正则表达式的书。在此对于作者的工作表示敬意以及歉意。

正则表达式的科班史

正则表达式发源于与计算机密切的两个领域,计算理论和形式语言。20世纪40年代两位神经生理学家Warren McCulloch和Walter Pitts研究出一种数学方式来描述神经网络的方法,它们把神经系统中的神经元描述成小而简单的自动控制单元。1956年数学家Stephen Cole Klenne在他们的研究的基础上发表了一篇名为《神经网络表示法》的论文,在其中,他们采取了一些称之为“正则集合”的数学符号来描述神经网络模型。

这个书上说的就自相矛盾了,无论怎样,神经元的研究也不能说是一开始就和计算机领域是密切相关的

之后UNIX的主要发明人Ken Thompson将这个符号系统引入了文本编辑器QED.正则表达式由此进入计算机的世界。之后Ken Thompson又将正则表达式引入了unix下的文本编辑器ed.ed最终演化为大家熟悉的grep.因为grep得名自ed中的正则表达式搜索命令g/re/p.

这个又说明了一个问题。不知道Ken Thompson对于神经网络是怎样了解的,还是说这个时候已经产生了编译原理的相关的理论了。但是可以确定的是,Ken Thompson肯定知识面非常地广。看编译原理或许也不会知道正则表达式是何时进入高级语言的。

正则表达式的一些感概

这篇是在原笔记中出现的,所以在注释当中又加了一些笔记,使得一些知识能够反映当前的认识的水平。

正则表达式应用中分为几个版本。各个版本对正则表达式的实现都不能说是尽善尽美。尤其是对于中文的支持并不是十分完善。而且关键是各个正则表达式中常含有不同的转义表示符,这使得在形式上它们并不相同,虽然对应于同一个正则表达式的理论。

所以在一个正则表达式工具当中:

正则表达式语法=正则表达式理论+工具实现的部分功能+工具的转义符系统

原来通配符也是正则表达式的一种,只不过以前在windows下从来没有人说它与正则表达式的关联,所以当首次听说Perl的时候,觉得正则表达式是一个很神秘的东西。

实际上在linux的bash的学习当中,也没有人说通配符就代表了正则表达式。

从现在来看通配符作为所谓的正则表达式的一个子集。以前还一直以为通配符来自于工业的背景,所以通配符和正则表达式的关系是后来发展中才被发现的。这导致我有时以为所谓的通配符是微软的发现。这就叫“无知者无畏”。

正则表达式用于称作“模式匹配”的领域。正则表达式被应用的时候,可以提供三个方面的功能:判断,选择,替换。三个功能当然是逐层递进的。“判断”是指在一段文本当中能不能找到正则表达式匹配的模式,“选择”是说,如果有这样一个模式,将从符合模式的几个子串当中选择哪一个,“替换”是说,对于选定的文本以何种方式替代它。

凡解析一个正则表达式都应当遵守这样的规则。其中的一部分,比如最长匹配原则,就是应用于“选择”时的一个规则。

在正则表达式的模型当中,一个具体的原则指的是一系列共同的前提,和相同体系的抽象。之所以可以进行选择,前提是已经给出了一系列的候选元素可供使用。而进行判断的目的就是得到这些候选元素。

一个正则表达式的特性可完全由这三个方面来描述。判断是基础,替换则是正则表达式语义分析能力的极限。实际应用当中,三个用途成金字塔的形状。不只是因为更高级的应用是建立在低级的应用的基础上,也因为低级的层次确实有不小的用武之地,否则也只能形式正方体结构。

后者的一个典型例子就是计算机网络分层模型。

正则表达式的三个流派,主要不同之处在于元字符的使用规定,本质还是一样的。对PCRE,BRE与ERE流派同时掌握并不是什么困难的事情。大致来说,三者所加的内容有一页左右,还不及对于正则表达式工具的特殊说明。

字符组

正则表达式的特点是用变量描述字符串。

也就是说正则表达式可以用变量代表一个字符串。个人理解。

正则表达式使用的时候注意的是,对于大多数字符(普通字符),使用正则表达式匹配的结果和一般的模式匹配应当是相同的(比如经典的KMP匹配算法)。其次是普通字符具有两个含义,一个是它所代表的字符,一个是它所表示的元字符。在编译原理当中可以看到正则表达式中,单个字符匹配,写成的结果以黑体的形式表达出来,表示它相当于一个终结符。

计算机字符在显示给我们看的时候,如果有换行符,就被显示成平面结构,但是对于计算机而言并没有这个概念。我们可以想象计算机看来,字符串本身是ASCII或unicode的一串字符序列。正则表达式匹配的实际上是一个字符序列的某种子序列,不管这些字符是不是控制字符,是不是可见字符,这个完全对于正则表达式的功能没有影响。

因为正则表达式不会接受所匹配的字符串作为正则表达式匹配指令的一部分

字符组是一列字符的集合,正则表达式程序匹配这一列字符当中唯一的一个。正则表达式将其匹配结果表示成一个确定的字符。原则上一个字符组括在一个中括号当中,形如[abcde]的字符组匹配a, b, c, d, e当中的一个。当只有一个的时候退化成一个单一的形式a,它表示的就是匹配字符a.

字符组可以以范围的形式表示,如[\xAA-\xZZ]表示编码为AA的字符到编码为ZZ的字符。

因为正则表达式在一个编码集当中工作,而不是一个字符集。在工作之前,正则表达式工具要能准确识别出一个编码单位。这对于utf-8这类采用变长编码技术的编码方式来说具有十分重要的意义。

-表示连接符号,如果要匹配-这个字符,只能使用转义了。一般来说,对于-的规定是,出现在字符组当中,并且从左到右可被解释为两个字符的连接时有特殊意义,否则按正常字符处理。其实这个符合一个原则,就是正则表达式引擎尝试将其解释为有特殊含义的字符,如果失败就解释为普通字符。因此正则表达式[0-9a-z]是有意义的,因为两个减号都可以被解释成连接。

在编译原理当中出现的正则表达式比在技术当中严格多了,并且是不支持量词的。学习这个正则表达式,就要放下另一个正则表达式体系,虽然可以保留两者的关联。

字符组的第一个字母是^号时代表不匹配里面所列字符当中的任何一个。

尝试用集合的概念理解正则表达式时,字符组首先代表\(A=\{a,b,c,d,\cdots \}\).使用连接符号的时候,相当于添加了很多个字符到A当中:[abc0-2]={a,b,c}+{0,1,2}. 而使用^号的时候代表[\^abc0-2]=P -({a,b,c}+{0,1,2})

为了更清楚地表达语义,定义\d,\w,\s分别表示数字0-9,单词与数字0-9a-zA-Z,以及空白字符[\t\n\r\v\f](代表制表,回车,换行,垂直制表,换页)

ASCII码值

二进制 十进制 十六进制 缩写 含义
0000 0000 0 0x00 NUL 空字符
0000 0001 1 0x01 SOH 标题开始
0000 0010 2 0x02 STX 本文开始
0000 0011 3 0x03 ETX 本文结束
0000 0100 4 0x04 EOT
0000 0101 5 0x05 ENQ 请求发出响应字符,以确认存在
0000 0110 6 0x06 ACK 确认回应
0000 0111 7 0x07 BEL 响铃,要求终端对用户予以提示
0000 1000 8 0x08 BS 退格,删除或叠打上一字符
0000 1001 9 0x09 HT 水平制表符(horizatonal tab)
0000 1010 10 0x0A LF 换行键,实际上是line feed,满行的意思
0000 1011 11 0x0B VT 垂直定位符号
0000 1100 12 0x0C FF 换页,form feed,满页
0000 1101 13 0x0D CR 回车,用于结束文本行
0000 1110 14 0x0E SO 取消变换,Shift Out
0000 1111 15 0x0F SI 启用变换,Shift In
0001 0000 16 0x10 DLE 跳出数据通讯
0001 0001 17 0x11 DC1 设备控制一
0001 0010 18 0x12 DC2 设备控制二
0001 0011 19 0x13 DC3 设备控制三
0001 0100 20 0x14 DC4 设备控制四
0001 0101 21 0x15 NAK 确认失败回应
0001 0110 22 0x16 SYN 同步,同步信号
0001 0111 23 0x17 ETB 区块传输结束
0001 1000 24 0x18 CAN 取消
0001 1001 25 0x19 EM 连接介质中断
0001 1010 26 0x1A SUB 替换
0001 1011 27 0x1B ESC 退出键,转义
0001 1100 28 0x1C FS 文件分区符
0001 1101 29 0x1D GS 组群分隔符
0001 1110 30 0x1E RS 记录分隔符
0001 1111 31 0x1F US 单元分隔符
0111 1111 127 0x7F DEL 删除

在dos系统当中控制符可能被显示为一些有意思的符号,比如SOH被显示为一个笑脸。

如何在键盘上映射控制字符

CTRL键的原意就是把键盘上的字符转成ASCII小64的控制字符。对于小写字母的情况,是比它的ASCII小32的控的字符,所以CTRL+I水平制表,CTRL+J换行。CTRL+M回车。键盘向计算机发出的也是ASCII码值。如果没有相对应的符号,实际是使用的新定义或者是新的控制序列。哑终端常使用控制序列。哑终端是相对于其它聪明的计算机来说,功能较为有限的计算机终端。其含义根据不同的场合而变化。

终端的概念很重要。可以从终端那里知道制表符的意思是移动到某个制表位的地方,然后进行显示。也就是说,显示器等若一个方格。所以在移动的时候就知道TAB键的工作原理了。它在移动的时候,可能的位置是0,4,8,12,…按下tab键的作用就是跳到最近的控制点上。

随着不使用纸张打印,这些字符的含义也有了变化,比如换页在屏幕终端就变成了清屏。由于单个的转义码开始不够用了,所以就有了转义序列。

分隔符和操作系统当中的记录式文件结构有不小的关系吧。介质结速EM的意思是磁带已满。ESC的最初的作用是避免其后的控制字符被控制设备解释。

替换的作用是下个可打印字符以二进制的形式被打印出来

CAN用于取消一个包的传输,NAK要求重传一个数据包。ACK表示包被正确接收。

在半双工通信的环境当中,ENQ从主站发出,表示要求从站发送下条报文,从站发送EOT表示自己完成了传输。

传输设备控制码的意义,可以使得设备之间不需要用到更多的控制线,可通过一条线路上传输ASCII实现传输控制。

DLE表示会话结束,释放线路资源。

编码为0的字符是一个特例,指的是纸带上那些没有穿孔的地方。而DEL的ASCII很特殊是因为它的ASCII全是1,这样一来,没有穿孔的地方就全部变成穿孔的地方了。因为在纸带上遇1表示穿孔,0表示不穿孔。

看来ASCII的解释可以从穿孔纸带,打印机,控制终端,现代终端多个角度。

有一个特别的字符组.号,表示匹配除了换行符\n之外的所有字符一次。

字符组在原则上允许交,并差操作并构成了正则表达式理论上用的比较多的运算。一般来说,并置就是直接连接。这一个功能是都支持的。

字符组总结:在一列集合中选择一个匹配,对字符组允许作否定,连接的操作,一些字符组可以提供范围功能。所以在正则表达式当中关于字符组的基本特性可以总结为以下的几个方面: 1. 如何选择唯一的匹配字符 2. 使用否定的方式选择唯一字符 3. 使用范围的方式选择字符 4. 有没有什么选择特定语义的字符的简记法 5. 建立在字符组上的交并差运算是否支持

运算之后还是一个正则表达式。所以对正则表达式的与或非也应当有所支持。不过这是正则表达式的理论的问题,本笔记并不能涉及到。

量词

量词用于细化对于字符或者字符组的规定,约束出现的次数。

对于正则表达式而言,每一个子表达式都称为一个模式。量词准确地说是用来表示所匹配的模式所出现的次数。表示的是这样的模式所匹配的文字匹配多少次。

也许用扩展比较合适。正则表达式返回的匹配结果是包含了次数的,也就是返回的是完整的字符串。

量词跟在它所限定的模式的后面,仅作用于前一个最小匹配模式。通用量词用{m,n}来描述。比如(pattern){m,n}表示模式pattern出现\(m\)次到\(n\)次之间的被匹配。

其实量讯号的使用有两个关键的地方,一个是如何标识它所修饰的单元,另一个是如何传入匹配的量词标识的次数。用符号表示一个是pattern,一个是range.

量词的范围是非负的数值,根据具体语言的不同,0可以省略。

一般来说正则表达式的书写是非常严谨的,不像C语言那样,参数之间可以后跟若干个空格。所以在量词之间也不要加入空格。

有几个常用的量词使用了特殊的元字符表示,表示限定的模式出现0到无穷多次,+表示所限定的模式至少要出现1次,?表示限定的模式至多可以出现1次。需要注意的是模式.在实现上效率还是比较低的,所以应当尽量避免使用。

在通配符体系当中,常有一些表示任意匹配的元字符,比如*,.,?其含义和正则表达式中的含义略微有些区别。

量词如果是一个范围,就可能有多个可以匹配的字符串。

正则表达式进行匹配的结果相当于在一个线性序列当中选择一个范围,这个范围就是所匹配的结果,有了一个这样的匹配,才会知道如何操作指定的子串。

正则表达式匹配的过程可以看成是逐个对应的问题。一个正则表达式提供了特定的信息,其信息的截止长度是到字符串的末尾。所以在匹配正则表达式re的时候,如果匹配从a开始,就从下面的字符串中逐个应用某个模式,如果在某一阶段不能匹配,就进行回溯,至到在不能回溯,而又没有将正则表达式匹配到最后的时候才返回匹配失败。

正则表达式的匹配也可以看成是一个树的匹配。每一个量词就决定了下面有多少个结点可以使用,比如a{3,5}建立的子树就有aaa,aaaa,aaaaa三颗。

量词的优先级问题。量词的优先级的标准是在有多个匹配的时候先匹配尽可能多的字符还是先匹配尽可能少的字符的问题。这个与正则表达式引擎的实现有问题,对于能否匹配不产生影响。忽略优先量词的意思是先匹配少的,匹配优先量词的意思是先匹配尽可能长的字符串。

说到这里正则表达式工作的过程完全就等价于一颗树了。能够到达树的终点说明可以匹配。但是到达树的终点可能有不同的路径,每条路径对应于一个可能的匹配。

实际应用中量词的优先顺序影响的不只是效率,更影响了最长匹配还是最短匹配的问题。如a{3,5}匹配aaaa,如果是匹配优先量词,就是aaaa,但是{m,n}形式的量词是忽略优先量词,所以匹配的结果是aaa(前面的三个a)。

多选,分组与捕获

多选的意思是在多个可以匹配的模式当中选择其中的一个,其用法是\((p1\vert p2)\). 如果中间没有分隔符,和普通匹配的效果一样。不过多选其实还有一个比较重要的副作用,就是创建模式所匹配字符串的一个拷贝。这可以通过编号引用用在后续的替换当中。分组也是用这的方法,意思是模式作为一个整体。后面所跟的量词都是作用于这样的一个整体。

捕获的目的是提取出符合模式的字符串。PCRE正则表达式当中用括号来表示开始一个捕获。之后可以通过对编号的引用使用它。编号的原则是,按左括号从左到右出现的次序决定相应模式编号的顺序。编号从1开始。如果使用了分组(括号)而不想分组被捕获,则使用语法(?:)来加以说明。

python支持\g<num>\g<num>形式的引用。

考虑到一般脚本,也支持用$num的形式引用。

断言

断言的作用是要求所匹配模式前后必须匹配某些特征,但是其匹配不作为匹配返回结果的一部分。断言有很多类型,常见的断言有单词边界,行起始,行结束以及环视。

断言的最一般形式就是环视。另外在正则表达式当中,断言都是跟着所匹配的模式的。比如如果ptwo在正则表达式中作为pone的断言,那么一般来说,其含义是ptwo后的模式正好是pone,中间没有任何其它的成份,或者ptwo前的模式正好是pone,中间没有其它任何成份,换言之,断言是紧跟它所断言的模式的。不会说,在文本某个位置符合了某个断言,但是隔了很长的文本之后,所断言的模式才开始出现。在通常情况下这种分隔的情况也不常用。但是或许在自然语言处理当中很有价值呢。

单词边界顾名思义就是一个模式的两端是单词分隔符,如空格等等。可以作为单词边界的字符构成一个集合,这个集合用字符组\b来表示。

这样一来\b就不一定表示断言,其所匹配的字符也会作为结果的一部分。

单词边界在PCRE当中常见的是\b,\B,\w,\W分别表示其左边是单词(非单词),右边是单词(非单词)。

这个说明PCRE的规定不太一致,因为\d表示的是一个匹配字符组,而\b却要表示一个断言。

当匹配的内容被视为一个字符串的时候,^被视为文本开始的位置,$被视为文本结束的位置。当匹配的内容被视为段落文本的时候,^被视为每行的开始位置,$被视为每行结束的位置。

不同的语言可能使用不同的符号表达这个语义。

环视的作用是加强对匹配文本模式所在位往置的限制。以所匹配文本的位置看,环视分为顺序环视和逆序环视,也就是匹配模式左端应当符合的模式,以及右端应当符合的模式。

原则上也可以通过捕获分组来实现环视的功能呀,不知道和以这种方式实现环视的功能有效率上的不同没有,还是说,环视在编译原理上和捕获分组一样?

PCRE的环视以下面四种形式实现(两种顺序乘是否):肯定顺序环视(?=pattern),否定顺序环视(?!pattern),肯定逆序环视(?<=pattern),否定逆序环视(?<!pattern).

正则表达式的全局选项

这个是经验的问题,因为为了书写的方便而设立一定的匹配模式,比如忽略大小写的功能。

全局选项实际上也可以作为局部选项而出现,因为它们都是加在一个模式之上的。一般来说。模式主要有:

忽略大小写模式 在此模式下一个字符可以与它的大写或者小写形式替换,也就是说不再只是代表这个字符,而是相当于包括大写和小写形式的一个分组。
单行模式 在此模式下,字符串被认为是一体的,点号可以匹配\n.
多行模式 在此模式下,^$分别匹配每一行的开始与结束
注释模式 在此模式下的正则表达式部分表示一个注释,里面的内容可以自由书写,在表达式较长或者有多行的时候可以提高正则表达式的可读性。

坦白来说,正则表达式本身确实没有什么可读性。

PCRE中在正则表达式内部设置某个模式的时候使用(?modifier),要取消某个模式的时候使用(?-modifier)。其中i表示忽略大小写,s表示单行模式,m表示多行模式,x表示注释模式。可以多个模式进行嵌套。

注释模式下用#开始一个注释,注释直到行尾。

对于我们来说比较有意义的时一个字符组所表示的真正的含义,所以在忽略大小写模式下,并没有对于匹配字符串的整体造成太大的影响。所以单行模式和多行模式要特别注意。注意的是什么呢,注意的是^$的含义。

正则表达式工具

sed

sed命令替换的基本格式是<address>s/<pattern>/<replacement>/<flags>

比如s/suff/(&ix)/g用于将suff替换成suffix

sed正则表达式的中文问题

linux下unicode正则表达式的匹配情况取决于当前系统的语言环境特别是编码。在一般情况下,元字符这部分直接可以在命令中替换。标准用法是使用\xx转义。

vim中正则表达式

匹配一个中文字符可以使用[^\x00-\xffff].

sed匹配空行和空格组成的行

删除空行的模式是 /^$/d

删除由空白字符组成的行的模式是/\s*$/d

VIM当中的正则表达式

在VIM中使用替换命令的一些情况

没有经验真可拍,稍微有点特殊的情况就得自己猜。

换行符属于一行的行尾,而匹配模式也可以仅仅只有一个位置符号,没有特定字符。总之,正则表达式的结果不过是一个位置范围而已,有时起始位置和结束位置相同也没有什么关系。

比如:

  1. 将行尾的换行符替换成制表符:s/\n/\t/,

这说明在正则表达式中支持转义字符

  1. 在每行之前增加一个制表符: s/^/\t/.

正则表达式中对于控制字符的转义

比如%s/\n\([^\t[:alpha:]]\)/\1/g

grep

在我的linux笔记中已经提到vi,sed,grep属于BRE流派。并且关于正则表达式的基本概念我们也都已经讲清楚了。在这里主要是如何用具体的工具实现正则表达式强大功能的问题。所以先介绍grep工具。

grep命令的功能就是在一个或者多个文件里搜索一个模式串并输出。和sed等工具相比,不必使用特殊命令(比如sed的s替换命令),因为grep的功能定位就是从文件中搜索模式(grep的名字由来就是g/re/p命令)因为更专业,所以用的时候更简单。而且unix工具的一个特点就是一个工具的功能得到准确的描述,所不同的只是实现的方法问题。

我们好好体会这一点的方式是,我们能够从定义推演出工具的设计思想来。从定义可以知道grep的功能有两个关键词,模式和文件。分别以pattern和file来代表。而向应用程序传递参数的时候又要有相应的选项,所以grep的命令语法是:

grep options pattern filename(s)

加上s表示grep可以接受多个文件。

  1. grep可以从标准输入流中搜过一个模式串。 (管道设计unix系统的特殊机制,现在我们知道管道可以看成具有特殊文件描述符的一个对象就行了。只要在程序设计中使用了这个特殊的文件描述符,就能通过管道方式传送数据)

  2. pattern是一个BRE正则表达式串。但是我们在启动grep程序之前,参数字符串还要经过shell变换处理一下。因而如果字符串比较特殊,还要经过一次shell的转义。一般要用引号括住。

示例用法:

grep "sales" emp.lst " 在emp.lst文件中搜索具有模式sales的行。输出到标准输出。

grep president emp.lst 同上,搜索的是模式president。

  1. 在grep后面的文件参数中有多个的时候,grep在输出结果每行开头会显示文件名和行号。

  2. grep参数解析的过程我们可以看成首先试图从argv字符串中读出一个作为pattern,或者通过一个选项给出pattern,如果不能满足,就将参数解释为一个文件名。

grep的参数解析

  1. grep的参数(理解正则表达式失配,grep程序匹配这句话的含义)
-i 不区分字母大小写(ignore case)
-v 显示不含指定模式的行(invert match,改变匹配模式)
-n 显示行号和行内容(默认只显示行内容)(line number)
-c 显示所匹配模式的行数(count)
-l 只显示匹配的文件(files with matchesO)(-L的意思与之相反)
-x 匹配整行的内容(即整行恰好被匹配)(line regexp)
-f file 从文件中读取模式,每行作为一个模式,行之间是与的关系
-E 使用ERE模式匹配(-G是使用BRE模式,BRE是默认设置)
-P 使用PERL模式匹配(并非是POSIX规范)
-e pattern 将-e参数后的一个参数解释为正则表达式。(如果正则表达式以减号开头,能够避免该参数被解释成一个选项)。
-r 读取文件时在目录下递归查找。

在VIM当中可以使用转义符进行输入。而转义的规定很多都来自于C语言一系。关于转义,其中由引导符和定义符构成。一个完整的转义应当是有定义的那些转义。然而如果转义后的序列不被识别,有可能被当成正常的字符处理,也可能报错。

一些技巧

sed中如何替换空行[2013年 03月 17]

在sed当中正确使用相应的表达式就可以了。因为sed的匹配是以行为基础的。使用符号sed '/^$/d'可以将文件中的不含任何字符的行删除。

注意在sed当中正则表达式前面没有像vim那样的s字符。sed即使是替换文本,也不使用s开头。