解析文本数据

数据解析是与反序列化密切相差的问题。考虑解析的最常见方式是从正则语法开始并基于此构造解析器。这将导致一个自下而上的解析器,较小的规则解析整个输入的较小组件。 最终的组合规则将给定顺序中的所有较小规则组合在一起形成最终解析器。这种正式定义有限规则集的方式称为 解析表达语法 Parsing Expression Grammar(PEG)。 这确保解析是明确的;如果解析成功,则只有一个有效的解析树,在 Rust 生态系统中,有几种不同的方式可以实现 PEG 。 每种方式都有优点和缺点。第一种方法是使用宏来定义于域的语言以进行解析。

该方法通过新的宏系统与编译器很好地集成,并且可以生成快速代码。 但是,这通常更难以调试和维护。由于此方法不允许重载运算符,因此实现必须定义 DSL ,这可能更多地是学习者的认知负载。 第二种方法是使用 特质系统 。此方法有助于定义自定义运算符,并且通常更易于调试和维护。使用第一种方法的解析器的示例是 nom;使用第二种方法的解析器的例子是 pom 和 pest 。

我们用于解析的用例主要在网络应用程序的上下文中。在这些情况下,有时处理原始字符串(或字节流)并解析所需信息而不是反序列化为复杂数据结构更有用。 常见的情况是任何基于文本的协议,例如 HTTP 。服务器可能会在套接字上接收原始请求作为字节流,并对其进行解析以提取信息。在本节中,我们将研究 Rust 生态系统中的一些常见解析技术。

现在,nom是一个解析器组合框架,这意味着它可以组合较小的解析器来构建更强大的解析器。 这是一种自下而上的方法,通常从编写非常特定的解析器开始,该解析器从输入中解析定义良好的东西。然后,框架提供了将这些小解析器链接到完整的解析器的方法。 这种方法与 lexyacc 情况下的自上而下方法形成对比,其中一种方法可以从定义语法开始。 它可以处理字节流(二进制数据)或字符串,并提供 Rust 的所有常规保证。 让我们从解析一个简单的字符串开始,在这种情况下是一个 HTTP GET 或 POST 请求。像所有货物项目一样,我们将首先建立结构:

$ cargo new --bin nom-http

然后添加依赖项(nom) :

$ cat Cargo.toml 
[package] 
name = "nom-http" 
version = "0.1.0" 
authors = ["Foo<foo@bar.com>"]

[dependencies.nom] 
version = "3.2.1" 
features = ["verbose-errors"]

crate 提供了一些通常对调试有用的额外功能;默认情况下禁用它们,可以通过将列表传递给 features 标志来打开它们,如上例所示。现在,让我们转到我们的主文件:

// chapter4/nom-http/src/main.rs

#[macro_use]
extern crate nom;

use nom::{ErrorKind, IResult};
use std::str;

#[derive(Debug)]
enum Method {
    GET,

    POST,
}

#[derive(Debug)]
struct Request {
    method: Method,

    url: String,

    version: String,
}

// A parser that parses method out of a HTT request
named!(parse_method<&[u8], Method>, return_error!(ErrorKind::Custom(12), alt!(map!(tag!("GET"), |_| Method::GET) | map!(tag!("POST"), |_| Method::POST))));

// A parser that parses the request part
named!(parse_request<&[u8], Request>, ws!(do_parse!( method: parse_method >> url: map_res!(take_until!(" "), str::from_utf8) >> tag!("HTTP/") >> version: map_res!(take_until!("\r"), str::from_utf8) >> (Request { method: method, url: url.into(), version: version.into() }) )));

// Driver function for running the overall parser
fn run_parser(input: &str) {
    match parse_request(input.as_bytes()) {
        IResult::Done(rest, value) => println!("Rest: {:?} Value: {:?}", rest, value),
        IResult::Error(err) => println!("{:?}", err),
        IResult::Incomplete(needed) => println!("{:?}", needed),
    }
}

fn main() {
    let get = "GET /home/ HTTP/1.1\r\n";
    run_parser(get);
    let post = "POST /update/ HTTP/1.1\r\n";
    run_parser(post);
    let wrong = "WRONG /wrong/ HTTP/1.1\r\n";
    run_parser(wrong);
}

很明显, nom 使用了大量的宏来生成代码,最重要的是 named! ,它接受一个函数签名并定义一个基于它的解析器, nom 解析器返回 IResult 类型的实例,它是枚举类型,并有三种变体:

  • Done(rest, value) 表示当前解析器成功的情况,在这种情况下,该值具有当前解析的值,其余的将具有要解析的剩余输入。
  • Error(Err(E)) 表示解析期间的错误。基础错误将包含错误代码,错误位置等,在一个大型的解析树中,这也可以保存指向更多错误的指针。
  • Incomplete(needed) 表示由于某种原因解析不完整的情况。需要的是一个枚举,它又有两个变体;第一个代表不知道需要多少数据的情况。第二个代码所需数据的大小。

