使用lex&yacc實現一個xml解析器

在開始編寫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的每一個節點,節點的屬性名,屬性類型,節點的類型 等結構準確無誤地打印輸出。

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章