使用lex&yacc实现一个xml解析器 .

来源:互联网 发布:越南女孩 知乎 编辑:程序博客网 时间:2024/05/16 14:11

在开始编写xml解析器之前我们先来简单介绍一下lex ,yacc。

 

Lex:

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等函数。

 

Yacc


如同 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库,需要自行实现这些函数。

 

 

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实现一个xml解析器

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的每一个节点,节点的属性名,属性类型,节点的类型 等结构准确无误地打印输出。

原创粉丝点击