我们从 HTTP 方法表示和结构的完整请求开始。在我们的示例中,我们只处理 GET 和 POST 。并忽略其他的所有内容。然后我们为HTTP方法定义一个解析器;我们的解析器将接受一片字节并返回Method枚举。这可以通过读取输入并查找字符串GET或POST来完成。 在每种情况下,基本解析器都是使用 tag! 构造,它解析输入以提取给定的字符串。 而且,如果解析成功的话,我们可以使用 map! 将结果转换为 Method ,将分析器的结果映射到函数。现在,对于解析方法,我们要么有一个post,要么有一个get,但决不能两者都有。我们使用 alt! 宏来表示前面构造的两个分析器的逻辑 或。 alt! 如果宏的任何一个组成宏可以解析给定的输入,那么宏将构造一个解析器来解析输入。最后,所有这些都包含在 return_error ,如果分析在当前分析器中失败,它会提前返回,而不是传递到树中的下一个分析器。

然后,我们通过定义 parse_request 来解析整个请求。我们从使用 ws! 从输入中删除多余的空白。 然后我们调用 do_parse! 链接多个子分析器。这一个不同于其他组合器,因为它允许存储中间分析器的结果。这对于在返回结果时构造结构的实例很有用。 在 do_parse! 我们首先调用 parse_method 并将其结果存储在一个变量中。在从请求中删除了方法之后,我们应该在找到对象的位置之前遇到空白。这是由 take-until!(" ) 处理的,它使用输入直到找到空白空间。使用 map-res! 将结果转换为 str 。 列表中的下一个分析器是使用标记移除序列 HTTPtag! 。接下来,我们通过读取输入来解析 HTTP 版本,直到看到一个 \r ,并将其映射回一个 str 。完成所有解析后,我们构造一个请求对象并返回它。 注意在序列中使用 >> 作为解析器之间的分隔符。

我们还定义了一个名为 run_parser 的助手函数,在给定的输入中运行解析器并打印结果。此函数调用解析器并对结果进行匹配,以显示结果结构或错误。 然后,我们用三个 HTTP 请求定义我们的主函数,前两个是有效的,最后一个是无效的,因为方法是错误的。运行此命令时,输出如下:


# #![allow(unused_variables)]
#fn main() {
$ cargo run 
    Compiling nom-http v0.1.0 (file:///Users/Abhishek/Desktop/rustbook/src/ch4/nom-http)
     Finished dev [unoptimized + debuginfo] target(s) in 0.60 secs
      Running `target/debug/nom-http` 
Rest: [] Value: Request { method: GET, url: "/home/", version: "1.1" } 
Rest: [] Value: Request { method: POST, url: "/update/", version: "1.1" } 
NodePosition(Custom(128), [87, 82, 79, 78, 71, 32, 47, 119, 114, 111, 110, 103, 47, 32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10], [Position(Alt, [87, 82, 79, 78, 71, 32, 47, 119, 114, 111, 110, 103, 47, 32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10])])
#}

在前两种情况下,所有内容都按预期进行了解析,然后我们得到了结果。正如预期的那样,在最后一种情况下解析失败并返回自定义错误

正如我们之前讨论的那样,nom 的一个常见问题是调试,因为调试宏要困难得多。 宏也鼓励使用特定的 DSL(比如使用>>分隔符),有些人可能会觉得难以使用。在撰写本文时,来自 nom 的一些错误消息对于找出给定解析器的错误是没有帮助的。 这些肯定会在未来有所改进,但与此同时,nom 提供了一些辅助宏来进行调试。

例如,dbg! 如果底层解析器未返回 Done,则打印结果和输入。 dbg_dump! 宏是类似的,但也打印出输入缓冲区的十六进制转储。 根据我们的经验,可以使用一些技术进行调试:

  • 通过将编译器选项传递给 rustc 来扩展宏。 Cargo 使用以下调用启用此功能:cargo rustc -- -Z unstable-options -pretty=expanded 扩展并漂亮打印给定项目中的所有宏。有人可能会发现扩展宏以跟踪执行和调试很有用。 Cargo中的相关命令, rustc -- -Z trace-macros 仅扩展宏。
  • 独立运行较小的解析器。给定一系列解析器和另一个解析器,可能更容易运行每个子解析器,直到其中一个错误出来。然后,可以继续调试失败的小解析器。这在隔离故障时非常有用。
  • 使用提供的调试宏 dbg!dbg_dump!。这些可以像调试打印语句一样用于跟踪执行。

pretty=expanded 现在是一个不稳定的编译器选项。在将来的某个时候,它将被稳定(或移除)。在这种情况下,不需要传递 -Z unstable-options 标志来使用它。

让我们看一个名为 pom 的另一个解析器组合子的例子。正如我们之前讨论的那样,这个很大程度上依赖于 traits 和 operator-overloading 来实现解析器组合。在撰写本文时,当前版本为 1.1.0 ,我们将在示例项目中使用它。 像往常一样,第一步是设置我们的项目并将pom添加到我们的依赖项:

$ cargo new --bin pom-string

Cargo.toml 如下:

[package] 
name = "pom-string" 
version = "0.1.0" 
authors = ["Foo<foo@bar.com>"]

[dependencies] 
pom = "1.1.0"

在此示例中,我们将解析示例HTTP请求,就像上次一样。这是它的样子: