Parse 简介

2017-12-06  本文已影响74人  Taozii

导言

提到Rebol语言的优秀特性那就不得不说它的解析引擎,简称Parse。这项来自Carl Sassenrath的伟大设计,在过去的15年里,使得Rebol用户免受正则表达式(以不可维护著称)的折磨。现如今,Parse的增强版本在Red语言中重装上阵。

简而言之,Parse是一个使用语法规则来解析输入序列的内部DSL(在Rebol生态圈称为“方言”)。Parse方言是TDPL家族的突出一员。常用来校验,验证,分解,修改输入的数据,甚至是实现内部或者外部DSL。

parse函数的用法很简单:

parse <输入序列> <规则>
<输入序列>: 任意序列类型的值(字符串,文件,区块,路径...)
<规则>: 一个区块(包含有效的Parse方言)

下面的示例代码可以直接复制到Red控制台中运行,即便你不懂Red和Parse方言,也能观其大略,不像正则表达式那样让人不知所云。

使用语法规则验证一些字符串和区块的输入:

parse "a plane" [["a" | "the"] space "plane"]  ;规则中可以包含子规则
parse "the car" [["a" | "the"] space ["plane" | "car"]]

parse "123" ["1" "2" ["4" | "3"]]
parse "abbccc" ["a" 2 "b" 3 "c"] ;指定数量
parse "aaabbb" [copy letters some "a" (n: length? letters) n "b"] ;将匹配绑定到变量,使用小括号执行Red表达式

