摘要:解析器就是编译原理中的语言的词法分析器,可以按照文法规则提取字符或者单词。功能:接受扫描器的输入,并根据语法规则对输入流进行匹配,匹配成功后执行语义动作,进行输入数据的处理。

C++ 程序员需要面对的最复杂的任务之一就是在一段合理的时间期限内编写一个解析器。在为 SQL 或 C++ 这类成熟的语言开发编译器时,使用 GNU Flex/Bison 或 ANTLR 解析器生成程序通常是不错的选择;但是对于使用更简单的 Backus Naur Form(BNF)的语法,这些工具陡峭的学习曲线并不总是物有所值。另一种替代选择是使用标准 Linux® 发行版附带的正则表达式库或 Boost regex 或 tokenizer 库,但是它们不能根据日渐复杂的语法进行良好扩展。

本文介绍了来自 Boost 的高可扩展性 Spirit 解析器框架。该解析器生成程序遵循 Extended Backus Naur Form (EBNF) 规范并使用 C++ 编写,可以显著缩短开发时间。要进一步阅读,请查看详细的 Spirit 文档。

安装 Spirit

您可以从 Boost 的 Web 站点免费下载 Spirit 框架(参见 参考资料 小节)。在开始使用
Spirit 进行开发之前,需注意以下事项:

  • 必须在源代码中包含 <spirit.hpp> 头文件。该头文件将大量使用元模板编程和仿函数(functor)。本文的所有代码均使用
    g++-3.4.4 进行编译。确保使用支持 C++ 特性的编译器。
  • 部分 Spirit 框架在内部使用来自 Boost 的正则表达式库,在已安装的代码库中检查 regex.h 头文件。
  • 确保 Boost 安装的根目录位于编译器的 include 搜索路径中。
  • Spirit 是一个只包括头文件的库,因此在链接时不需要任何额外的库。但是 regex 是一个例外。要将 regex 源代码只作为头文件包含,可在代码中使用预处理器指令 define
    BOOST_SPIRIT_NO_REGEX_LIB

第一个 Spirit 项目

如果提供一个随机的单词列表,您的第一个 Spirit 项目将使用 C++ 风格列出列表中 Hello World(即 Hello 和 World 两词在输入流中连在一起出现)出现的次数。参见清单
1;清单 2 显示了输出。

清单 1. 列出单词 Hello World 在输入流中出现的次数
#define  BOOST_SPIRIT_NO_REGEX_LIB

#include "regex.h"
#include "spirit.hpp"
#include "boost/spirit/actor.hpp"
using namespace boost::spirit; const string input = "This Hello World program using Spirit counts the number of
Hello World occurrences in the input"; int main ()
{
int count = 0;
parse (input.c_str(),
*(str_p("Hello World") [ increment_a(count) ]
|
anychar_p)
);
cout << count >> endl;
return 0;
}

Spirit 框架的强大在于它为大量基本类型提供了内置解析器,包括单独的字符、数字和字符串。更复杂的解析器通常都使用这些内置解析器对象创建。在 清单
1
 中,str_p 和 anychar_p 都是
Spirit 中预定义的解析器 —— str_p 匹配它所提供的字符串(在此为 Hello
World)并成功调用 increment_a 例程将计数加
1。anychar_p 是另一个预定义解析器,它可以匹配任何字符。

让我们看一看 parse 函数,它实际上是
Spirit 框架中最重要的例程。它接受一个输入流和一个语法,并在内部通过语法运行此输入流。在本例中,输入流来自 input.c_str(),而 str_p 和 anychar_p 为语法提供语义。如果熟悉解析的话,将很快就明白 parse 函数的第二个参数相当于提供了一个
BNF。

其他预定义的 Spirit 解析器

考虑符合以下模式的解析器:<employee
name: string> <employee id: int> <employee rating: float>
。您需要根据从该字符串提取的数据填充 Employee 数据结构。下面是一个典型的字符串:"Alex
8 9.2 Jim 91 5.6"

Spirit 为字符串(alpha_p)、整数(int_p)、和实数(real_p)预定义了解析器。因此,可以认为 parse 例程应该使用以下语法调用:parse(input.c_str(),
alpha_p >> int_p >> real_p)
。这里的逻辑是 parse 将在输入流中首先查找一个字符串,然后查找整数,最后查找一个实数。这样可行吗?行不通。清单
2 展示了可以解析数据的可行代码片段。

清单 2. 使用 alpha_p、int_p 和 real_p 预定义解析器
#define  BOOST_SPIRIT_NO_REGEX_LIB

#include "regex.h"
#include "spirit.hpp"
#include "boost/spirit/actor/assign_actor.hpp" using namespace std;
using namespace boost::spirit; const string input = "Alex 8 9.2 Jim 91 5.6"; typedef struct {
string name;
int idcode;
float rating;
} Employee; int main ()
{
string name;
int idcode;
float rating; int status = parse (input.c_str(),
*((+alpha_p) [assign_a(name)] >> ' ' >>
int_p[assign_a(idcode)] >> ' ' >>
real_p[assign_a(rating)] >> !blank_p)
).full;
cout << status << endl;
return 0;
}

初始调用失败有以下几个原因:

  • alpha_p 解析了单个的字符。要解析字符,必须使用 +alpha_p(这类似于
    EBNF + 操作符,表示一个或多个字符,不同的是
    Spirit 在前面而不是后面使用它)。
  • 使用空格分隔字符串、整数和实数。必须解释这种行为。可以通过两种方式实现:使用 '
    '
    ;或者使用 blank_p 预定义解析器,这更好,它同时解释了空格和制表符。

下面是修改后的解析调用:

parse(input.c_str(), *((+alpha_p) >> ' ' >> int_p >> ' ' >> real_p) >> !blank_p);

第二个参数严格匹配一个非字母和数字组成的字符串,该字符串后面依次为空格、整数、另一个空格,最后是一个实数。当解析器达到实数后,它将查找一个空格/制表符,并重新开始匹配序列或终止。! 操作符表示空格/制表符出现了
0 次或 1 次。* 操作符表示该序列出现了
0 次或 1 次,并因此匹配一个空字符串。

显然,第二个字符串与传统解析器使用的潜在语法规则之间存在直接联系。下面是针对当前需求的典型语法规则:

:S -> (ALPHA INT REAL)*

ALPHAINT 和 REAL 通常由
lexer 提供。例如,INT 被定义为
(0-9)+。可以使用 Spirit 合并这些步骤。

如何诊断错误?

如果解析器出现了错误,可以使用几种方法诊断2错误。最简单的检验方法是测试 parse 方法返回的数据结构。返回的数据结构被称为parse_info,而 hit 字段表示解析是否成功完成。清单
3 展示了来自 Boost 源代码的 parse_info 结构。

清单 3. 解析方法返回的 parse_info 结构
   template <typename IteratorT = char const*>
struct parse_info
{
IteratorT stop; // points to final parse position
bool hit; // true when parsing is successful
bool full; // when the parser consumed all the input
std::size_t length; // number of characters consumed by parser parse_info(
IteratorT const& stop_ = IteratorT(),
bool hit_ = false,
bool full_ = false,
std::size_t length_ = 0)
: stop(stop_)
, hit(hit_)
, full(full_)
, length(length_) {} template <typename ParseInfoT>
parse_info(ParseInfoT const& pi)
: stop(pi.stop)
, hit(pi.hit)
, full(pi.full)
, length(pi.length) {}
};

assign_a 是什么?

如果预定义解析器匹配字符串,则需要将结果保存起来。通过使用 assign_a 结构,解析后的字符串将被分配给相应的变量。Spirit
框架中分配/修改变量的通用结构称为actors,它位于 boost/spirit/actor 文件夹中。

Spirit 操作符及其语义

Spirit 附带了一些预定义的操作符。表 1 总结了这些操作符及其语义。后面的示例将使用这些操作符。

表 1. Spirit 操作符及其语义
操作符 语义
x >> y 匹配 x 然后匹配 y
x | y 匹配 x 或 y
x & y 匹配 x 和 y
x – y 匹配 x 但不匹配 y
x ^ y 匹配 x 或 y,但不同时匹配两者
*x 对 x 匹配 0 次或多次
+x 对 x 匹配 1 次或多次
!x 对 x 匹配 0 次或 1 次
( x ) 匹配 x;用于基于优先权的分组
x [ function expression ] 如果匹配了 x,执行函数/仿函数
x % y 对 x 匹配 1 次或多次,使用 y 分隔

了解到目前为止所开发的内容之后,现在可以开始定义 C 风格的浮点数语法。清单 4 展示了 BNF。

清单 4. 用于浮点数的 BNF
Real-Number : Fractional-Part (Exponent-Part)?
Fractional-Part : (DIGIT)* DOT (DIGIT)+
|
(DIGIT)+ DOT
Exponent-Part : ('e'|'E') ('+'|'-')? (DIGIT)+
DIGIT : ['0'-'9']
DOT : '.'

清单 5 提供了等效的 Spirit 语法。

清单 5. 浮点数的 Spirit 语法,与清单 4 的 BNF 等效
Real-Number = Fractional-Part >> ! Exponent-Part
| +digit_p >> Exponent-Part
; Fractional-Part = *digit_p >> '.' >> +digit_p
| +digit_p >> '.'
; Exponent-Part = ('e' | 'E') >> !('+' | '-') >> +digit_p;

可以看到,Spirit 上下文中的 Y
= A >> B
 与解析器上下文的 Y : A
B
 相同,其中 A 和 B 可以是末端,也可以是非末端。注意,用户并不需要为此类琐碎的操作定义语法:Spirit
已经提供了预定义的 parser
real_p
 来解析实数。

Spirit 中的预定义解析器

Spirit 框架的灵活性源于它为常见处理提供了众多预定义解析器。表 2 提供了包含其中一些解析器的列表。

表 2. Spirit 中的一些预定义解析器
解析器 语义
ch_p 匹配一个单个的字符。
range_p 匹配从低/高字符对中创建的一组字符中的单个字符。例如,range_p('a',
'z') 
匹配 a 和 z 之间的所有字符。
anychar_p 匹配任何单个的字符,包括 NULL 终端符 \0
str_p 匹配一个字符串:例如 str_p("mystring") 匹配字符串 mystring
blank_p 匹配空白和制表符组成的连续序列。
space_p 类似于 blank_p,但它还匹配返回字符和换行字符。
digit_p 匹配一个数字。
upper_p 匹配任何大写字符。
nothing_p 诊断工具;从不匹配任何内容并且总是失败。

Spirit 指令

本节讨论 Spirit 的另一个强大特性 —— 指令。Pascal 和 VHDL 等大小写敏感语言中的 lexer 要复杂一些,因为它们必须解析 begin 和 BEGin等内容并为解析器生成相同的标记。Spirit
使用 parser directives 解决这个问题。例如,预定义指令 as_lower_d 将输入流转换为小写(参见清单
6)。

清单 6. 使用 as_lower_d 指令进行大小写敏感的解析
#define  BOOST_SPIRIT_NO_REGEX_LIB

#include "regex.h"
#include "spirit.hpp"
#include "boost/spirit/actor/assign_actor.hpp" using namespace std;
using namespace boost::spirit; const string input = "THis iS a ranDOm sTRInG"; int main ()
{
string val;
int status = parse (input.c_str(),
as_lower_d[str_p ("this is a random string")
[assign_a(val)] ]).full;
cout << status << endl;
cout << val << endl; return 0;
}

清单 6 的输出为 1,
THis iS a ranDOm sTRInG
。必须理解解析器与解析器指令之间的差异,后者仅修改附带的解析器的行为,实际上扩充了该解析器的策略。

Spirit 提供了其他预定义解析器的指令和一些编写解析器的方法。让我们看一下 longest_d 解析器指令。考虑清单
7 并猜猜它的输出是什么。

清单 7. 使用模糊的语法进行解析
#define  BOOST_SPIRIT_NO_REGEX_LIB

#include "regex.h"
#include "spirit.hpp"
#include "boost/spirit/actor/assign_actor.hpp" using namespace std;
using namespace boost::spirit;
const string input = "20245.1"; int main ()
{
int val;
int status = parse (input.c_str(), int_p[assign_a(val)] | real_p).full;
cout << status << " " << val << endl; return 0;
}

清单 7 的输出是 0
20245
。为什么会这样?显然,解析期间整个输入缓冲区都没有被使用,因此 status 为 0。为了理解这一点,需要注意
Spirit 是如何解析的:为示例规则 S : R1
| R2 | .. | RN
 提供多个替代选择,左边的内容获得最大优先权。这类似于 C/C++ 处理条件的方式:在表达式 if
(x && y) 
中,如果 x 为真,则不计算 y。这种行为有助于保持工具的处理速度。

在本例中,int_p 匹配
20245 —— 但是在这之后它遇到了一个点字符,并且没有处理它的规则。因此,解析器退出。

解决方法是对语法规则的所有可用的替代内容进行重新分组,但是手动重新分组很容易出错。更好的方法是使用 longest_d 指令,该指令将尝试匹配消耗输入流的最大长度的规则。清单
8 展示了修改后的 parse 例程调用。

清单 8. 使用 longest_d 预定义的解析器指令
  int status = parse (input.c_str(),
longest_d [int_p | real_p[assign_a(val)] ]
).full;

通过这一修改,输出现在变为 1
20245.1

使用 Spirit 开发完备的语法

本节将讨论使用 Spirit 框架设计一组用户定义的语法规则。要设计自己的语法,Spirit 要求执行以下操作:

  1. 创建一个从预定义 grammar 类继承而来的派生类。grammar 类是一个模板类,被其派生类 DerivedT 和上下文类 ContextT 参数化。语法类的声明如下所示:

    template<
    typename DerivedT,
    typename ContextT = parser_context<> >
    struct grammar;
  2. 您设计的派生类必须有一个名为 definition(可以不修改此名)的嵌套的模板类/结构。definition 类有以下特性:
    • 它是类型名为 ScannerT 的模板类。
    • 语法规则在其构造函数中定义。构造函数被作为引用传递给实际的语法 self
    • 必须提供名为 start 的成员函数,它表示
      start 规则。

清单 9 展示了用户定义语法的基本框架。

清单 9. 用户定义的语法类的基本框架
    struct my-grammar : public grammar<my-grammar>
{
template <typename ScannerT>
struct definition
{
rule<ScannerT> startRule;
definition(my-grammar const& self) { /* define grammar rules here */ }
rule<ScannerT> const& start() const { return startRule; }
};
};

假设您希望支持清单 10 所示的简单语法,该语法部分解析 C/C++ 枚举。

清单 10. C/C++ 枚举的简单语法
enum_specifier : ENUM '{' enumerator_list '}'
| ENUM IDENTIFIER '{' enumerator_list '}'
| ENUM IDENTIFIER
; enumerator_list : enumerator
| enumerator_list ',' enumerator
; enumerator : IDENTIFIER
; ENUM: "enum";
IDENTIFIER: ['a'..'z']+;

清单 11 展示了相应的 Spirit 代码。程序的输出为 1,表示成功完成解析。

清单 11. 解析 C/C++ 枚举的 Spirit 代码
#define  BOOST_SPIRIT_NO_REGEX_LIB

#include "regex.h"
#include "spirit.hpp"
#include "boost/spirit/actor/assign_actor.hpp" using namespace std;
using namespace boost::spirit; struct my_enum : public grammar<my_enum>
{
template <typename ScannerT>
struct definition
{
definition(my_enum const& self)
{
enum_specifier = enum_p >> '{' >> enum_list >> '}';
enum_p = str_p("enum");
enum_list = +id_p >> *(',' >> +id_p);
id_p = range_p('a','z');
} rule<ScannerT> enum_specifier, enum_p, enum_list, id_p;
rule<ScannerT> const& start() const { return enum_specifier; }
};
}; string input = "enum { ah, bk }"; int main ()
{
my_enum e;
int status = parse(input.c_str(), e, space_p).hit;
cout << status << endl;
return 0;
}
参考:
(1)官网 http://www.boost.org/doc/libs/1_35_0/libs/spirit/
(2)http://blog.chinaunix.net/uid-10716167-id-2934083.html
转自:http://www.ibm.com/developerworks/cn/aix/library/au-boost_parser/

