正则指引
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

引子:关于正则表达式……

正则表达式这个名字看起来总有点古怪,概念似乎也不简单,甚至需要用一整本书来讲解;可是,它到底是什么呢?

身为技术人员,我相信你总会与字符串打交道,相应地,各种语言也都提供了与字符串有关的函数。不妨先看看下面几个问题,字符串函数是如何解决的(下面的代码使用Python语言,它很直观,正文里有基础的介绍。现在,你只需要知道def是定义函数的关键词即可)。

1. 判断字符ch是否数字字符

      def isDigit(ch) :
          return ch == "0" or ch == "1" …… or ch == "9"

2. 判断字符串str是否电话号码(为简单起见,现在只考虑固定电话号码,也就是长度在7~8位之间的数字字符串,且第一位不为0)

      def isPhoneNum (str) :
          if len(str) >= 7 and len(str) <= 8 and str[0] != "0" :
            for ch in str :
                if not isDigit(ch) :
                    return false
            return true
          return false

任务本身并没有增加太多,但是程序复杂了很多倍;如果你不这样看,那么,来个更复杂的。

3. 找出一段文本中所有的电话号码

最直观的办法是,在字符串中的每个位置截取7~8个字符,调用之前的isPhoneNum()。这么做看起来没问题,只是效率太低。

当然,很容易就可以做点改进,只在“当前字符为数字字符”的情况下调用isPhoneNum()。这样效率倒是改进了,但是还有问题没有解决:要求找到的是长度大于等于7个字符,小于等于8个字符的“数字字符串”,而不是“子字符串”——也就是说,假如数字字符串是64240000,需要将它找出来;如果数字字符串是13800138000,则需要忽略它,以及其中的任何子串(比如1380013800138000)。

所以,用isPhoneNum()找出字符串之后,还需要保证它之前的字符不是数字字符,之后的字符也不是数字字符。看起来很简单,但为了避免越界错误,又需要判断:如果当前字符是整段文本的第一个字符,则不需要判断之前的字符,因为它不存在;同样,如果找出的字符串在整段文本的末尾,则不需要判断之后的字符,因为它同样不存在……

到现在为止,即便只是找到最简单的固定电话号码,程序也非常复杂,难以维护。如果要查找的是形式更多变的文本,比如带区号的电话号码(021-64240000 或者03718888888)、手机号码(13800138000或者+8613800138000或者013800138000),程序更是不可想象,更不用说文件路径名、URL地址、电子邮件地址了!然而,日常开发中我们又确实经常需要面对这类任务,有什么更好的办法呢?

正则表达式就是解决这类问题的万灵药。虽然许多人有点看不起它,觉得不入流,科班教材里也不会花太多篇幅来介绍它,但它确实是解决问题的利器——之前提到的三个例子,用正则表达式都可以轻松解决。

1. 判断字符ch是否数字字符

      def isDigit(ch) :
          return re.search(ch, "[0-9]") != None

看起来很复杂,其实并不复杂:这里真正要关心的就是正则表达式[0-9],它表示“从0到9之间的任意字符”,很形象吧?re.search()是正则表达式运算函数,它判断ch能否由正则表达式[0-9]匹配,可以则返回一个结果,否则返回None(这些细节正文中会讲到)。

2. 判断字符串str是否电话号码

      def isPhoneNum(str) :
          return re.search(str, "[1-9][0-9]{6,7}") != None

这个正则表达式最开始是[1-9],表示第一个字符必须是1~9 之间的数字字符;之后是[0-9]{6,7},表示长度在6和7之间,由0~9之间的数字字符组成的字符串(两部分加起来,整个字符串的长度在7和8之间)。要解决的问题复杂了,正则表达式仍然直观形象。

3. 找出一段文本中所有的固定电话号码

      def findNumStr(str) :
          return re.findall(str, '(?<![0-9])[1-9][0-9]{6,7}(?![0-9])')

这个正则表达式之前多出了(?<![0-9]),表示“之前不能是[0-9]”;之后多出了(?![0-9]),表示“之后不能是[0-9]”。虽然稍微复杂点,但意思明确,而且不难理解。re.findall()的意思也很明显:找到所有这样的字符串。

可以想象,循着这种思路,更复杂的电话号码、手机号码等任务都不难解决。更重要的是,之前需要许多行语句才能完成的任务,现在基本上只需要一个正则表达式,一条语句就可以完成。正因为如此,不少人虽然认为正则表达式不够花哨、漂亮,却不得不承认它是一种 “匕首应用”——匕首,没有十八般兵刃那么大方,关键时候却不可或缺,所以值得花时间练练。同样,正则表达式虽然不能用来显摆,但总有派得上用场的地方,花时间练练绝不是坏事。即便你的工作不是纯粹的文本处理(比如日志分析),也总会有用到正则表达式的地方(比如查找和修改源代码),所以我希望,这本书能陪伴你练出一身正则表达式的好功夫,在关键场合能亮出趁手的工具。

最后,为了尊重传统教科书的习惯,附上正则表达式的“科班史”:

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

之后,UNIX的主要发明人Ken Thompson将这个符号系统引入了文本编辑器QED(意思是“在文本中搜索某种模式”),正则表达式由此也进入了计算机世界。随后Ken Thompson又将正则表达式引入了UNIX下的文本编辑器ed,ed最终演化为大家熟悉的grep(grep得名自ed编辑器中的正则表达式搜索命令g/re/p,其中的re表示“正则表达式”)。