parse [a] ['b | 'a | 'c]
parse [hello nice world] [3 word!] ;匹配区块可以使用类型
parse [a a a b b b] [copy words some 'a (n: length? words) n 'b]

下面展示如何解析IPv4地址:

four:     charset "01234" ;charset函数用于创建一个bitset!类型的值
half:     charset "012345"
non-zero: charset "123456789"
digit:    union non-zero charset "0" ;bitset!类型可以使用Red的集合运算union函数进行组合

byte: [
      "25" half
    | "2" four digit
    | "1" digit digit
    | non-zero digit
    | digit
]
ipv4: [byte dot byte dot byte dot byte]

parse "192.168.10.1" ipv4
parse "127.0.0.1"    ipv4
parse "99.1234"      ipv4
parse "10.12.260.1"  ipv4

data: {
    ID: 121.34
    Version: 1.2.3-5.6
    Your IP address is: 85.94.114.88.
    NOTE: Your IP Address could be different tomorrow.
}
parse data [some [copy value ipv4 | skip]]
probe value                      ; 输出: "85.94.114.88"

一个粗糙然而实用的email地址验证器:

digit:   charset "0123456789"
letters: charset [#"a" - #"z" #"A" - #"Z"]
special: charset "-"
chars:   union union letters special digit
word:    [some chars]
host:    [word]
domain:  [word some [dot word]]
email:   [host "@" domain]

parse "john@doe.com" email
parse "n00b@lost.island.org" email
parse "h4x0r-l33t@domain.net" email

验证字符串形式的数学表达式(来自Rebol/Core手册

expr:    [term ["+" | "-"] expr | term]     ;规则可以递归定义
term:    [factor ["*" | "/"] term | factor]
factor:  [primary "**" factor | primary]
primary: [some digit | "(" expr ")"]
digit:   charset "0123456789"
    
parse "1+2*(3-2)/4" expr        ; 返回 true
parse "1-(3/)+2" expr           ; 返回 false

创建简单的解析器用于解析一个HTML子集:

html: {
    <html>
        <head><title>Test</title></head>
        <body><div><u>Hello</u> <b>World</b></div></body>
    </html>
}

ws: charset reduce [space tab cr lf]

parse html tags: [
    collect [any [
        ws
        | "</" thru ">" break
        | "<" copy name to ">" skip keep (load name) opt tags
        | keep to "<"
    ]]
]

; parse函数将会返回如下区块树
[
    html [
        head [
                title ["Test"]
            ]
        body [
            div [
                u ["Hello"]
                b ["World"]
            ]
        ]
    ]
]

Parse方言

Parse的核心原则如下:

(*)只要规则中使用了collect关键字,parse函数就返回一个区块,不论顶层规则是否匹配成功。

Parse的规则由如下元素构成:

Red的Parse实现可以使用下面的关键字,这些关键字可以自由组合。(共分成了5类)

匹配

ahead rule :预匹配规则,但是输入流位置不前进

end :当前输入流位置处于末尾则返回成功

none :总是返回成功(通用规则)(译者注:有误?实验结果更像是匹配空而不是通配)

not rule :反转子规则的结果

opt rule :预匹配规则,可选性地匹配该规则。

quote value :以字面值匹配(用于Parse方言的转义)

skip :输入流位置前进一个元素(字符或者一个值)

thru rule :前进输入流位置直到成功匹配规则,并将输入流位置放到匹配之后。

to rule :前进输入流位置直到成功匹配规则,并将输入流位置放到匹配之前。

控制流

break :中断匹配循环并返回成功

if (expr) :对Red表达式求值,如果为false或者none,匹配失败并回溯

into rule :转换输入流为已匹配的序列(字符串或者区块)并且使用指定的规则近一步解析

fail :强制当前规则匹配失败并回溯

then :不论接下来的匹配是否成功,都跳过下一个替代规则

reject :中断匹配循环并返回失败

迭代

any rule :重复匹配零次或者多次rule直到匹配失败或者输入流不再前进

some rule :重复匹配一次或者多次rule直到匹配失败或者输入流不再前进

while rule :重复匹配一次或者多次rule直到匹配失败并且忽略输入流是否前进

分解

collect [rule] :以区块的形式返回在匹配规则中收集到的值

collect set word [rule] :将匹配规则中收集到的值放入区块,并将区块绑定到word

collect into word [rule] :将匹配规则中收集到的值插入到word所引用的区块

copy word rule :拷贝匹配的内容并绑定到word

keep rule :拷贝匹配的内容并添加到一个区块

keep (expr) :将Red表达式中的最后一个值添加到一个区块

set word rule :将匹配的第一个值绑定到word

修改

insert only value :在当前输入流的位置插入一个值并将位置放置到该值之后

remove rule :删除匹配项

在Parse的核心原则中提到了两种解析模式。这在Red中是很有必要的(Rebol亦然),因为我们有两种基本的序列类型:string!和block!。当前字符串类型是由Unicode代码点的数组表示的(后来的Red版本中将会支持字符数组),区块类型是任意Red值的数组(包括其他区块)。

这在Parse方言的使用中引起了一些微小的差别。例如,新增的bitset!数据类型可以定义任意地字符集,用于在字符串中一次性匹配大量的字符。下面是使用bitset和迭代进行匹配的示例:

letter: charset [#"a" - #"z"]
digit:  charset "0123456789"

parse "hello 123 world" [5 letter space 3 digit space some letter]

Bitset! 数据类型

bitset值是一个存储布尔值的bit数组。在Parse上下文中,bitset可以表达整个Unicode范围内的任意字符集,并用来在单个操作内匹配一个输入字符,这相当有用。bitset!类型是在这次的0.4.1版本引入的,因而对其支持的特性有必要作一番概述。它和Rebol3中的bitset!实现基本是同一个水平。

要创建一个bitset,你需要提供一个或者多个字符作为基本规格。可以使用不同的形式提供:表示Unicode代码点的整型数值,char!类型的值,string!类型的值,前面几种元素的范围或者组合。一个新的bitset使用如下语法创建:

make bitset! <基本规格>
<基本规格>:char!, integer!, string! or block!

例如:

; 创建一个空的bitset,至少可以容纳50个bits
make bitset! 50

; 创建一个bitset,第65位bit被设置
make bitset! #"A"

; 创建一个bitset,第104,105位bit被设置
make bitset! "hi"

; 使用不同的值设置bit位
make bitset! [120 "hello" #"A"]

; 使用char!值的范围来创建bitset
make bitset! [#"0" - #"9" #"a" - #"z"]

两个值(只允许char!和integer!类型)加一个破折号定义了一个范围。

bitset的大小是根据提供的基本规格调整的。跟最高位的字节边界对齐。

charset函数用于方便的创建bitset,因此你可以这样写:

charset "ABCDEF"
charset [120 "hello" #"A"]
charset [#"0" - #"9" #"a" - #"z"]

要读写bitset的某个bit位,路径表示法再方便不过了:

bs: charset [#"a" - #"z"]
bs/97     ; will return true
bs/40     ; will return false
bs/97: false
bs/97     ; will return false

(注意:bitset的索引是从0开始的)

除此之外,bitset类型还支持下面的操作:
pick, poke, find, insert, append, copy, remove, clear, length?, mold, form

关于这些操作的详细用法请参考Rebol3的bitset文档

为了处理Unicode字符这么大的范围,bitset之外的bit位被当作虚拟位来处理,因此访问或者设置这些位不会发生错误,bitset会根据需要自动扩展。然而这还是不足以应付很大的范围,比如除了数字以外的所有Unicode字符。这种情况下通过定义bitset的补集,既可以表达大范围的字符又占用很少的空间。

bitset补集的创建方式和普通bitset一样,只是需要使用not开头而且一般使用区块作为规格:

; 除数字以外的所有字符
charset [not "0123456789"]

; 除十六进制符号外的所有字符
charset [not "ABCDEF" #"0" - #"9"]

; 除空白字符外的所有字符
charset reduce ['not space tab cr lf]

集合运算对于bitset也是可用的,然而当前Red只支持union(毕竟这是bitset用得最多的函数)。使用union,你可以将两个bitset合并为一个新的bitset:

digit: charset "0123456789"
lower: charset [#"a" - #"z"]
upper: charset [#"A" - #"Z"]

letters:  union lower upper
hexa:     union upper digit
alphanum: union letters digit

Parse的实现

Red中的Parse方言是使用FSM实现的,不同于Rebol3依赖于递归函数调用。使用FSM使得一些有趣的特性成为可能,例如暂停和恢复解析,序列化解析状态然后发送到远端,然后重新加载并继续解析。现在这些都可以轻易办到。

Red的Parse由接近1300行Red/System代码实现,其中相当一部分用于优化常见使用情况下的迭代循环。手写了大约770个单元测试用于覆盖基本特性。

当前Parse是以解释器模式运行的,这对于大部分使用情况都足够快了。为了满足那些需要最大化性能的严重依赖解析的应用,给parse添加静态编译器的相关工作也已经开展。到时生成的Red/System代码应该平均比解释器版本快一个量级。当Red实现自举后,还会实现一个Parse的JIT编译器用于处理静态编译器不能处理的问题。

当Red有了更多新特性后,Parse也会相应的持续改进以便利用这些新特性。将来的多项改进包括,当binary!类型可用后,Parse也会添加对binary!类型的解析,并且当port!类型可用时,流解析也将成为可能。

Red的Parse还支持一个公开的面向事件API,你可以通过/trace refinement给parse函数传递一个回调函数。

parse/trace <输入> <规则> <回调函数>

<回调函数> 规格:

func [
    event   [word!]   ; 跟踪事件
    match?  [logic!]  ; 上次匹配操作的结果
    rule    [block!]  ; 当前位置的对应规则
    input   [series!] ; 将要匹配的下一个位置的输入序列
    stack   [block!]  ; 内部的解析规则栈
    return: [logic!]  ; TRUE: 继续匹配, FALSE: 退出
]

事件列表:
- push    : 当一个规则或者区块入栈时
- pop     : 当一个规则或者区块出栈之前
- fetch   : 当一个新的规则被采用之前
- match   : 当一个值匹配发生了
- iterate : 当一个新的迭代开始了 (ANY, SOME, ...)
- paren   : 当一个小括号中的表达式被求值后
- end     : 当到达输入末尾时

该API将来会扩展出更多细粒度的事件。该API可以被用于对Parse进行跟踪,获取状态,调试,... 咱们且拭目以待Red用户们会玩出什么新的花样!;-)

有一个默认的回调函数被用于跟踪。你可以方便的通过使用parse-trace函数来使用它:

parse-trace <输入> <规则>

你可以试着用一些简单的解析规则来查看输出结果。

关于DSL支持

Parse是实现DSL(内部或者外部)解析器的强大工具,它可以在规则中内联Red表达式,从而能够轻易的将DSL语法和其语义关联起来。为了表明这点,下面是一个Parse写的解释器示例,用于解释著名的晦涩语言Brainfuck

bf: function [prog [string!]][
    size: 30000
    cells: make string! size
    append/dup cells null size

    parse prog [
        some [
              ">" (cells: next cells)
            | "<" (cells: back cells)
            | "+" (cells/1: cells/1 + 1)
            | "-" (cells/1: cells/1 - 1)
            | "." (prin cells/1)
            | "," (cells/1: first input "")
            | "[" [if (cells/1 = null) thru "]" | none]
            | "]" [
               pos: if (cells/1 <> null)
               (pos: find/reverse pos #"[") :pos
               | none
              ]
            | skip
        ]
    ]
]

; This code will print a Hello World! message
bf {
    ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.
    >++.<<+++++++++++++++.>.+++.------.--------.>+.>.
}

; This one will print a famous quote
bf {
    ++++++++[>+>++>+++>++++>+++++>++++++>+++++++>++++++++>
    +++++++++>++++++++++>+++++++++++>++++++++++++>++++++++
    +++++>++++++++++++++>+++++++++++++++>++++++++++++++++<
    <<<<<<<<<<<<<<<-]>>>>>>>>>>>----.++++<<<<<<<<<<<>>>>>>>
    >>>>>>.<<<<<<<<<<<<<>>>>>>>>>>>>>---.+++<<<<<<<<<<<<<>>
    >>>>>>>>>>>>++.--<<<<<<<<<<<<<<>>>>>>>>>>>>>---.+++<<<<
    <<<<<<<<<>>>>.<<<<>>>>>>>>>>>>>+.-<<<<<<<<<<<<<>>>>>>>>
    >>>>>>+++.---<<<<<<<<<<<<<<>>>>.<<<<>>>>>>>>>>>>>>--.++
    <<<<<<<<<<<<<<>>>>>>>>>>>>>>-.+<<<<<<<<<<<<<<>>>>.<<<<>
    >>>>>>>>>>>>>+++.---<<<<<<<<<<<<<<>>>>>>>>>>>>>>.<<<<<<
    <<<<<<<<>>>>>>>>>>>>>>-.+<<<<<<<<<<<<<<>>>>>>>>>>>>>>-.
    +<<<<<<<<<<<<<<>>>>>>>>>>>>>>--.++<<<<<<<<<<<<<<>>>>+.-
    <<<<.
}

注意:为了使得实现更加简短,该实现仅限于处理一层的"[...]"结构。使用Parse写的稍长和稍复杂的完整实现点这里

所以当前应付较小的DSL应该不在话下。对于较复杂的DSL理论上也没问题,然而当DSL的语义变得更复杂时,Parse能起的作用就没这么大了。对于大部分的用户来说,实现一个较高级DSL的解释器或者编译器都绝非易事。针对这个问题,Red将来要着手在Parse之上提供一套元DSL(meta-DSL)包装,暴露一组更高级的API用于抽象解释器或者编译器的核心组件,使得编写复杂DSL变得更容易。要实现一个DSL用来创建其他DSL的想法绝不是痴人说梦,Rascal语言中已经存在这种强大的形式了。Red也会朝着这个方向迈进,然而不会有Rascal这么复杂。

上一篇下一篇

猜你喜欢

热点阅读