boost之词法解析器spirit的更多相关文章

  1. Boost学习之语法解析器--Spirit

    Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C ...

  2. Atiit 如何手写词法解析器

    Atiit 如何手写词法解析器 1.1. 通过编程直接从正则->nfa->dfa->表驱动词法解析一条龙自动生成.那是用程序自动生成是需要这样的,自己手写完全不必要这么复杂1 1.2 ...

  3. 自制操作系统Antz(14)——实现内置编程语言(词法解析器)

    AntzScript

  4. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

  5. Scala词法文法解析器 (二)分析C++类的声明

    最近一直在学习Scala语言,偶然发现其Parser模块功能强大,乃为BNF而设计.啥是BNF,读大学的时候在课本上见过,那时候只觉得这个东西太深奥.没想到所有的计算机语言都是基于BNF而定义的一套规 ...

  6. Scala词法文法解析器 (一)解析SparkSQL的BNF文法

    平台公式及翻译后的SparkSQL 平台公式的样子如下所示: if (XX1_m001[D003]="邢おb7肮α䵵薇" || XX1_m001[H003]<"2& ...

  7. jQuery 2.0.3 源码分析Sizzle引擎 - 词法解析

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 浏览器从下载文档到显示页面的过程是个复杂的过程,这里包含了重绘和重排.各家浏览器引擎的工作原理略有差别,但也有一定规则. 简 ...

  8. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析

    jQuery源码9600多行,而Sizzle引擎就独占近2000行,占了1/5.Sizzle引擎.jQuery事件机制.ajax是整个jQuery的核心,也是jQuery技术精华的体现.里面的有些策略 ...

  9. 一起写一个JSON解析器

    [本篇博文会介绍JSON解析的原理与实现,并一步一步写出来一个简单但实用的JSON解析器,项目地址:SimpleJSON.希望通过这篇博文,能让我们以后与JSON打交道时更加得心应手.由于个人水平有限 ...

随机推荐

  1. Oracle Day3 多行函数、多表查询

    1.多行函数 Sum avg max min count 组函数具有滤空的作用(添加nvl屏蔽该功能) 分组group by 多行分组 分组过滤 where 和分组过滤的区别(having) 分组的增 ...

  2. ios NSComparator 三种枚举类型

    NSComparator有3种枚举类型 NSOrderedDescending 降序,但是用他可以实现升序或者降序都没问题. NSOrderedAscending 升序,但是目前没有使用出任何效果.. ...

  3. python selenium基本

    基本 from selenium import webdriver import re driver = webdriver.Firefox() driver.get('https://www.goo ...

  4. RESTful架构2--架构详解

    转自:RESTful架构详解 1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在200 ...

  5. android网络编程之HttpUrlConnection的讲解--DownLoadManager基本用法

    1.DownLoadManager是Android用系统服务(Service)的方式来优化处理长时间的下载操作的一个工具类.避免了我们去处理多线程,通知栏等等. 2.不要忘记添加权限 <uses ...

  6. mac中使用终端生成RSA私钥和公钥文件

    1.打开终端输入:cd Desktop/   //进入桌面 2.OpenSSL   //打开 OpenSSL 3.生成私钥pem,  执行命令   genrsa -out rsa_private_ke ...

  7. Ajax及select级联

    cascade.jsp(Test/WebContent/jsp/cascade.jsp): <%@ page language="java" contentType=&quo ...

  8. Java学习笔记之接口和抽象类

    接口(interface)1.interface创建一个接口,implements实现接口 interface jiekou{} class lie implements jiekou{}2.接口可以 ...

  9. java transient修饰符

    1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问. 2)transient关键字只能修饰变量,而不能修饰方法和类.注意,本地变量是不能被trans ...

  10. HttpServletResponse对象

    Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象.和代表响应的response对象. request和response对象即然代表请求和响应,那我们 ...