前言
一、缘起
1.
前面在字符编码系列文章的前言中曾说过,类似于字符编码这样基础、重要、应用广泛而又特别容易让人困惑的主题还有字节序(即大小端表示)、正则表达式以及浮点数实现、日期时间处理等等。其中,字节序、正则表达式跟字符编码的关系非常密切。字符编码以及字节序的问题已经在字符编码系列文章中介绍过了,这个系列再来讨论正则表达式。
不同于字符编码,正则表达式目前市面上并不缺乏专业著作,比如那本被誉为正则表达式学习圣经的《精通正则表达式》就很值得一读,另外该书的译者余晟先生所写的《正则指引》也不错;如果仅用于入门,则《正则表达式必知必会》肯定不能错过,还有网上流传极广的《正则表达式30分钟入门教程》也是不错的入门资料。
但是,结合我自身痛苦的正则表达式学习经历和运用体会,仅有这些是远远不够的。记得被大家称之为“轮子哥”的大神级程序员vczh在知乎上说过,当初被正则表达式虐得一气之下,干脆自己写了一个正则引擎(源码托管在Github上),才算真正彻底搞懂正则表达式(于是被戏称为“一言不合”就造轮子)。当然不是每个程序员都能如此生猛,但即便都有这么生猛,似乎也没必要都像“轮子哥”这样真的自己都去再造一遍正则引擎的“轮子”。
那到底应该怎样才能最高性价比地掌握正则表达式这个神器呢?这正是我写这个系列文章的目的。
2.
正则表达式,一听就是个非常专业的术语,对于大多数人而言完全不知道这跟自己有啥关系。但事实上,只要是平时用Word写点东西的人,都可能用得上。
没错,Word就支持正则表达式,虽然所支持的功能远远不如常规的正则表达式强大,语法上也有很大的不同,因而只能算得上是准正则表达式或类正则表达式;但对我而言,只要是用Word写文章,就已经无法想象没有正则表达式的情形了。
当然,在Word中使用正则表达式,只能算是小试牛刀。正则表达式更大的用武之地在于各大常用高级编程语言、编辑器以及grep、sed和awk等命令行文本处理工具中,用以文本的查找、提取、替换、切分等操作。
3.
正则表达式是典型的那种没用过的话,不觉得对自己有什么影响,可是一旦用过了,就再也回不去了的神器。当然,我这里所说的“用过”,不是指简单用用一些基本功能,而是指能够熟练运用其基本功能和高级功能。用得越熟练,就会越惊叹于其强大与神奇。
看到这里,我相信某些学过正则表达式、会使用一些基本功能的童鞋,心里或许在犯嘀咕了:神器是神器,可这玩意儿看起来就像天书一样,也太难学、太难懂了,要达到熟练运用的程度,谈何容易!短短的一个正则表达式,或许不到10个字符,其中的每个字符都认识,但连在一起,却越看越迷惑,越想越迷糊……
是的,正则表达式既然被捧上了神器级别的高度,自然是有着相当强大的功能,这当然就意味着其有非常深厚的内涵,也就意味着有很多需要注意的细节。
4.
注意,我这里没有说正则表达式是因为复杂而难以理解,因为深厚的内涵不等于复杂,细节很多不意味着难以理解。看到这里,或许有人有意见了,正则表达式还不算复杂?还不够难理解?你秀智商呢还是秀优越感呐?哦,相信我,其实这两者我都不太沾边。智商我也只是中等而已,否则早就不在这里码字了;而优越感则更提不上——既不高也不富更不帅,何来优越感?!
其实,我真正想说的是,繁复或许是真的,杂乱倒未必。因此,简单地说正则表达式复杂,似乎不够准确而客观。正如跟一个牛叉而又性格独特(废话,真正牛叉的人基本上都有独特的性格)的人打交道,关键不在于纠结其性格的独特、脾气的古怪,而是重在充分了解并理解其独特的性格、古怪的脾气,然后在此基础上与他/她进行良好的沟通,以便能好好发挥其牛叉之处。
5.
学习并熟练掌握正则表达式的过程也是如此——关键在于先要摸透其“性格”到底独特在哪里,其“脾气”又究竟古怪在何方。一旦摸清楚了其“性格”,其“脾气”,学习起来就事半功倍了。
因此,我下面准备从我自己的角度,先尝试着来分析一下正则表达式那独特的“性格”与古怪的“脾气”,看看究竟为什么正则表达式给那么多人的感觉都是那么难以“亲近”。
二、正则表达式为什么难学?
1.
对于正则表达式的分析和解读,目前大多数文章和书籍多集中在正则表达式自身,比如对正则表达式的各个元字符、元转义序列以及匹配原理的分析和解读上。
当然,这些自然也是很有必要的,而且是学习的主要内容,是理解正则表达式所必需的。然而,很多人在看了大量这类文章和书籍之后,仍然觉得正则表达式很难看懂,不好理解,经常有一种智商被碾压的即视感。
2.
难道真的是正则表达式的学习者智商不够吗?其实,理解一个事物,都应该有两个维度,或者说两个层面:一是,深入到该事物本身里面去理解;二是,跳出到该事物外面,站在更高的一个维度或层面来理解。
正如苏轼那首著名的哲理诗《题西林壁》所说的,“不识庐山真面目,只缘身在此山中”。很多时候往往是这样,当你只从该事物本身来看的话,就如在云里雾里,是远远不够的;而一旦跳出到该事物之外,站在更高的一个角度来看,则又正如王安石的《登飞来峰》中所说:“不畏浮云遮望眼,只缘身在最高层”。
3.
对正则表达式而言,前者正是目前大多数文章和书籍在做的;而后者,却很少有文章和书籍能够跳出正则表达式,站在更高的维度或层面来分析和解读正则表达式。这里就包括了被誉为正则表达式学习圣经的《精通正则表达式》,以及该书的译者余晟先生所著的《正则指引》两书。
这里需要特别强调一下的是,我绝没有贬低上述这两本专业著作及其作者和/或译者之意,而且恰恰相反,这两本专著正是本系列文章的重要参考书。尤其无论是作为《精通正则表达式》的译者,还是作为《正则指引》的著者,余晟先生都绝对称得上是既专业又严谨。
4.
那么,前面所谓“更高的维度或层面”,到底指的是什么呢?那就是,从编程语言发展史以及编程范式的角度来看正则表达式。什么?正则表达式竟然也算得上是一门正式的编程语言吗?别急,请继续往下看。
正则表达式有一个非常明显的特点:高度简洁、高度抽象。正则表达式中短短的几个字符,或许就代表了一段复杂的处理逻辑和匹配算法。
5.
我们知道,程序代码是对现实事务处理逻辑的抽象,而正则表达式则是对复杂的字符匹配程序代码的进一步抽象;也就是说,高度简洁的正则表达式,可以认为其背后所对应的是字符匹配程序代码,而字符匹配程序代码,背后对应的是字符匹配处理逻辑。
因此可以这么认为,字符匹配处理逻辑,抽象为字符匹配程序代码;字符匹配程序代码,再进一步抽象为高度简洁的正则表达式。所以说,高度简洁的正则表达式也是高度抽象的。
6.
事实上,从编程语言发展的角度来看,正则表达式也是一种编程语言,而且是属于第4代语言(4GL)——面向问题语言(第1代语言为机器语言——由0和1组成的位串,第2代语言为汇编语言——用接近于英语单词的助记码mnemonic code来代替由0和1组成的位串,第3代语言为高级语言——用接近于自然语言的语法元素编写程序,如C/C++、Java、C#、Perl、Python、PHP、JavaScript等语言,第4代语言为面向问题语言——用针对问题领域专门设计的语法元素编写程序或表达式,如SQL、SAS、SPSS、LaTeX、Regex(即正则表达式)等,第5代语言为人工智能语言——Prolog、Mercury、OPS5等;不过,从第4代语言到第5代语言的演化还不是很清晰,目前学术界争议较大,这里不作讨论)。
【注:这里强烈推荐郑晖先生所著的《冒号课堂——编程范式与OOP思想》一书,该书既以宏观视角,纵向审视了编程语言发展简史,横向比较了各类编程语言的特点;又以微观实践,娓娓道来各编程范式的优劣得失,更是深入探讨了面向对象编程的方方面面,是不可多得的一本中文原创计算机专著。这里是我在豆瓣写的该书书评,供参考。】
第4代语言相对于第3代语言,更专注于其所应用或者说其所适用的某个特定的业务逻辑和问题领域。程序员主要负责分析问题,以及使用第4代语言来描述问题,而无需花大量时间去考虑具体的处理逻辑和算法实现(事实上,最初之所以提出第4代语言的概念,就是希望非专业程序员也能做应用开发)。
7.
从编程范式(programming paradigm)的角度上来讲,第4代语言属于声明式编程范式,声明式编程重在目标而非过程、重在描述而非实现,以声明式语句直接描述问题,专注于问题的分析和表达,而非专注于处理逻辑和算法实现过程,其具体的处理逻辑和算法实现是由语言解析引擎(编译器或解释器)来负责的。
当然,这样一来,这些由语言解析引擎实现的处理逻辑和具体算法其通用性就会较差,只能适用于某些特定业务或特定领域。也正是这个原因,第4代语言基本都是局限于某些特定领域的,多被认为是领域特定语言DSL(Domain Specific Language)。
区别于算法实现可由程序员自由灵活设计的通用编程语言GPPL(General-Purpose Programming Language,作为第3代语言的高级语言基本上都属于通用编程语言),领域特定语言DSL的算法基本上由语言解析引擎自动实现,程序员灵活设计、自由发挥的空间很小,因此DSL几乎没有通用性(而且DSL大都是非图灵完备的语言),只能专用于解决特定业务方向和业务领域的问题。
比如,SQL是专用于数据库操作的语言、SAS和SPSS是专用于统计分析的语言、LaTeX是专用于排版的语言,而正则表达式Regex(regular expression)则是专用于处理字符匹配的语言。
8.
理解了这一点,就比较容易理解正则表达式是字符匹配处理逻辑的抽象;更进一步地来说,正则表达式中的某些元字符与特殊结构,可理解为某种具体的程序逻辑和算法的体现。
比如,正则表达式中的量词*这一元字符,就是高级语言的处理逻辑“循环结构”的体现(具体来说量词*代表的是不定次数循环),而前后多个量词的嵌套就是多层循环的嵌套;或运算符|这一元字符,就是高级语言的处理逻辑“选择结构”的体现。
而当或运算符|出现在由量词*所限定的圆括号中时,其实就是“循环结构”中嵌套了“选择结构”;而如果进一步地,“循环结构”所嵌套的“选择结构”中的某个分支,又被某个量词*所限定,那么则相当于“循环结构”所嵌套的“选择结构”又嵌套了“循环结构”。
理解这一点非常重要,是快速、深入理解正则表达式的一把钥匙、一条捷径。站在编程语言发展史和编程范式的高度,再结合对正则表达式本身原理的深入理解,里外结合,高下相较,既登高望远、一览众山小,又洞幽烛微、复观千水深,正则表达式的奥义,就能尽在掌握之中了。
9.
当然,正则表达式之所以难学、难理解,除了由于正则表达式作为一个字符匹配领域的DSL,具有高度简洁、高度抽象的特点之外,大致上应该还有以下几个原因:
0) 学习者不求甚解,不了解正则引擎内部的基本原理
作为正则表达式的使用者,不需要深入了解正则引擎内部原理的技术实现细节,那是正则引擎开发者更应该了解的;但若完全不了解其基本工作原理和运行机制,也是不足取的。
1) 有多个多义元字符,特别容易使人混淆、迷乱
比如-、+、?、^,尤其是元字符?,既可以作为量词表示其所限定的子表达式为可选(即匹配0次或1次),也可以置于量词之后表示懒惰匹配,而且还有很多特殊分组结构中用到它,比如(?<name>sub-regex)、(?:sub-regex)、(?>sub-regex)、(?=sub-regex)、(?!sub-regex)、(?<=sub-regex)、(?<!sub-regex)、(?|sub-regex)、(?modifier-modifier)、(?(condition)|)、(?R)、(?num)、(?#comment)等;还记得我自己当初刚开始学习的时候,一看到正则表达式中的问号?,我就有一种独自在风中凌乱的感觉。
2) 转义也是难点
什么情况下需要转义,什么情况下不需要转义,貌似复杂得令人抓狂;当然,其实是有一定的规律的,掌握了这些规律,再遇到转义问题,不至于心潮澎湃了。
3) 学习期望与学习方法不对
不应该期望一次性记住、学会并熟练运用,正确的学习姿势应该是:先简单入门,对一些基本的规则与元字符大致了解一遍,有个印象就好,在需要时再回过头来看,不用刻意去强行记忆;然后接下来就应该多练、多实践、多运用,边学、边深入、边熟练。
4) 有用于入门的好教程、备忘单,也有用于深入的大部头专著,但却缺乏好的速查手册
由于需要边学、边深入、边熟练,因此,平时手头边更需要的不是简单的入门教程、备忘单(Cheat Sheet),也不仅仅是知识点分散于各处的大部头专著,而是一本按语法元素将知识点综合在一起进行编排的、在需要回过头来看时能够随时快速翻查的速查手册。这样,在实践运用中遇到问题就可方便随时快速翻查,而这一点恰恰对于正则表达式这种不可能短期内快速掌握并熟练运用的专业工具的学习与使用非常重要。
5) 没有使用好的学习工具
你知道regex101.com、RegexBuddy、regexper.com等正则表达式的专业网站和专业工具吗?这些堪称学习正则表达式的神器,可令学习事半功倍,但很多人不知道,或知道但很少使用。
三、关于本系列文章的编排设计
1.
本系列有关正则表达式的文章,出自于我自己在学习正则表达式的过程中所经历过的真切体会和真实痛点。因此,正如前面所述,采取的编排风格类似于速查手册。
但是要特别注意,这仅仅是出于边用边快速翻查的目的而作出的编排设计,不等于是通常大家所理解的那种简单解释一下概念,然后罗列一下功能,再加几个示例的鸡肋般“食之无味,弃之可惜”的字典式简易手册(这种简易手册仅供入门使用);更不是将元字符、元转义序列、特殊结构的简单解释编排在一张A4大小纸张上的备忘单(当然,这种备忘单也并非没有意义,至少通过一张A4大小的纸张就可快速了解正则表达式所支持的语法元素包括哪些,因此本系列文章也会提供几份我收藏的备忘单供大家参考,但显然也仅供入门使用)。
这也就是文章名称中之所以特别强调“刨根究底”,而不是直接名之为速查手册、快速参考之类的重要原因。
2.
因此,本系列文章与相关专著一样,也同样会涉及到正则引擎内部的相关匹配原理与匹配机制的解释(而且还独创性地总结为了几大原则,便于“以简驭繁”、“提纲挈领”地快速掌握要领以便于记忆和理解),只是与其他专著用专门章节进行介绍不同,而是各自糅合于对相关语法元素的解释之中了。
这种为了便于快速翻查而没有将匹配原理与匹配机制予以专章介绍的特殊编排,自然也有其缺点(比如,你可能会在不同的语法元素中发现类似的雷同解释,这或许有重复啰嗦之嫌,但毕竟这符合我们的编排目的),但问题在于市面上进行专章介绍的专著已经有很多了,再重复它们意义不大;而专门针对前述的正则表达式学习和运用痛点的文章和专著则基本没有,而这正是本系列文章的意义和目的所在。
3.
也因此,出于更偏向于实践运用的目的,本系列文章不会花费过多的笔墨在DFA、NFA等过于深入的正则表达式幕后技术细节的讲解上。
事实上,我认为只要大致了解它们的基本原理与工作机制以及两者之间在功能特性上的差异,就完全可以熟练掌握并运用正则表达式了,除非你是想自己开发一个正则引擎,实在没必要过于陷入DFA、NFA等状态机(自动机)的实现细节上。
虽然前面曾提到过,“轮子哥”vczh为了彻底搞懂正则表达式,硬生生自己重新造了一个“轮子”。然而,要是为了学个正则表达式,都非要这样重造轮子,既无可能,也实在没有必要。
那么,真的在不重造轮子、不陷入DFA、NFA等技术细节的基础上,也能搞懂正则表达式?
我自己的体会是,能!本系列文章就是我自己学习心得体会的总结,文章中除渗透了前文所述的“里外结合,高下相较”这一相对“务虚”的基本思路之外,当然也有相对“务实”的“干货”——总结出来的八大原则(包括六大基本原则:最左原则、先到先得原则、最长原则、逐位置依次尝试匹配原则、整体匹配优先原则、占有匹配优先原则;以及两大衍生原则:最左先到先得原则、最左最长原则)、多角度立体的文字讲解,以及大量图示。
因此,我相信通过反复阅读本系列文章,再多加练习、勤于实践,然后在实际运用时再不断回过头来随时翻看,应该完全可以熟练掌握这个像毒品一样会让人用上瘾的神器。
好了,牛皮吹过了,到底是骡子是马,后面会拉出来遛遛……
四、下面是正则表达式系列文章将会涉及到的内容:
一)什么是正则表达式
二)为什么使用正则表达式
三)正则表达式简史(含正则表达式流派简介)
五)正则表达式基础
六)八大原则简介,包括:
六大基本原则:最左原则、先到先得原则、最长原则、逐位置依次尝试匹配原则、整体匹配优先原则、占有匹配优先原则;
两大衍生原则:最左先到先得原则、最左最长原则
七)元字符逐个详解,包括:\、(、)、[、{、.、-、*、+、?、|、^、$,其中-、+、?、^为多义元字符
八)元转义序列逐个详解,包括:
固定字符:\a、\b(字符组内部)、\e、\f、\n、\r、\t、\v(非Perl系)
字符组简记:\d、\D、\h、\H、\N{}、\p{}与\pP、\P{}与\PP、\s、\S、\v(仅Perl系)、\V、\w、\W
进制转义字符:\octal-num(Perl系中也可写作\o{octal-num})、\xhex-num(Perl系中也可写作\x{hex-num})、\uhex-num(非Perl系,Ruby1.9+等个别语言中还可写作\u{hex-num})
控制字符:\cX系列
锚点:\A、\z、\Z、\b(字符组外部)、\b{}、\B、\B{}、\G
引用:\num、\g{num}、\gnum、\k{name}、\k<name>、\k‘name‘
修饰:\E、\F、\l、\L、\Q、\u(仅Perl,不是仅Perl系)、\U
其他:\C、\K、\N、\R、\X、\<、\>
九)特殊构造(特殊结构)逐个详解,包括:
字符组、多选分支结构、捕获分组、命名捕获分组、非捕获分组、预查分组(即环视分组)、固化分组(即原子分组)、嵌入条件分组、内联选项与取消内联选项分组、注释分组、分支复位分组、表达式引用分组、平衡分组等
十)匹配模式逐个详解,包括:i、s、m、x、g等常用匹配模式
十一)POSIX字符组方括号表达式、排除型POSIX字符组方括号表达式
十二)字符组运算:字符组减法运算、字符组逻辑与运算
十三)正则表达式各语法元素优先级
(未完待续)