rust nom 一个文本解析器的使用

简述

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解析器。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 Rust 编写一个 3D 查看,可以使用以下步骤: 1. 确定所需的3D引擎库。Rust 中有多个3D引擎库可供选择,例如 wgpu、glium、piston等。在这里,我们以 wgpu 为例。 2. 创建一个 Rust 项目,并使用 Cargo 管理依赖。在项目中添加 wgpu 依赖。 3. 编写代码来加载 3D 模型。这可以通过使用第三方库,例如 assimp-rs 来实现。 4. 编写代码来渲染 3D 模型。可以使用 wgpu 提供的 API 来实现。 以下是一个简单的示例代码,该代码使用 wgpu 和 assimp-rs 实现了一个简单的 3D 模型查看: ```rust use std::path::Path; use wgpu::{BackendBit, Device, Instance, Queue, Surface}; use assimp::import::Importer; use assimp::scene::Scene; async fn run() { // 初始化 wgpu let instance = Instance::new(BackendBit::PRIMARY); let surface = unsafe { instance.create_surface(window) }; let adapter = instance .request_adapter(&RequestAdapterOptions { power_preference: PowerPreference::Default, compatible_surface: Some(&surface), }) .await .unwrap(); let (device, queue) = adapter .request_device( &DeviceDescriptor { features: Default::default(), limits: Default::default(), shader_validation: true, }, None, ) .await .unwrap(); // 加载 3D 模型 let importer = Importer::new(); let path = Path::new("path/to/your/model"); let scene = importer.read_file(path).unwrap(); // 渲染 3D 模型 // ... } fn main() { env_logger::init(); // 创建事件循环 let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); let mut state = AppState::new(&window); // 运行事件循环 let event_loop_proxy = event_loop.create_proxy(); let event_loop_runner = event_loop.run(event_loop_proxy, &mut state); smol::block_on(run()); } ``` 这段代码中,我们首先初始化了 wgpu,并加载了一个 3D 模型。然后,我们可以使用 wgpu 提供的 API 来渲染 3D 模型,例如创建顶点缓冲区、着色程序、渲染管道等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值