PEG实践续集

上一篇博客里,我向大家介绍了PEG。不过也许我并没有给大家实际常用的那种例子。而且可能上一篇博客里写太多专业词汇了,了解相关知识的可能看得有点意思;不了解的,可能也就是点赞666了。所以我想在这篇中再给大家两个例子。第一个例子就是验证中国大陆的身份证号是否符合规则。根据我的经验大家或许都用某些方式匹配过。

在所有大家的选项中,可能最常用的匹配方式,就是正则表达式。为了文章的完整性,我在这里写一下18位身份证号的正则表达式:

正则表达式有一个特点,写比读容易。当然对于没有掌握正则表达式这项技能的人来说,可能二者都不太容易。而从网上复制粘贴往往就是应付这类工作的办法。网上显得越牛的人写出的正则,一般会认为更靠谱些。不过这终究不如自己看懂了再用要放心。

这个正则会匹配

身份证号。这样一个简单无比的逻辑被写成正则表达式以后需要占用81个字符的长度。当然我不是说它长,它不算长,就是有点信息密集。或许你曾听说过这个世界上有一小部分编程语言是不需要token之间加上空格的,而正则表达式,是一种你想加空格都不许加的。

不过大家应该知道,上面看到这个正则因为其逻辑相当简单,所以在匹配身份证的时候,匹配规则并不严格。比如

这种一看就不是真实身份证,而且连完全不懂编程的一般人都可以随便写出的虚拟身份证号都可以轻松过关,这在有些应用场景里,可能就不能满足用户的需求了。

在实际中,我的同事就使用过超过100行的代码较为严格地校验身份证号。但如果用PEG的话,也许既不失正则的简洁性,同时还具有较好的扩展性。下面就来看看我的代码:

说句心里话,我其实一直期待一种模式匹配,能够用一会儿正则,用一会儿函数,该回溯的时候还能回溯。PEG目前恰好能满足我的期待。

在上面的代码中,可以容易看出,idcard文法的前17位进行了一个捕获,最后1位进行了另一个捕获,然后用这两个捕获做了一个匹配期间捕获(match-time capture),传到parity函数里判断捕获是否真正成功。而parity函数也就是大家熟知的用身份证前17位计算最后一位,并与实际最后一位进行比较的函数。

LPeg相比于正则表达式,就好比专业排版工具(如Adobe InDesign)相比于Microsoft Word。我的意思是,用前者匹配字符串其实是有点大材小用,但它在匹配复杂规则的时候的方便性是不言而喻的。


第一个例子算是一个热身。第二个例子是我最近接触到的一个有趣的task,就是解析中文数字。最近在程序员圈子里有一个repo引起了大家的广泛关注,它就是新生的文言编程语言。中文数字解析就是其中一个组成部分,而且看似简单,实则并不简单。我在这个例子里也没有实现完全的规则判定,而且仅限整数。

使用num这个文法如果匹配成功,就会返回中文数字对应的阿拉伯数字。这段程序唯一有点特别的地方,就是用了层叠捕获(fold capture),其实也就是大家熟悉的函数式编程里的归纳(reduce)。在LPeg里用~>这个符号来表示(前面是波浪线)。

中国数字如果只是简单匹配并不难,但要仔细匹配,抠细节,却不容易。比如“零”的占位,比如百不能放在千的前面,再比如万可以放在亿前面。一些复杂的规则加进来,就需要更精细的匹配。

在千百十个四位我每个都写了条规则,为的就是让small_num的规则尽量严格。小单位就是不能放在大单位前面,零不能多,但可以少:

到了万以上,就要用一套复合规则,也把一个small_num加上一个大单位(万、亿等)作为一个digit_group来匹配。一个group内其实就是做一个乘法运算,但不完全是这样,例如“億萬”要解释成一亿零一万。但这个“一”的省略很微妙,因为“萬億”要解释成一兆,也就是1012,并不会在后面的亿那里加上一个省略的“一”。所以我把省略“一”这个位置直接捕获成一个空字符串,都交给tmul这个函数处理。tmul不是一个简单的相乘,它会乘完把结果、两个乘数都记录在一个table里。

而多个table之间的相加,则使用了tadd这个函数谨慎地进行相加,同时保留相乘之后单位的信息,进行reduce操作。“谨慎”之处就在于要判断二者的单位,前者单位小,是;前者单位大,是。另外有一个小点需要注意,因为所有的相加之后还会是一个table,所以我后面用函数捕获elem1获取结果,然后再跟其他一般的数进行简单add。


通过这两个例子,相信大家对PEG的认识应该更近了一层吧。多样的捕获也是LPeg的一个特色,如果只支持简单捕获的话,想实现复杂的功能可就费劲了。上面两端示例代码也收录在我最近才建立的一个repo里:AlexanderMisel/lpeg_patterns。欢迎大家分享交流。