在开始编写xml解析器之前我们先来简单介绍一下lex ,yacc。
Lex工具是一种词法分析程序生成器,它可以根据词法规则说明书的要求来生成单词识
别程序,由该程序识别出输入文本中的各个单词。一般可以分为<定义部分><规则部
分><用户子程序部分>。其中规则部分是必须的,定义和用户子程序部分是任选的。
(1)定义部分
定义部分起始于 %{ 符号,终止于 %} 符号,其间可以是包括include语句、声明语句
在内的C语句。这部分跟普通C程序开头没什么区别。
%{
#include "stdio.h"
int linenum;
%}
(2) 规则部分
规则部分起始于"%%"符号,终止于"%%"符号,其间则是词法规则。词法规则由模式和
动作两部分组成。模式部分可以由任意的正则表达式组成,动作部分是由C语言语句组
成,这些语句用来对所匹配的模式进行相应处理。需要注意的是,lex将识别出来的单
词存放在yytext[]字符数据中,因此该数组的内容就代表了所识别出来的单词的内容。
类似yytext这些预定义的变量函数会随着后面内容展开一一介绍。动作部分如果有多
行执行语句,也可以用{}括起来。
%%
title showtitle();
[\n] linenum++;
[0-9]+ printf("Int : %s\n",yytext);
[0-9]*\.[0-9]+ printf("Float : %s\n",yytext);
[a-zA-Z][a-zA-Z0-9]* printf("Var : %s\n",yytext);
[\+\-\*\/\%] printf("Op : %s\n",yytext);
. printf("Unknown : %c\n",yytext[0]);
%%
lex规则部分具有优先级的概念:
1 Lex会选择最长的字符匹配规则。
2 当存在多个规则同事满足时,这时Lex只会选择第一个规则
(3) 用户子程序部分
最后一个%%后面的内容是用户子程序部分,可以包含用C语言编写的子程序,而这些子
程序可以用在前面的动作中,这样就可以达到简化编程的目的。
Lex(Lexical Analyzar) 一些的内部变量和函数
内部预定义变量:
yytext char * 当前匹配的字符串
yyleng int 当前匹配的字符串长度
yyin FILE * lex当前的解析文件,默认为标准输出
yyout FILE * lex解析后的输出文件,默认为标准输入
yylineno int 当前的行数信息
内部预定义宏:
ECHO #define ECHO fwrite(yytext, yyleng, 1, yyout) 也是未匹配字符的默认动作
内部预定义的函数:
int yylex(void) 调用Lex进行词法分析
int yywrap(void) 在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。因此它可以用来解析多个文件。
注意lex库提供一些默认的函数,例如main,yywrap函数等。 当编译时不带-ll选项时,必须自行实现main函数和yywrap等函数。
在linux中假设现在有lex规则文件a.l。运行lex a.l 便自动生成lex.yy.c
cc -o test lex.yy.c -ll 便生成了可执行程序。注意当编译时不带-ll选项时,必须自行实现main函数和yywrap等函数。
如同 Lex 一样, 一个 Yacc 程序也用双百分号分为三段。 它们是:声明、语法规则和 C 代码
用 Yacc 来创建一个编译器包括四个步骤:
1 编写一个 .y 的语法文件(同时说明 C 在这里要进行的动作)。
2 编写一个词法分析器来处理输入并将标记传递给解析器。 这可以使用 Lex 来完成。
3 编写一个函数,通过调用 yyparse() 来开始解析。
4 编写错误处理例程(如 yyerror())。
5 通过yacc工具对.y生代码并编译它。
yacc 必须与lex一起使用,而lex可以单独使用。因为yacc需要使用lex来返回标记。
在linux中假设现在有lex规则文件a.l, a.y。
运行lex a.l 便自动生成lex.yy.c
运行 yacc -d a.y 便自动生成了y.tab.c y.tab.h -d表示生成头文件
cc -o test lex.yy.c y.tab.c -ll -ld 便生成了可执行程序
同样 yacc库也提供了一些默认的函数 例如main,yyerror等,如果不连接yacc库,需要自行实现这些函数。
1、嵌入式动作在规则内变成一个符号,所以它的值($$)像任何其他的符号一样对于规则末端的动作都是可以用的。
如:
thing: A {$$=17;} B C
{printf(%d"",$2);}
在使用嵌入式动作时,如果正在使用“%union”和有类型的符号值,则在引用动作值时就得将值放入尖括号内,例如将它放入嵌入式动作时的$<type>$,以及在规则末端动作中引用它的$<type>3。
2、明确的符号类型
通过在¥和符号数字之间使用或在2个$之间的尖括号内插入类型名,如$<xxx>3或$<xxx>$,YACC允许为符号值引用声明一个明确的类型。3、标记值
YACC语法分析程序中每个符号都有一个相关的值。在语法分析程序中,一定要声明所有具有值的标记的值类型。
%union声明标识符号值的所有可能的C类型。将拷贝到输出文件的YYSTYPE类型的C联合类型声明中。
4、yyerror()提供错误报告例程
5、YYABORT使得语法分析例程yyparse()以一个非零值立即返回,显示失败
6、YYACCEPT:yyparse()以一个零值返回,显示成功。
7、yyparse()语法分析程序的入口点,成功返回零,失败返回非零
8、YACC的冲突,YACC只能向前看一个标记。如下的没有冲突:tgt
start:x B
|y C;
x: A;
y:A;
9、移进/归约冲突
stmt:if '(' cond')' stmt
|if '(' cond ')' stmt ELSE stmt
if (cond) if (cond) stmt ELSE stmt如何理解
1)if (cond){if (cond) stmt ELSE stmt}
2)if (cond){if (cond) stmt }ELSE stmt
设置明显的优先级阻止YACC发布警告
%nonassoc LOWER_THEN_ELSE或%nonassoc THEN
%nonassoc ELSE
ELSE优先级比LOWER_THEN_ELSE高,因为移进优先级必须高于归约优先级
1、继承的属性
属性以标记值开始,即从语法分析树的叶子开始。每次规则被归约时,信息概念性地在语法分析树中上移。并且它的动作根据规则右侧符号值勤合成符号($$)的值。
例如:
declaration:class type namelist;
class: GLOBAL {$$=1;}
|LOCAL {$$=2;}
;
type:REAL {$$=1;}
|INTEGER {$$=2;}
;
namelist:NAME {mksymbol($0,$-1,$1);}
|namelist NAME {mksymbol($0,$-1,$2);}
yacc允许访问它的左侧的内部堆栈上的符号($0,$-1,$-2....),上例中$0指在namelist产生式的符号之前被堆栈的type的值。$0指class。
继承的符号类型必须在动作代码中采用明确的类型提供类型名。
2、文字块
%{
.....
%}
内容被原封不动地拷贝到生成的C源文件中靠近开头的部分。
3、优先级和结合性
结合性:%left左结合,%right右结合,%nonassoc非结合操作
通过声明结合性的顺序决定优先级,声明在后的优先级高
4、左、右递归,左递归更适合于YACC处理
exprlist:exprlist ','expr;
exprlist:expr ','exprlist;
5、规则尾端有明确的优先级:
expr:expr '*' expr
|expr '-' expr
|'-'expr %prec UMINUS;
6、符号类型
YACC在内部将每个值做为包括所有类型的C联合类型来声明。在%union声明中列出所有的类型,YACC将其转变为称为YYSTYPE的联合类型的类型定义。对于值已在动作代码中设置和使用的符号,必须声明它的类型。对非终结符使用%type(如%type<dval> expression),对标记即终结符使用%token,%left,%right,%nonassoc,(如:%token<dval>REAL)。在使用$$、$1等引用值,YACC会自动使用联合的适当字段。
lex & yacc 可以使用的地方特别多,例如语法解析, 设计自定义的编程语言,解析配置文件等等。下面就给出一个简单例子的范例。
假设我们有一套成熟的游戏系统,这套系统服务端跟客户端都通过c编写,通信方式直接采用c struct + socket 方式。
有一天我们的游戏系统需要扩展给第3方公司使用,而第3方公司采用flash开发客户端,他们要求我们提供给他们的接口都采用标准的xml格式协议。
这时我们就自然会想到要编写一个xml 与c struct相互转换的接口程序。
例如我们有一个接口的c struct定义是这样的:
struct request
{
int delay;
int pid;
int threadid;
char ip [20];
int usetime;
}
而我们接收第3方公司开发的协议格式要求如下:
<?xml version="1.0"?>
<request >
<delay>5</delay>
<serviceinfo pid="667" threadid="554" ip="192.168.5.55" usetime="77"/>
</request>
这时我们需要将xml 与c stuct进行相互转换,怎么转换呢,我们可以采用配置文件的方式,
配置好每一个要转换的c struct的转换成xml的格式,或xml转换回struct的格式,我们可以设计上面例子要转换的配置文件如下:
<?xml version="1.0"?>
<request >
<delay>int</delay>
<serviceinfo pid="int" threadid="int" ip="string" usetime="int"/>
</request>
当我们收到一串xml协议时,我们就可以根据我们配置的格式进行每个字段的解析,并按照格式要求转换为c struct。
这时我们在编写程序之前就得先自行解析我们每一个xml的结构,为了方便阐述,xml里面的数据类型只支持 int ,string,double
我们采用lex & yacc 来解析xml方式配置的的结构,具体步骤如下:
1 先编写好需要的数据结构文件: xmlstruct.h
struct XMLNODEATTR
{
char * attr_name; // 属性名字
char * attr_value; // 属性的数据类型
struct XMLNODEATTR* next; //指向下一个属性
} ;
struct XMLNODE
{
struct XMLNODE* parent; //指向父节点
struct XMLNODE* child; //指向第1个孩子节点
struct XMLNODE* next; //指向下一个兄弟节点
struct XMLNODEATTR* attrs; //指向节点属性
short isbasenode; //是否叶子节点 0/否,1/是
char * value_type; //节点的数据类型,当不是叶子节点时,此字段忽略
char * node_name; // 节点名
};
2 编写词法分析文件 parsexml.l
%{
#include "y.tab.h"
#include "xmlstruct.h"
%}
%%
"<?xml version=\"1.0\"?>" {
return XMLHEADER;
}
"int" |
"string" |
"double" { yylval.szname=strdup(yytext); return TYPE; }
[A-Za-z][A-Za-z0-9]* { yylval.szname=strdup(yytext); return NAME;}
"</" |
"/>" { yylval.szname=strdup(yytext); return NODEENDTAG; }
"=" |
"<" |
">" { return yytext[0]; }
[ \t] ; /* ignore white space */
\n ;
\" ;
. { yyerror("unknown char"); return 0; }
%%
2 编写语法分析文件 parsexml.y
%{
#include "xmlstruct.h"
#include "stdio.h"
struct XMLNODE *pCurNode=NULL;
void printXml(struct XMLNODE *pCurrentXml);
%}
%union {
char * szname;
struct XMLNODEATTR * node_attr;
struct XMLNODE * xml_node;
}
%token <szname> NAME
%token <szname> TYPE
%token <szname> NODEENDTAG
%token XMLHEADER
%type <node_attr> attr
%type <node_attr> attr_list
%type <xml_node> node_begin
%type <szname> node_end
%type <xml_node> node_novalue
%type <xml_node> node
%type <xml_node> node_list
%%
xml: xmlhead node { printf("xml\n"); printXml($2); }
;
xmlhead: XMLHEADER { printf("xmlheader\n"); }
;
node_list: node { $$=$1 ; printf("node_list\n"); }
| node node_list { $1->next=$2; $$=$1; printf("node_list1\n"); }
;
node : node_begin TYPE node_end { $$=$1 ;
$$->isbasenode=1;
$$->value_type=strdup($2);
printf("node1 type=%s\n", $2);
}
| node_novalue { $$=$1 ; printf("node2\n"); }
| node_begin node_list node_end { $$=$1 ; $$->child=$2;
pCurNode=$2; while(pCurNode) {pCurNode->parent=$1;pCurNode=pCurNode->next;}
printf("node3\n");
}
;
node_novalue: '<' NAME attr_list NODEENDTAG {
$$=(struct XMLNODE *) malloc(sizeof(struct XMLNODE)) ;
$$->parent=NULL;$$->child=NULL;$$->next=NULL;$$->attrs=NULL;$$->isbasenode=0;$$->value_type=NULL;
$$->node_name=strdup($2) ;
$$->attrs=$3 ;
$$->isbasenode=1;
$$->value_type=NULL;
printf("node_novalue nodename=%s\n", $2);
}
;
node_begin: '<' NAME attr_list '>' { $$=(struct XMLNODE *) malloc(sizeof(struct XMLNODE)) ;
$$->parent=NULL;$$->child=NULL;$$->next=NULL;$$->attrs=NULL;$$->isbasenode=0;$$->value_type=NULL;
$$->node_name=strdup($2) ;
$$->attrs=$3 ;
printf("node begin1=%s\n", $2);
}
| '<' NAME '>' {
$$=(struct XMLNODE *) malloc(sizeof(struct XMLNODE)) ;
$$->parent=NULL;$$->child=NULL;$$->next=NULL;$$->attrs=NULL;$$->isbasenode=0;$$->value_type=NULL;
$$->node_name=strdup($2) ;
printf("node begin2=%s\n", $2);
}
;
node_end: NODEENDTAG NAME '>' { $$=strdup($2); printf("node end=%s\n", $2); }
;
attr_list: attr { $$=$1 ; printf("attr name=%s,type=%s\n",$1->attr_name,$1->attr_value); }
| attr attr_list { $1->next=$2; $$=$1; printf("attr1\n"); }
;
attr: NAME '=' TYPE { $$=(struct XMLNODEATTR *) malloc(sizeof(struct XMLNODEATTR)); $$->next=NULL;
$$->attr_name=strdup($1); $$->attr_value=strdup($3);
printf("%s = %s\n", $1,$3);
}
;
%%
void printXml(struct XMLNODE *pCurrentXml)
{
if(NULL==pCurrentXml)
{
printf("NULL XML! \n");
return ;
}
printf("Node:%p, name=%s,isbasenode=%d ",pCurrentXml,
pCurrentXml->node_name,pCurrentXml->isbasenode );
if(pCurrentXml->value_type !=NULL)
{
printf("type=%s",pCurrentXml->value_type);
}
printf("\n");
if(pCurrentXml->attrs !=NULL)
{
struct XMLNODEATTR* pAttrNext=pCurrentXml->attrs;
printf("Node:name=%s,Attr:",pCurrentXml->node_name);
while( pAttrNext !=NULL )
{
printf("%s=%s,",pAttrNext->attr_name,pAttrNext->attr_value);
pAttrNext=pAttrNext->next;
}
printf("\n");
}
if(pCurrentXml->child !=NULL)
{
struct XMLNODE *pNode=pCurrentXml->child;
while( pNode !=NULL )
{
printXml(pNode);
pNode=pNode->next;
}
}
}
3 编写main : main.c
#include <stdio.h>
extern int yyparse();
extern FILE *yyin;
int nCount=1;
int yywrap()
{
return 1;
}
void yyerror(char *s)
{
fprintf(stderr, "%s\n", s);
}
int main()
{
yyin = fopen("test.xml", "r");
if (yyin == NULL)
{
printf("open file error!\n");
}
else
{
yyparse();
}
return 0;
}
lex parsexml.l 生成 lex.yy.c
yacc -d parsexml.y 生成 y.tab.h y.tab.c
cc -o test lex.yy.c y.tab.c main.c
找一个xml结构体放入test.xml
运行test 无论xml多么复杂,嵌套还非嵌套都会将xml的每一个节点,节点的属性名,属性类型,节点的类型 等结构准确无误地打印输出。