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