1. 程式人生 > 其它 >從零編寫一個解析器(1)—— 解析數字

從零編寫一個解析器(1)—— 解析數字

長久以來,由於我在工作中使用 go 語言,所以時常會遇到需要將 sql 轉換為 struct 的需求,雖然在網上能夠找到一些將 sql、json 等轉換為 struct 的工具,但大都無法配置,要麼只支援將 json 轉 struct,要麼轉換後 tag 的風格不符合我所需要的。
基於這種情況,我一直想自己寫一套可自定義的轉換工具,要想能靈活的轉換,先需要對原始碼字串(sql 或者 json)進行解析,因此,我們從這裡開始,逐步學習如何實現一個解析器,最終的目標是可以靈活的將 sql、json 轉換為 go struct。

nom是 Rust 中一個強大的解析器庫,而我們就是要基於 nom 對源字串進行解析。

本文的前半部分深度參考 nom 倉庫中的一個文件

萬丈高樓平地起,要想用 nom 寫好一個解析器,我們先要對 nom 進行一些瞭解,因此先從一些小示例開始,主要是通過一些 nom 自帶的函式來實現簡單的解析。

第一次解析

根據 文件 中的介紹,我們先從解析一個括號中的數字 —— (12345) 開始。先定義一個函式簽名,它用於把字串 (12345) 解析成數字:

fn parse_u32(input: &[u8]) -> IResult<&[u8], u32>

parse_u32 是函式名稱,它接收一個 input 引數,IResult

nom::IResult,是 nom 中常用的結果返回型別。可以通過文件檢視其宣告和註釋:

/// Holds the result of parsing functions
///
/// It depends on the input type `I`, the output type `O`, and the error type `E`
/// (by default `(I, nom::ErrorKind)`)
///
/// The `Ok` side is a pair containing the remainder of the input (the part of the data that
/// was not parsed) and the produced value. The `Err` side contains an instance of `nom::Err`.
///
/// Outside of the parsing code, you can use the [Finish::finish] method to convert
/// it to a more common result type
pub type IResult<I, O, E = error::Error<I>> = Result<(I, O), Err<E>>;

它的型別由輸入、輸出的型別和錯誤型別而定,在返回 Ok 時,它包含了輸入的剩餘部分以及解析結果;在返回 Err 時,它包含的是 nom::Err 型別例項。

基於 nom 的解析器,大都是自下而上構建的,先編寫最小的解析單元,然後使用組合子將它們組合到更復雜的解析器中。
nom 中已經提供了很多的基礎的解析單元。利用這些解析單元,我們可以做兩種選擇:

  • 1.解析特定的內容
  • 2.組合更上層的解析器

圍繞這兩點,我們可以先開始嘗試 —— 解析 (12345)

很明顯,我們無法直接用基礎的解析器直接解析出 (12345) 中的數字部分,因為基礎解析器解析的內容是比較單調的,比如可以用來解析 aaa97900 等這類比較由規律的單元。

既然無法直接解析 (12345),我們就需要手動組合這些基礎解析器。基礎的解析器大都位於 nom::*::complete 下,比如 nom::bytes::complete::tag

(12345) 由一個左小括號開始,緊跟著一批數字字元然後是右小括號結束。據此我們可以將其拆分為:

  • (
  • 12345
  • )

