这个星期,项目中要使用C++或C语言解析JSON格式的数据,把解析的结果放到一个通用的数据结构。这个通用的数据结构,实际上是作为web服务层(这一层大家可以认为是类似于PHP服务器或webpy的服务器容器)到web页面层(这一层是语法类似PHP脚本或者tornardo模板)的数据传输的协议。 之所以要这样处理, 主要是因为这个web类的项目(一般的web类项目也是如此)需求变化较快,而web的服务层使用是采用C++进行开发的,为了使当web服务层的数据格式变化不影响web页面层,所以双方使用统一的通用的数据结构。而之所以交代这么多的背景是, 为了让大家了解为什么我们不使用类似rapidjson或jsoncpp来实现json的解析而需要手写解析器。 因为使用类似rapidJson或者是jsoncpp之类的Json解析器,相当于我们要做:
JSON文档 -> json DOM -> 通用数据结构。
而如果手写解析器,只需要做:
JSON文档 -> 通用数据结构。
少一层转换能换来很多效率的提升。
说了这么多,下面开始进入正题。 以前学编译原理的时候,老师推荐过LEX /YACC来写编译器,其实这是古老的UNIX软件。 LINUX上有他们的GNU版本 FLEX、BISON。 这两个东西一个是词法分析器,一个是语法分析器。词法分析器的作用是把字符解析成单词。一般的把单词称为token, 而语法分析器则是把单词解析成语法树。
首先来看flex的使用:简单来说分为两步: 1 先定义一个flex的输入文件,描述词法。2 用flex程序处理这个文件,生成对应的C语言源代码文件。
一般flex的输入文件以.l文件结尾, 比如这个文件json.l。
01 |
%{ |
02 |
03 |
#define YYSTYPE _EasyTData* |
04 |
#include <iostream> |
05 |
#include "stdio.h" |
06 |
#include "easytdata.h" |
07 |
#include "json.y.hpp" |
08 |
09 |
10 |
%} |
11 |
12 |
int [+-]*[0-9]+ |
13 |
num [+-]*([0-9]|\.)* |
14 |
string \ "(\\.|[^\\" ])*\" |
15 |
ignore_char [ \t\r\n] |
16 |
identifier [a-zA-Z_][a-zA-Z0-9_]* |
17 |
18 |
%% |
19 |
{identifier} { |
20 |
if ( strcmp (yytext, "true" )==0) |
21 |
{ |
22 |
json2tdata_lval=ed_factory_bool( true ); |
23 |
return TRUE; |
24 |
} |
25 |
else if ( strcmp (yytext, "false" )==0) |
26 |
{ |
27 |
json2tdata_lval=ed_factory_bool( false ); |
28 |
return FALSE; |
29 |
} |
30 |
else if ( strcmp (yytext, "null" )==0) |
31 |
{ |
32 |
json2tdata_lval=ed_factory_none(); |
33 |
return NIL; |
34 |
} |
35 |
else |
36 |
{ |
37 |
json2tdata_lval=ed_factory_string(yytext); |
38 |
return IDENTIFIER; |
39 |
} |
40 |
} |
41 |
42 |
{num} { |
43 |
json2tdata_lval=ed_factory_int( atoi (yytext)); |
44 |
return NUM; |
45 |
} /*要区分浮点数*/ |
46 |
47 |
48 |
{string} { |
49 |
/*去掉前后引号的处理,存到TData里面不需要引号*/ |
50 |
/**/ |
51 |
json2tdata_pre_process_string(yytext); |
52 |
json2tdata_lval=ed_factory_string(yytext); |
53 |
return STRING; |
54 |
} |
55 |
"{" { return L_BRACE;} |
56 |
"}" { return R_BRACE;} |
57 |
"[" { return L_BRACKET;} |
58 |
"]" { return R_BRACKET;} |
59 |
":" { return COLON;} |
60 |
";" { return SEMICOLON;} |
61 |
"," { return COMMA;} |
62 |
{ignore_char} |
63 |
%% |
文件分成三个部分。第一部分是从%{到 }%标记的部分。 这个部分会原封不动的复制到flex的生成代码中。文件开头定义了一个YYSTYPE宏。每个TOKEN可以有一个lval值属性,YYSTYPE定义类型就是token的lval的类型。_EasyTData是我们的web服务层和web页面层公用的通用数据结构。然后就是一些要include的头文件,第一部分就完了。
flex的输入文件的第二部分,是从%}到%%之间的部分,这部分用正则表达式定义了一些数据类型。 比如int num string ignore_char identifier等。 int型的定义就是(+-号)后面跟着一些重复的数字。注意这里使用的正则表达式的形式是ERE而不是BRE。 ERE与BRE比较明显的区别就是,ERE使用+表示字符重复一次以上,*表示字符重复0次以上。BRE使用{1,}这种方式表示字符重复1次以上。
flex的输入文件的第三部分,是%%到%%的部分。这里定义了词法分析器在解析的处理动作。yytext是一个flex内部的标识符,表示匹配到的字符串。上文介绍了,lval也是一个内部标识符,表示TOKEN的值。json2tdata_是标识符的前缀, 在执行flex的时候,用-P指定。
flex输入文件写完之后,使用下面这条命令,就可以把flex的输入文件转换为C语言的源代码了。 1 |
flex -P "json2tdata_" -o json.l.cpp json.l |
语法分析是使用bison工具。使用bison工具也是分为两步,第一步写bison的输入文件,第二步用bison程序生成C语言源码。
bison的输入文件一般用.y作为后缀名,比如下面这个json.y, 看下bison的输入文件长什么样子。
01 |
%{ |
02 |
|
03 |
#include "stdio.h" |
04 |
#include "easytdata.h" |
05 |
// #define YYDEBUG 1 |
06 |
#define YYSTYPE _EasyTData* |
07 |
|
08 |
extern int json2tdata_lex(); |
09 |
void json2tdata_error( const char *msg); |
10 |
|
11 |
_EasyTdata *g_oJsonData; //结果存放点 <span></span> %} |
12 |
%token INT NUM STRING IGNORE_CHAR L_BRACE R_BRACE L_BRACKET R_BRACKET COLON SEMICOLON COMMA IDENTIFIER TRUE FALSE NIL |
13 |
%% |
14 |
15 |
Json : Value {g_oJsonData=$1; /*printf("=========\nResult ToJson():%s",g_oJsonData.ToJson().c_str());*/ } |
16 |
17 |
Object : L_BRACE Pairs R_BRACE {$$=$2;} |
18 |
| L_BRACE R_BRACE {$$=ed_factory_map();} |
19 |
20 |
Array : L_BRACKET Elements R_BRACKET {$$=$2;} |
21 |
| L_BRACKET R_BRACKET {$$=ed_factory_vector();} |
22 |
23 |
ID : NUM {$$=$1;} |
24 |
| STRING {$$=$1;} |
25 |
| IDENTIFIER{$$=$1;} |
26 |
27 |
Pair : ID COLON Value |
28 |
{ |
29 |
|
30 |
$$ = ed_factory_pair($1, $3); |
31 |
} |
32 |
33 |
Pairs : Pairs COMMA Pair { |
34 |
ed_map_add_pair($1,$3); |
35 |
$$ = $1; |
36 |
} |
37 |
|