nom – 乐高式富有语义的parser
简介
写过parser的人,不管是简单的自定义协议,或者复杂的协议,一般都是采用自上往下的解释方式,从第1个字节,一路开黑,到最后字节。遇到;
用一个判断,遇到:
用一个match
等等,switch
相应的case
,所谓遇神拜神,遇鬼杀鬼,遇佛却不知所措。这样的问题是,加上错误处理,if else
可能会过于复杂而凌乱,时间久了,难以维护。稍微高端点的,可能会写出几个复杂一点的正则表达式,不过最后也很有可能,最终忘记当初写这正则的含义。高端玩家估计就用lex/yacc flex/bison
,的确好用又好维护,除了增加一下描述文件,增加一些与开发语言无关的东西。不过杀鸡焉用牛刀,这么庞大的工具,有必要割本来就小的小JJ?!
nom提供一种比较清奇的思路,估计作者深受乐高的熏陶,才有这种创造。我们知道,乐高玩具,提供的是很限的几个基础形状模块,通过一个个模块的组合起来,就可以实现各种物件的创作,栩栩如生。nom的思路并不是教你如何像lex/yacc
构造特定语法的模板文件,也不期待你自顶而下解释,而是像乐高一样,提供很多基础的基础形状,如tag, take_while1, is_a
等,并提供组合一起的方法,如alt, tupple, preceded
等。用各种方法可以组合,形成各式各样的parser,而又不损失任何性能。
nom函数的设计高度一致,几乎所有函数的调用,都返回带相同签名的函数:
|
|
nom的基础函数构造块,分两个版本,一个是complete
和stream
版本,两个直观上最大的区别是报错方式,stream
版本报错为Err::Incomplete(n)
,complete
直接报Err::Error/Err::Failure
。除此之后,使用上并没有明显的差异。
nom的parser和combinator应有尽有,没有的也可以组合出来。除了docs.rs的文档,作者还贴心的列出来(因为rust生成的文档不方便一起看),这里不累赘,参考作者介绍choosing_a_combinator。
示例
tokio的官方教程tutorial是学习rust很好的一个开始的地方,把之前C/C++
已有的概念用rust实现了一遍,既亲切又熟悉。tokio的官方教程tutorial是实现简单redis功能,代码仓库位于: mini-redis。 mini-redis对于redis协议的解析是通过一个字节解析的,幸好简单,所以原实现并不凌乱。现修改为用nom解析方式,代码仓库位于: https://github.com/buf1024/buf1024.github.io/tree/master/.demo/rust-lib/mini-redis,协议解析的文件:frame.rs
|
|
mini-redis简单实现了redis的5个基本协议,实现并不复杂,拼拼凑凑就成了,魔改的nom的版本如下:
|
|
可以看到nom的版本有很简洁的方式,甚至一行代码即可实现,不需要原代码那样,一个字节一个字节的判断,来操控和移动内存位置。就类似乐高积木一样,一个一个的叠起来,简单又简洁。
测试:
|
|
小结
rust的nom提供一种类似于乐高积木的方式去构造解析器,通过组合各种基本的parser,完全可以构造出足够复杂的解析器,同时不会因为组合各种parser,而失去任何性能。同时,得益于其命名方式和统一的函数返回形式,基于nom的解析器,语义上比那些get_line
之类的,清晰太多。所以在解析任务上,除了有现成而又成熟的解析库,完全可以考虑采用的,而非get_line,get_u8
那样一个字节的或者正则那样一个模式的,开黑下去……