简述
nom, 是 一个Rust 的文本解析器库,它的运行消耗低、速度快,针对一些文本的解析非常的方便,是一个值得学习新手的库。
nom与正则表达式最大的不同在于,正则是通过表达式在内部将文本解析完成后返回给使用者,对于使用者完全不知道它的中间运行过程,更无法介入其运行过程中,所以在一些非常复杂的文本解析时,正则表达式则表现的有些不尽如人意。
nom相对于正则更倾向于以文本切片的方式进行处理。它提供了大量的解析器、组合器,至于怎么使用就看程序员们的自由发挥了。
我们在这里比较nom与regex,并不是要证明哪一个更好更厉害,两者并不是对立的,而是相辅相成的,使用nom做不规则的文本解析,取出其中有规律的文本,再使用regex进行匹配,开发与运行效率事半功倍。
优先说明,解析器、组合器中使用的是泛型,也就是只要是符合泛型规则的类型都可以被解析器、组合器进行处理。但这里为了描述和理解的方便,统一使用&str
版本
编写此博客时使用的是nom 5.0版本,后面版本可能有部分功能的升级,但整体的思路应该不会变的。
当然如果后面出现了较大的变化,使此博客的代码无法运行,或者整体的思路发生了变化,请告诉我,我会重新学习,更新此博客。
Cargo.toml
[dependencies]
nom = "5.0"
早期版本
在早期版本中大量使用了宏,为了便于理解,举一个例子
例子的需求:我有一个字符串,若这个字符串以abc开头,则把abc去掉,把后面的字符串给我,如果不是以abc开头,则把整个字符串给我就行了。
为了解决这个需求,使用nom是需要使用到一个宏tag!
tag!(s: T) -> IResult<(T, T), E>
IResult中的第一个T: 匹配完成后剩余的字符
IResult中的第二个T: 匹配的字符
在最新的版本中宏照常可以使用的,只是宏的内部实现进行了全部的更新。
因为需要使用到宏,所以还是需要加上宏的启用标识
#[macro_use]
extern crate nom;
例:
named!(parser<&str, &str>,
tag!("abc")
);
#[test]
fn test() {
let s = "abcdef";
let s_expected = "abc";
let remain_expected = "def";
let r = parser(s);
assert_eq!(Ok((remain_expected, s_expected)), r);
}
此处用named!宏创建了一个parser函数,泛型中有两个参数 <T, O>,T为第一个&str,O为第二个&str
传入的参数类型: T
输出的类型: Result<(T, O), E>,E为nom的错误类型,这个后面会再说到
所以针对上述的需求我们只需要取返回的T即可。
通过上述的例子,觉得实现还是比较简单明了。但是如果遇到一个复杂的需求,需要的解析器、组合器增多,整个代码的阅读性就会降低,而且还会暴露一个很大的问题:错误提示机制不准确,这里说的错误是运行时报的错误,不是编译阶段的错误。往往编写完成一个解析方法后,一运行,出现错误,但根据错误的提示,无法找到对应的错误地方,因为使用了大量的宏,无法进行debug,所以整个的排错过程将非常的痛苦。
新的改变
针对早期版本的这些缺点,进行了一次非常大规模的重构,整个代码的架构基本上重新翻写了一遍,但熟悉早期版本的程序员依然还是可以按照早期版本的方法去编写。不得不说这点做的很赞!
新的版本中,相应的解析器、组合器不再是宏,而且转换为函数实现,这样整个代码将更加清晰,最主要的是可以进行。
尽管其错误机制依然没有很大的改进,但因为每一步的运算结果我们都可以拿得到了,那么我们可以进行自己的错误处理,所以之前的错误提示不清晰的问题也不是什么大问题了。
还是拿早期版本的那个字符串的例子还简单的写一下
use nom::IResult;
#[test]
fn test2() {
let s = "abcdef";
let r: IResult<&str, &str> = nom::bytes::streaming::tag("abc")(s);
let s_expected = "abc";
let remain_expected = "def";
assert_eq!(Ok((remain_expected, s_expected)), r);
}
常用的几个解析器、组合器
在新的版本中每个的解析器、组合器都有一到多个的实现,拿tag来说,就有
nom::bytes::complete::tag
nom::bytes::streaming::tag
两种实现。
一般complete模块下在字符串结束的时候会返回Err::Error的错误
stream模块下返回的是Err::Incomplete错误
最直接的影响是,如果判断到了字符串结尾,而你的组合器使用的是opt many这种执行子解析器的组合器,如果子解析器使用的是stream模块下的,那么当处理到字符串结束的时候,opt mang类型的组合器依然会返回错误,原因就是这种类型的组合器,只有在子解析器返回Err::Error错误时才会继续进行组合器的自己的逻辑处理。
在这里列举几个最常用的,不过不再写出在哪个模块下了,还有很多解析器、组合器在这里就不一一的赘述,具体可以去这里查看文档
tag(s): 以s开头进行拆分
char(c): 以c字符开头进行拆分,与tag类似,只是char传入的是单个的字符
space0: 以0~n个空格、\t开头的进行拆分,效果与tag类似,只是这里固定为空格或\t
sapce1: 以1~n个空格、\t开头的进行拆分,这里需要最少一个空格或\t
alt: 组合解析器,nom中的if语句
如: alt((space1, char('a'), tag("bcd")))(input),意思为:切分input,先匹配space1,不匹配则匹配char('a'),以此类推,直到遇到第一个匹配的,都不匹配则抛出ParserError
opt: 如果运算不成功,则返回None。
如: opt(tag("abc"))(input) 如果input="bcde",返回值则为Ok(("bcde", None)),不会返回Err错误了
many0: 返回一个Vec,它会将上一轮处理后的数据剩余的字符串在进行一边处理,结果合入Vec中。
如: many(tag("abc"))(input)如果input="abcabcdef",返回值则为Ok(("def", ["abc", "abc"]))
take_while(jud): jud是一个返回值为bool的表达式,从头还是匹配输入的值,直到表达式返回false为止①
escaped: 判断转义字符。需要三个参数,第一个是常规过滤,第二个是控制字符,第三个是会被转义的字符
如:escaped(digit1, '\\', one_of("\"n\\"))(input) 如果input="1234\\\\abc" 返回结果为Ok(("abc", "1234\\\\"))
因为语言中的字符串\本身就需要转义,所以input实际输入1234\\
在escaped中第二个转义控制符是\,那么在第一次碰到\时就会暂存,如果接下来的字符是需要匹配的 " n \ 中的随意一个,那么就算通过继续往下匹配。②
①
针对take_while的描述可能不太清楚,在这里举一个例子
例子的需求:我这里有一个字符串,从左边开始验证,直到遇到第一个不是数字的字符为止。
针对这个需求,我们需要先写一个函数,接收char,判断这个char是否为数字,然后使用take_while调用此函数即可
use nom::character::is_digit;
use nom::bytes::streaming::take_while;
use nom::IResult;
fn jud_digit(chr: char) -> bool {
is_digit(chr as u8)
}
#[test]
fn test_split_digit() {
let s = "123abc";
let r: IResult<&str, &str> = take_while(jud_digit)(s);
let remain_expected = "abc";
let digit_expected = "123";
assert_eq!(Ok((remain_expected, digit_expected)), r);
}
针对jud_digit这个函数多说两句,入参为char,并不代表take_while中的表达式都是入参为char
take_while中的表达式泛型约束为
Fn(<Input as InputTakeAtPosition>::Item) -> bool
查看trait InputTakeAtPosition可知,默认对两种类型进行了实现
impl<'a> InputTakeAtPosition for &'a [u8] {
type Item = u8
}
impl<'a> InputTakeAtPosition for &'a str {
type Item = char
}
所以只是因为我们的入参为&str,所以才使用char而已,自己使用是根据自己的情况判别即可
②
注意: 在escaped例子中,使用的是digit1,而不能使用digit0.
因为查看escaped的源码
use crate::traits::AsChar;
move |input: Input| {
let mut i = input.clone();
while i.input_len() > 0 {
match normal(i.clone()) {
Ok((i2, _)) => {
// 完全匹配的逻辑
}
Err(Err::Error(_)) => {
// 匹配抛出Err::Error错误时的逻辑
// 此处的逻辑是开始匹配控制符和需要转义的字符
}
Err(e) => {
return Err(e);
}
}
}
Ok((input.slice(input.input_len()..), input))
}
可以看出只有当第一个传入的函数抛出Err::Error错误时,escaped才会进行转义的匹配,而digit0与digit1最大的区别就是digit0在不匹配时不会抛出错误,所以当使用digit0后,整个程序会进入死循环。
后续
本篇讲了nom一些最基本的用法,还有常用的解析器、组合器。在下一篇将使用实现一个简单的sql解析器。