目录

用 nom 解析文本

nom (文档在这里) 是 Rust 的解析器库,它非常值得新手投资。

如果你必须解析一个已知的数据格式,比如 CSV 或者 JSON,那么最好使用一个专门库,像Rust CSV或者第 4 节讨论的 JSON 库。

同样,对配置文件使用专用的解析器,比如ini要么toml。 (后一个特别酷,因为它与 Serde 框架结合在一起,就像我们看到的serde_json

但是如果文本不规则,或者某种格式,那么你需要扫描那些文本,但不是通过写很多乏味的字符串处理代码。 常建议去看看正则表达式,但认识久后,会感到沮丧,因正则表达式可能并不透明。 nom 提供了一种解析文本的方法。足够强大,大体上讲就是,组合更简单的解析器。 正则表达式有其局限性,例如,使用正则表达式来解析 HTML,不怎么行吧,但是你 可以 使用 nom 解析 HTML。 如果你有兴趣编写自己的编程语言,nom 是一个很好的借鉴,作为领略这条艰难的道路的开端。

有一些用于学习 nom 的优秀教程,但我想从 hello-world 级开始建立一些初步的熟悉感。 您需要了解的基本知识 - 首先,nom 一直是宏,其次,nom 倾向于使用切片 ,而不是字符串。 第一要点,是你必须特别小心才能获得 nom 表达式,因为错误信息不会很友善。 第二要点是 nom 可以用于 任何 数据格式,而不仅仅是文本。 人们使用 nom 解码二进制协议和文件头。它也可以在 UTF-8 以外的编码中,与”文本”合作。

nom 的最新版本与字符串切片工作良好,不过,您需要使用以_s结尾的宏.

#[macro_use]
extern crate nom;

named!(get_greeting<&str,&str>,
    tag_s!("hi")
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done(" there", "hi")

named!宏会创建函数,函数需要一些输入类型(&[u8]默认) 并将第二个类型返回到尖括号中。 tag_s!匹配字符流中的一个字面字符串,其值是表示该文字的字符串切片。(如果你想与&[u8]合作,那用tag!宏。)

我们用一个&str,调用定义的get_greeting解析器,并返回一个IResult。实际上,我们得到了匹配的值。

看看” there” - 这是匹配后剩下的字符串切片。

我们想忽略空格。tag_s!包进ws!,我们就可以在空格,制表符或换行符的任何位置匹配”hi”:

named!(get_greeting<&str,&str>,
    ws!(tag_s!("hi"))
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done("there", "hi")

结果就像之前的”hi”,只不过剩下的字符串是”there”!,空格已被跳过。

很好地匹配了”hi”,尽管这还不是很有用。

让我们匹配”hi”“bye”。 alt!宏 (”备选项”) 采用|符号分割解析器表达式,这样就可以匹配其中的 任何 。 请注意,您可以在这里使用空格来使解析器函数更易于阅读:


# #![allow(unused_variables)]
#fn main() {
named!(get_greeting<&str>,
    ws!(alt!(tag_s!("hi") | tag_s!("bye")))
);
println!("{:?}", get_greeting(" hi "));
println!("{:?}", get_greeting(" bye "));
println!("{:?}", get_greeting("  hola "));
// Done("", "hi")
// Done("", "bye")
// Error(Alt)
#}

最后一场匹配失败了,因为没有其他备选项能匹配”hola”.

显然,我们需要了解IResult类型多一点,但首先让我们比较这与正则表达式的解决方案:


# #![allow(unused_variables)]
#fn main() {
    let greetings = Regex::new(r"\s*(hi|bye)\s*").expect("bad regex"); // (hi|bye) 两种可能性
    let caps = greetings.captures(" hi ").expect("match failed");
    println!("{:?}",caps);
// Captures({0: Some(" hi "), 1: Some("hi")})
#}

正则表达式肯定更 紧凑{compact}! 我们需要将()放在,由|分隔的两种可能性中, 所以我们让 greeting 捕获(captures) 些什么或者没有。第一个结果是整个字符串,第二个是匹配的捕获。 (’|’是正则表达式中所谓的’备选’操作符,这是alt!宏语法的动机名。)

但,这是一个非常简单的正则表达式,它们很快就会变得复杂。因作为一种文本微语言,你必须转义重要的字符如*(。 如果我想匹配”(hi)”或”(bye)”,则正则表达式变为 \s*\((hi|bye)\)\s*,但是 nom 解析器只是变成了alt!(tag_s!("(hi)") | tag_s!("(bye)"))

这也是一个重量级的依赖。在这款相当微弱的 i5 笔记本电脑上,nom 的例子大约需要 0.55 秒的时间编译完成,这并不比”Hello world”慢多少。 但正则表达式的例子大约需要 0.90s。 该 Nom 示例的剥离版本生成的可执行文件约为 0.3Mb (与静态链接的 Rust 程序大约一样),而正则表达式为 0.8Mb。

一个 Nom 解析器返回什么

IResult与标准Result类型有一个有趣的区别 - 有三种可能性:

  • Done- 成功 - 您将得到结果和剩余的字节
  • Error- 未能解析 - 你得到一个错误
  • 不完全{Imcomplete}- 需要更多数据

我们可以写一个dump泛型函数,处理可以调试打印的任何返回值。 也说明下to_result方法会返回一个常规Result- 这可能是大多数情况下,用到的方法,因它不是返回值,就是返回一个错误。

#[macro_use]
extern crate nom;
use nom::IResult;
use std::str::from_utf8;
use std::fmt::Debug;

fn dump<T: Debug>(res: IResult<&str,T>) {
    match res {
      IResult::Done(rest, value) => {println!("Done {:?} {:?}",rest,value)},
      IResult::Error(err) => {println!("Err {:?}",err)},
      IResult::Incomplete(needed) => {println!("Needed {:?}",needed)}
    }
}


fn main() {
    named!(get_greeting<&str,&str>,
        ws!(
            alt!( tag_s!("hi") | tag_s!("bye"))
        )
    );

    dump(get_greeting(" hi "));
    dump(get_greeting(" bye hi"));
    dump(get_greeting("  hola "));

    println!("result {:?}", get_greeting(" bye  ").to_result());
}
// Done Ok("") "hi"
// Done Ok("hi") "bye"
// Err Alt
// result Ok("bye")

解析器返回任何未解析的文本,并且能够表明没有足够的输入字符,对 流{stream} 解析非常有用。常见情况下,to_result会是你的朋友。

合并解析器

让我们继续 greeting(问候) 示例,并设想问候语包含”hi”或”bye”,再加上一个名字。Nom::alpha匹配一系列字母字符。pair!宏将收集两个匹配解析器的结果,作成一个元组:


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str,(&str,&str)>,
        pair!(
            get_greeting,
            nom::alpha
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
// result Ok(("hi", "Bob"))
#}

现在,进一步想象,这个 greeter 有点害羞或不知道其他人的名字: 让我们把名字变成可选。 自然而然,元组的第二个值变成了Option


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str, (&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(nom::alpha)
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
    println!("result {:?}", full_greeting(" bye ?").to_result());
// result Ok(("hi", Some("Bob")))
// result Ok(("bye", None))
#}

留意下,将现有的问候语解析器与,一个名称挑选的解析器合并起来很简单,结果就是名字是可选的。 见识到 nom 的强大力量了吧,这也是它被称为”解析器组合库”的原因。 您可以从更简单的解析器构建复杂的解析器,您可以单独测试它们。 (在这一点上,等价的正则表达式开始看起来像一个 Perl 程序: 正则表达式不能很好地合并。 )

但是,我们还不能葛优瘫! full_greeting(" bye")可能会有Imcomplete错误。 Nom 知道”bye”后面可能会有一个名字,并希望我们给它更多的数据。 解析器工作的时候到了,你可以把文件按块喂它吃,但是在这里我们需要告诉 nom 输入已完成。


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str,(&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(complete!(nom::alpha)) // 已完成
        )
    );

    println!("result {:?}", full_greeting(" bye ").to_result());
// result Ok(("bye", None))
#}

解析数字

nom 提供了一个digit函数,它与一系列数字相匹配。 所以我们使用map!,将字符串转换为整数,并返回完整的Result类型。


# #![allow(unused_variables)]
#fn main() {
use nom::digit;
use std::str::FromStr;
use std::num::ParseIntError;

named!(int8 <&str, Result<i8,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

named!(int32 <&str, Result<i32,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

println!("{:?}", int8("120"));
println!("{:?}", int8("1200"));
println!("{:?}", int8("x120"));
println!("{:?}", int32("1202"));

// Done("", Ok(120))
// Done("", Err(ParseIntError { kind: Overflow }))
// Error(Digit)
// Done("", Ok(1202))
#}

所以我们得到的是,一个解析器的IResult,包含转换而来的Result- 当然,在这里失败的原因不止一种。 请注意,我们的转换函数的主体具有完全相同的代码; 而实际转换取决于函数的返回类型。

整数可能有标志。 我们可以将整数作为一对来捕获,其中的第一个值可能是一个符号,第二个值可能是后面的任何数字。

考虑:


# #![allow(unused_variables)]
#fn main() {
named!(signed_digits<&str, (Option<&str>,&str)>,
    pair!(
        opt!(alt!(tag_s!("+") | tag_s!("-"))),  // 会是一个标志吗?
        digit
    )
);

println!("signed {:?}", signed_digits("4"));
println!("signed {:?}", signed_digits("+12"));
// signed Done("", (None, "4"))
// signed Done("", (Some("+"), "12"))
#}

当我们对中间结果不感兴趣时,只需要所有的匹配输入,那recognize!是你需要的。


# #![allow(unused_variables)]
#fn main() {
named!(maybe_signed_digits<&str,&str>,
    recognize!(signed_digits)
);

println!("signed {:?}", maybe_signed_digits("+12"));
// signed Done("", "+12")
#}

使用这种技术,我们可以识别浮点数。 同样,我们在所有这些匹配项上,将字节切片映射到字符串切片。 tuple!是泛型化的pair!,尽管我们对这里生成的元组不感兴趣。 complete!是需要解决不完整的问候时的相同问题 - “12”是没有可选浮点数部分的有效数字。


# #![allow(unused_variables)]
#fn main() {
named!(floating_point<&str,&str>,
    recognize!(
        tuple!(
            maybe_signed_digits,
            opt!(complete!(pair!(
                tag_s!("."),
                digit
            ))),
            opt!(complete!(pair!(
                alt!(tag_s!("e") | tag_s!("E")),
                maybe_signed_digits
            )))
        )
    )
);
#}

通过定义一个宏小助手,搞一些测试。 如果floating_point匹配它给出的所有字符串,那测试通过。


# #![allow(unused_variables)]
#fn main() {
macro_rules! nom_eq {
    ($p:expr,$e:expr) => (
        assert_eq!($p($e).to_result().unwrap(), $e)
    )
}

nom_eq!(floating_point, "+2343");
nom_eq!(floating_point, "-2343");
nom_eq!(floating_point, "2343");
nom_eq!(floating_point, "2343.23");
nom_eq!(floating_point, "2e20");
nom_eq!(floating_point, "2.0e-6");
#}

(虽然有时候,感觉宏有点 肮脏,但让你的测试漂亮是件好事。)

然后我们可以解析和转换浮点数。在这里,是不顾一切错误的乌托邦式例子:


# #![allow(unused_variables)]
#fn main() {
    named!(float64<f64>,
        map_res!(floating_point, FromStr::from_str)
    );
#}

注意的是,要逐步构建复杂的解析器,那么首先,单独测试每个部分。这是解析器组合器相较于正则表达式的强大优势。这是分而治之的经典编程策略。

多个匹配进行操作

我们与pairs!tuple!见过面了,它将固定数量的匹配捕获项,作成 Rust 元组。

这有many0many1- 他们都捕获无限数量的匹配项,作成 Vec。 不同的是,前一个可能会捕获”零或多个”,后一个则是”一个或多个” (如正则表达式*+之间的差异) 所以many1!(ws!(float64))会解析”1 2 3”到vec![1.0,2.0,3.0],但会在空字符串上会失败。

fold_many0是一个 递算 操作。使用二元运算符将匹配值组合为单个值。例如,这就是 Rust 开发者 以前如何对迭代器进行求和sum加入; 这个fold从一个初始值 (这里是零) 开始,启动 累加器(acc) ,并+迭代器的迭代值v,并返回给acc,继续。

流程就像acc = 0 + v0(第一次),acc = v0 + v1(第二次)...


# #![allow(unused_variables)]
#fn main() {
    let res = [1,2,3].iter().fold(0,|acc,v| acc + v);
    println!("{}",res);
    // 6
#}

以下是 nom 等价物:


# #![allow(unused_variables)]
#fn main() {
    named!(fold_sum<&str,f64>,
        fold_many1!(
            ws!(float64),
            0.0,
            |acc, v| acc + v
        )
    );

    println!("fold {}", fold_sum("1 2 3").to_result().unwrap());
    //fold 6
#}

到目前为止,我们必须捕获每个表达式,或者只是用recognize!抓住所有匹配的字节:


# #![allow(unused_variables)]
#fn main() {
    named!(pointf<(f64,&[u8],f64)>,
        tuple!(
            float64,
            tag_s!(","),
            float64
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
 //got (20, ",", 52.2)
#}

对于更复杂的表达式,捕获所有解析器的结果,会导致相当不整洁的类型!我们可以做得更好.

do_parse!让你只提取你感兴趣的值- 感兴趣的匹配项用>>分割: 格式是 name:parser}。最后,括号里有一个代码块。


# #![allow(unused_variables)]
#fn main() {
    #[derive(Debug)]
    struct Point {
        x: f64,
        y: f64
    }

    named!(pointf<Point>,
        do_parse!(
            first: float64 >>
            tag_s!(",") >>
            second: float64
            >>
            (Point{x: first, y: second}) // first  second 是 临时值
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
// got Point { x: 20, y: 52.2 }
#}

对标签值 (就是那个逗号)不感兴趣,但我们将两个浮点值分配给用于构建结构的临时值。最后的代码可以是任何 Rust 表达式,随你。

解析算术表达式

随着必要的知识建立,我们可以做简单的算术表达式。

这是用正则表达式,无法真正完成的一个很好的例子。

这个想法是从下往上建立表达式。表达式由 terms 组成,如加或减。 terms 由 factors 组成,它们相乘或除。 和(现在)factor 只是浮点数:


# #![allow(unused_variables)]
#fn main() {
    named!(factor<f64>,
        ws!(float64)
    );

    named!(term<&str,f64>, do_parse!(
        init: factor >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("*") | tag_s!("/")),
                factor
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "*" {acc * v.1} else {acc / v.1}
            }
        )
        >> (res)
    ));

    named!(expr<&str,f64>, do_parse!(
        init: term >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("+") | tag_s!("-")),
                term
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "+" {acc + v.1} else {acc - v.1}
            }
        )
        >> (res)
    ));
#}

这更准确地表达了我们的定义 - 一个表达式至少包含一个 term,和零或多个加减项。

我们不collect它们,而用适当的 fold 操作。 (这是 Rust 不能很好地处理表达式类型的情况之一,所以我们需要一个类型提示。) 这样做会建立正确的 运算符优先级 -*总是比+优先等等。我们在这里需要浮点数断言,而正好有一个箱.

approx ="0.1.1",添加到您的 Cargo.toml 中,就可以开始了:


# #![allow(unused_variables)]
#fn main() {
#[macro_use]
extern crate approx;
...
    assert_relative_eq!(fold_sum("1 2 3").to_result().unwrap(), 6.0);
#}

我们来定义一个方便的小宏,来测试。stringify!将表达式转换为,我们可以输入expr的字符串字面量, 然后将测试得到的值,与 Rust 的执行结果进行比较。


# #![allow(unused_variables)]
#fn main() {
    macro_rules! expr_eq {
        ($e:expr) => (assert_relative_eq!(
            expr(stringify!($e).to_result().unwrap(),
            $e)
        )
    }

    expr_eq!(2.3);
    expr_eq!(2.0 + 3.0 - 4.0);
    expr_eq!(2.0*3.0 - 4.0);
#}

这非常酷 - 只需几行即可获得 表达式评估器! 但它能变得更好。 我们在因素{factor}解析器,增加了一个数字的替代方案 - 能包含在括号内的表达式:


# #![allow(unused_variables)]
#fn main() {
    named!(factor<&str,f64>,
        alt!(
            ws!(float64) |
            ws!(delimited!( tag_s!("("), expr, tag_s!(")") ))
        )
    );

    expr_eq!(2.2*(1.1 + 4.5)/3.4);
    expr_eq!((1.0 + 2.0)*(3.0 + 4.0*(5.0 + 6.0)));
#}

最厉害的是现在,能 递归 定义这堆表达式!

delimited!的特别魔力,在于括号可以嵌套 - nom 确保括号匹配。

我们现在已经拥有,超越正则表达式的能力了,0.5Mb 的剥离版可执行文件仍然是”hello world”正则表达式程序的一半大小。