#关于flex&bison(lex&yacc)解析JSON的探索 **目录** [TOC] ##前言 关于lua,有句话很出名: >它使我们很脸红…… 像`Lua`、`TCC`这种,又小又好用的东西,然而我们却写不出来这样的东西,确实是很可惜的…… 其实我还是研究过`Lua`的源码一段时间的……比如说`Lua1.0`版本比较简单,里面就是用`lex&yacc`工具辅助进行解析的…… 此外我还对`wxBasic`的实现也比较感兴趣(好歹我也是自称`BASIC`出身的程序媛啊……惭愧惭愧),它也是用`lex&yacc`工具辅助进行解析的…… 编译原理这门课我还真没学过,然而自学了三年,还是停留在第一章,只能说意志不坚啊…… 话说直到前几天,在群里有个同事问,`JSON`的解析需要什么算法么?我说与其说需要算法,不如说需要编译原理吧……可惜我对编译原理也是略通啊…… 但是这也并不能阻止我用`lex&yacc`去写一个`JSON`解析器啊……当然,我们的目标不在于实现,而在于弄清楚其原理…… 同事的求问激起了我的强烈兴趣,我写一个`JSON`解析器尝试一下……以此来打开我进入新世界的大门~ 先说说工具和环境,工具用的是`UnxUtils`中的`flex&bison`工具,环境用的还是万年不变的`MinGW`……关于`JSON`的说明资料…… ##词法解析器(lex/flex) 说干就干,先是用`flex`工具,写一个词法解析器,这一步还是相对简单的…… 当然我们的目标不是写一个相对完整的`JSON`,所以,在对`Number`的正则上也是随便意思一下…… 但是对于`String`类型的词法解析还是比较复杂的,我凑了半天没凑出正则了,后来我就参考了这篇文章《[Lex识别C风格字符串和注释](http://blog.csdn.net/xfxyy_sxfancy/article/details/45024573 "Lex识别C风格字符串和注释")》…… 总体来说还是没有什么难点的……我们先写一些测试输出看看拼凑的正则对不对,感受一下lex神器带来的优越感~ 当然,在这儿我们用的工具是`flex`……到词法解析这一步,整体还是挺顺风顺水的…… `testjson.l`文件: ```lex %{ #include <string.h> char* yylval; %} def [_a-zA-Z][_a-zA-Z0-9]* str \"(\\\"|[^\"])*\" num [0-9]+(\.[0-9]+)? arrs \[ arre \] objs \{ obje \} split , desc : %% {str} { yylval = strdup(yytext); printf("[STR:%s]",yylval); free(yylval);} {def} { yylval = strdup(yytext); printf("[DEF:%s]",yylval); free(yylval);} {num} { yylval = strdup(yytext); printf("[NUM:%s]",yylval); free(yylval);} {arrs} {printf("[ARRS]");} {arre} {printf("[ARRE]");} {objs} { printf("[OBJS]"); } {obje} { printf("[OBJE]"); } {desc} { printf("[DESC]"); } {split} { printf("[SPLIT]");} %% ``` 然后用flex工具生成C文件,用gcc编译一下,相关命令: ```Shell flex -o testjson.c testjson.l gcc -o testjson.exe testjson.c -lfl ``` 当然,我写了个批处理来编译运行它,内容如下: ```Shell @echo off color 1E echo 当前目录:%~dp0 cd "%~dp0" echo flex编译testjson.l生成testjson.c... flex -o testjson.c testjson.l echo gcc编译testjson.c链接libfl.a输出testjson.exe... gcc -o testjson.exe testjson.c -L. -lfl echo 运行testjson.exe输入json.txt作为测试文件... echo 输出结果: testjson.exe<json.txt pause ``` 其中的示例`json.txt`内容如下(后面再不声明的情况下,都是用的这个串): ```json { a:[1,2,3], b:["a\tbc","12 3","4,5\"6",{ x:1, y:"cc\ncc" },4.56], "text":"I'm OK~", "1-2":234 } ``` 运行批处理生成结果如下: ``` 当前目录:C:\jsonParse\lexTest\ flex编译testjson.l生成testjson.c... gcc编译testjson.c链接libfl.a输出testjson.exe... 运行testjson.exe输入json.txt作为测试文件... 输出结果: [OBJS] [DEF:a][DESC][ARRS][NUM:1][SPLIT][NUM:2][SPLIT][NUM:3][ARRE][SPLIT] [DEF:b][DESC][ARRS][STR:"a\tbc"][SPLIT][STR:"12 3"][SPLIT][STR:"4,5\"6" ][SPLIT][OBJS] [DEF:x][DESC][NUM:1][SPLIT] [DEF:y][DESC][STR:"cc\ncc"] [OBJE][SPLIT][NUM:4.56][ARRE][SPLIT] [STR:"text"][DESC][STR:"I'm OK~"][SPLIT] [STR:"1-2"][DESC][NUM:234] [OBJE] 请按任意键继续. . . ``` 输出的结果感觉还是挺优雅的~ 特别是给批处理配上颜色之后,大家感受一下效果图: ![词法解析器输出效果图](lexTestDemo.png "词法解析器输出效果图") ##语法解析器(yacc/bison) 然后,就是语法解析了……轮到`yacc`神器出场了,当然,我们在这儿用的工具是`bison`…… 对于`yacc`来说,最关键的东西就是语法生成树了……语法生成树的构造还是挺学问的…… 我尝试了一些写法……总之,终于凑出一个写法来结果是对的…… 因为编译原理自学的不好,所以搞出一些奇怪的异常来…… 在此要提醒的有这些:`yacc`源文件中的`#define YYSTYPE char*`放得高点比较好,还有就是那三个函数的定义,不写上会报`warnning`,尽管没什么影响…… 好了,先要改写`json.l`文件,使其的词法解析结果`token`作为`json.y`的输入: ```lex %{ #include <string.h> #include "json.tab.h" extern char* yylval; %} space [ \t\n]+ def [_a-zA-Z][_a-zA-Z0-9]* str \"(\\\"|[^\"])*\" num [0-9]+(\.[0-9]+)? arrs \[ arre \] objs \{ obje \} split , desc : %% {str} { yylval = strdup(yytext); return STR; } {def} { if(strcmp(yytext,"true")==0){ return TRUE; }else if(strcmp(yytext,"false")==0){ return FALSE; }else if(strcmp(yytext,"null")==0){ return NIL; }else{ yylval = strdup(yytext); return STR; } } {num} { yylval = strdup(yytext); return NUM; } {arrs} { return ARRS; } {arre} { return ARRE; } {objs} { return OBJS; } {obje} { return OBJE; } {desc} { return DESC; } {split} { return SPLIT; } {space} {} %% ``` 然后是`json.y`的语法树解析,输出一下对于解析结果的一些初步规划,比如说`push`和`add`之类的,这样可以查看结果跟预想的是不是一样: ```yacc %{ #define YYSTYPE char* #include <stdio.h> #include "lex.yy.c" int yyparse(void); int yyerror(const char* msg); int yywrap(); %} %token STR NUM DESC SPLIT ARRS OBJS ARRE OBJE FALSE TRUE NIL %% command : value {printf("end\n");} value: STR {printf("value:[string]%s\n",$1);free($1);} |NUM {printf("value:[number]%s\n",$1);free($1);} |FALSE {printf("value:FALSE\n");} |TRUE {printf("value:TRUE\n");} |NIL {printf("value:NULL\n");} |object {printf("value:OBJECT\n");} |array {printf("value:ARRAY\n");} ; arrs: ARRS {printf("array_start\n");}; array : arrs ARRE {printf("[empty]\n"); } | arrs values ARRE {printf("[...]\n"); } ; objs: OBJS {printf("object_start\n");}; object : objs OBJE {printf("{empty}\n");} | objs pairs OBJE {printf("{...}\n");} ; values : values SPLIT value {printf("add %s\n",$3);} |value {printf("add %s\n",$1);} |values SPLIT ; pairs : pairs SPLIT pair {printf("put\n");} |pair {printf("put\n");} |pairs SPLIT ; pair : STR DESC value {printf("key %s\n",$1);} ; %% int main() { return yyparse(); } int yyerror( const char* msg) { fprintf (stderr,"%s\n",msg); return 0; } int yywrap() { return 1; } ``` 同样,写个脚本运行一下,同样的测试字符串: ```Shell @echo off color 1E echo 当前目录:%~dp0 cd "%~dp0" echo flex编译json.l生成lex.yy.c... flex json.l echo bison编译json.y生成json.tab.h、json.tab.c... bison -d json.y echo gcc编译json.tab.c输出json.exe... gcc json.tab.c -o json.exe echo 运行json.exe输入json.txt作为测试文件... echo 输出结果: json.exe<json.txt pause ``` 输出的结果是这个样子的: ![语法解析输出效果图](bisonTestDemo.png "语法解析输出效果图") 还是挺有感觉的……配合堆栈以及`Map`和`List`的复式结构,就可以把这个`json`中表现的内容存起来了~ ##配套堆栈处理 语法解析是第一道坎,而相关的堆栈操作和处理便是第二道坎…… 每一步执行什么操作都是要有所设计,做到心中有数的…… 刚才已经看到了语法解析的处理结果,必须要配合堆栈才能把数据处理好…… 可惜,`C语言`里要写个`Stack`或者是`Map`是挺麻烦的……我并不准备这么干…… 因为我们的目标是弄清楚编译原理,而不是要实现各什么东西…… 于是我选择了语法解析之后,将结果转为`JS`语句,将`JSON`输出形成一个返回`JS对象`的函数…… 听上去还是挺酷的~相关的`json.l`文件不用变动,其中的`json.y`文件变成了这样: ```yacc %{ #define YYSTYPE char* #include <stdio.h> #include "lex.yy.c" int yyparse(void); int yyerror(const char* msg); int yywrap(); %} %token STR NUM DESC SPLIT ARRS OBJS ARRE OBJE FALSE TRUE NIL %% command : value {printf("\treturn curValue;\n");} value: STR {printf("\tcurValue=\"%s\"\n",$1);free($1);} |NUM {printf("\tcurValue=%s\n",$1);free($1);} |FALSE {printf("\tcurValue=false;\n");} |TRUE {printf("\tcurValue=true;\n");} |NIL {printf("\tcurValue=null;\n");} |object {printf("\tcurValue=curObj;\n\tcurObj=stack[stack.length-1];\n\n");} |array {printf("\tcurValue=curObj;\n\tcurObj=stack[stack.length-1];\n\n");} ; arrs: ARRS {printf("\tcurObj=[];\n\tstack.push(curObj);\n");}; array : arrs ARRE {printf("\tcurObj=stack.pop();\n"); } | arrs values ARRE {printf("\tcurObj=stack.pop();\n"); } ; objs: OBJS {printf("\tcurObj={};\n\tstack.push(curObj);\n");}; object : objs OBJE {printf("\tcurObj=stack.pop();\n");} | objs pairs OBJE {printf("\tcurObj=stack.pop();\n");} ; values : values SPLIT value {printf("\tcurObj.push(curValue);\n");} |value {printf("\tcurObj.push(curValue);\n");} |values SPLIT ; pairs : pairs SPLIT STR DESC value {printf("\tcurObj[\"%s\"]=curValue;\n",$3);} |STR DESC value {printf("\tcurObj[\"%s\"]=curValue;\n",$1);} |pairs SPLIT ; ; %% int main() { printf("(function(){\n\tvar stack = [];\n\tvar curObj = null;\n\tvar curValue=null;\n\n"); int r = yyparse(); printf("})()"); return r; } int yyerror( const char* msg) { fprintf (stderr,"%s\n",msg); return 0; } int yywrap() { return 1; } ``` 相应的测试批处理文件内容: ```Shell @echo off color 1E echo 当前目录:%~dp0 cd "%~dp0" echo flex编译json.l生成lex.yy.c... flex json.l echo bison编译json.y生成json.tab.h、json.tab.c... bison -d json.y echo gcc编译json.tab.c输出json.exe... gcc json.tab.c -o json.exe echo 运行json.exe输入json.txt输出“生成结果.txt”... json.exe<json.txt>生成结果.txt pause ``` 生成的结果: ```JavaScript (function(){ var stack = []; var curObj = null; var curValue=null; curObj={}; stack.push(curObj); curObj=[]; stack.push(curObj); curValue=1 curObj.push(curValue); curValue=2 curObj.push(curValue); curValue=3 curObj.push(curValue); curObj=stack.pop(); curValue=curObj; curObj=stack[stack.length-1]; curObj["a"]=curValue; curObj=[]; stack.push(curObj); curValue="a\tbc" curObj.push(curValue); curValue="12 3" curObj.push(curValue); curValue="4,5\"6" curObj.push(curValue); curObj={}; stack.push(curObj); curValue=1 curObj["x"]=curValue; curValue="cc\ncc" curObj["y"]=curValue; curObj=stack.pop(); curValue=curObj; curObj=stack[stack.length-1]; curObj.push(curValue); curValue=4.56 curObj.push(curValue); curObj=stack.pop(); curValue=curObj; curObj=stack[stack.length-1]; curObj["b"]=curValue; curValue="I'm OK~" curObj["text"]=curValue; curValue=234 curObj["1-2"]=curValue; curValue=false; curObj["mybool"]=curValue; curValue=null; curObj["mynull"]=curValue; curValue=true; curObj["myreal"]=curValue; curObj=stack.pop(); curValue=curObj; curObj=stack[stack.length-1]; return curValue; })() ``` 既然生成了`js`,就可以在浏览器里执行一下看看效果有什么不同: ![JSON转JS操作效果图](json2jsOperDemo.png "JSON转JS操作效果图") 从效果图上来看,结果还是挺棒的~~~~~ 这里,来讲解一下这段代码跟上面那段代码不同的地方。 刚才说了,`C`里面写堆栈、`List`和`Map`啰里啰嗦的,还是`js`方便……`js`中用变量`stack`作为`json`解析的处理堆栈…… 注意,在语法解析时,`yacc`里面其实是有堆栈的,一个是运行堆栈,一个是值堆栈,这是语法解析用的堆栈…… 而`js`中这个`stack`是真正的运行时堆栈……传说中的`runtime`对象……只是听上去高大上而已…… `stack`的出栈入栈就这么被体现出来了~ 还有一个是`curValue`、`curObj`,是保存当前值的变量……这两个变量赋值来赋值去的,看上去挺烦的…… 但是我觉得虽然很繁琐,但是这么写很好用啊~ 然后就这么出栈入栈,赋值来赋值去,一个生成`js对象`的函数就这么生成了……是不是很神奇? ##后话 我就这么通宵一晚上用`flex&bison`工具完成了`JSON`解析的大致原理性的实现…… 然后第二天去群里向那个求问的同事显摆,然而那个同事连看都不看一眼,表示,要用`java`写才行……还要用原生`JDK`方法,不能引入第三方包…… 我苦逼的问为啥,他说写完`java`版的`json`解析,就可以去阿里工作了! 我哈哈一笑,去阿里这么简单?我既然能写个`flex&bison`版的,我肯定就能写个`java`版的呀…… 你看,我这看了1/10本的编译原理的人都能写出来,这阿里的门槛不早被踏破了? 我说你等着,我很快就给你出个`java`版本的……于是我又通宵一晚上写了个`java`版本的…… 至于这个`java`版本的,留到下一篇博客中再说吧……


发表评论

必填,公开,这样称呼起来方便~

必填,不会被公开,不必担心~

http://

非必填,公开,目的是方便增进友好访问~

必填,请输入下方图片中的字母或数字,以证明你是人类

看不清楚
必填,最好不要超过500个字符
     ↑返回顶端↑