因此實現返回解析 ( 的解析器的可以選擇 nom::bytes::complete::tag
它的函式簽名是:

pub fn tag<T, Input, Error: ParseError<Input>>(
    tag: T
) -> impl Fn(Input) -> IResult<Input, Input, Error>
where
    Input: InputTake + Compare<T>,
    T: InputLength + Clone,

可以看到該函式的返回值是 impl Fn(Input) -> IResult<Input, Input, Error> —— 即一個閉包。該閉包可以解析源字串中特定的字串。
比如你像解析 ( 開頭的字串(如 (123)),則可以寫成 tag("(");如果你想解析 ## 開頭的字串(如 ## someMdTitle),則可以寫成:tag("##")
用一個單元測試試試:

fn test_tag1() {
    // part 1
    fn my_parser1(s: &str) -> IResult<&str, &str> {
        tag("(")(s)
    }
    let res = my_parser1("(123)");
    assert_eq!(res, Ok(("123)", "(")));
    // part 2
    fn my_parser2(s: &str) -> IResult<&str, &str> {
        tag("##")(s)
    }
    assert_eq!(my_parser2("## someMdTitle"), Ok((" someMdTitle", "##")));
}

單元測試中的第 1 部分中,聲明瞭一個解析 ( 的解析器,然後呼叫解析器解析字串:my_parser1("(123)");
然後斷言,返回 Ok(),其中包含的值是一個元組,第 0 個元素是解析完剩餘的字串 "123)",第 1 個值是解析結果 (

單元測試中的第 2 部分中,聲明瞭一個解析 ## 的解析器,然後呼叫該解析器解析字串:my_parser2("## someMdTitle"),並斷言其返回值
返回 Ok(),其中包含的值是一個元組,第 0 個元素是解析完剩餘的字串 someMdTitle,第 1 個值是解析結果 ##

好了,雖然有點初級,但至少我們起步了!

加速

雖然我們可以通過簡單的解析器解析出需要的字串,但我們的目標可不是單純地解析出 ( 或者 ## 之類地單調字元,我們的首要目標是解析出字串((12345))中括號中的數字!

此時,我們就需要用到組合子。通過組合子對不同基礎解析器的組合,可以組合出更復雜的解析器。

nom 倉庫中提供了一個分類組合子的文件

從其中,我們可以找到一個適用於我們場景的組合子,例如:delimited
文件中對該解析器的描述是:用第一個解析器匹配一個物件,然後丟棄它;然後用第二個解析器匹配特定的內容,並獲取它;最後用第三個解析器匹配物件,並將物件丟棄。
剛好適用於我們的 (12345)

  • 1.用第一個解析器解析出 (,並丟棄;
  • 2.然後用第二個解析器匹配出 12345
  • 3.最後用第三個解析器解析出 ) 並丟棄。

用程式碼實現如下:

fn parse_u32(input: &[u8]) -> IResult<&[u8], &[u8]> {
    delimited(tag("("), digit0, tag(")"))(input)
}

是的,只有一行程式碼,就能實現解析 (12345)。這個函式中,我們可能對 digit0 比較陌生,它是 delimited 函式中的第二個解析器,nom 中封裝好的
它位於 nom::character::complete::digit0

解析數字

細心的朋友可能注意到,parse_u32 函式返回值是 IResult<&[u8], &[u8]>,也就是說,在成功時,它返回的輸入的剩餘資料的型別是 &[u8];解析的結果的型別也是 &[u8]
這是否和我們所說的拿到數值不太一致?

是的,文章開頭,我們的函式簽名是 fn parse_u32(input: &[u8]) -> IResult<&[u8], u32>
我們期望剩餘的輸入是 &[u8],解析的結果是 u32 型別。因此,我們需要在基於 parse_u32 的基礎上,將 &[u8] 型別的解析結果轉換成 String,再將字串轉換成 u32 型別:

fn parse_u32_ver1(input: &[u8]) -> IResult<&[u8], u32> {
    let mut my_parser = delimited(tag("("), digit0, tag(")"));
    let res = my_parser(input);
    match res {
        Ok((remain, raw)) => {
            let s1 = String::from_utf8_lossy(raw);
            let num: u32 = s1.parse().unwrap();
            Ok((remain, num))
        }
        Err(err) => Err(err),
    }
}

我們通過 String::from_utf8_lossy(raw); 將解析結果轉成 utf-8 編碼的字串,通過 let num: u32 = s1.parse().unwrap(); 將字串轉換成 u32 型別。

寫個單測驗證一下:

fn test_parse_u32_ver1() {
    assert_eq!(
        parse_u32_ver1("(12345)".as_bytes()),
        Ok(("".as_bytes(), 12345))
    );

    assert_eq!(parse_u32_ver1("(0)".as_bytes()), Ok(("".as_bytes(), 0)));
}

太棒了,驗證通過!

至此,總算是完成了一個簡單的解析器,但距離解析 sql、json 還很遠,不著急,慢慢來。我們下一章來試試如何解析字串。

參考

持之以恆!