FSM有限状态机运用分析系列一 —— XML解析器

XML(Extensible Markup Language)即可扩展标记语言,也是一种常用的数据文件格式。相对于INI来说,它要复杂得多,INI只能保存线性结构的数据,而XML可以保存树形结构的数据。先看下面的例子:

1 <?xml version="1.0" encoding="utf-8"?>
2 <mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="all/all">
3 <!--Created automatically by update-mime-database. DO NOT EDIT!-->
4 <comment>all files and folders</comment>
5 </mime-type>

第一行称为处理指令(PI),是给解析器用的。这里告诉解析器,当前的XML文件遵循XML 1.0规范,文件内容用UTF-8编码。

第二行是一个起始TAG,TAG的名称为mime-type。它有两个属性,第一个属性的名称为xmlns,值为 http://www.freedesktop.org/standards/shared-mime-info。第二个属性的名称为type,值为 all/all。

第三行是一个注释。

第四行包括一个起始TAG,一段文本和结束TAG。

第五行是一个结束TAG。

XML本身的格式不是本文的重点,我们不详细讨论了。这里的重点是如何用状态机解析格式复杂的数据。

按照前面的方法,先把数据读入到一个缓冲区中,让一个指针指向缓冲区的头部,然后移动指针,直到指向缓冲区的尾部。在这个过程中,指针可能指向:起始TAG,结束TAG,注释,处理指令和文本。由此我们定义出状态机的主要状态:

1. 起始TAG状态
2. 结束TAG状态
3. 注释状态
4. 处理指令状态
5. 文本状态

由于起始TAG、结束TAG、注释和处理指令都在字符‘<’和‘>’之间,所以当读入字符‘<’时,我们还无法知道当前的状态,为了便于处理,我们引入一个中间状态,称为“小于号之后”的状态。在读入字符‘<’和‘!’之后,还要读入两个‘-’,才能确定进入注释状态,为了便于处理,再引入两个中间状态“注释前一”和“注释前二”。再引入一个“空”状态,表示不在上述任何状态中。

状态转换函数:
1. 在“空”状态下,读入字符‘<’,进入“小于号之后”状态。
2. 在“空”状态下,读入非‘<’非空白的字符,进入“文本”状态。
3. 在“小于号之后”状态下,读入字符‘!’,进入“注释前一” 状态。
4. 在“小于号之后”状态下,读入字符‘?’,进入“处理指令”状态。
5. 在“小于号之后”状态下,读入字符‘/’,进入“结束TAG”状态。
6. 在“小于号之后”状态下,读入有效的ID字符,进入“起始TAG”状态。
7. 在“注释前一” 状态下,读入字符‘-’, 进入“注释前二” 状态。
8. 在“注释前二” 状态下,读入字符‘-’, 进入“注释” 状态。
9. 在 “起始TAG” 状态、“结束TAG” 状态 、“文本” 状态、“注释”状态 和“处理指令”状态结束后,重新回到“空”状态下。

这个状态机的图形表示如下:

 
下面我们来看看代码实现:

  1 void xml_parser_parse(XmlParser* thiz, const char* xml)
  2 {
  3     /*定义状态的枚举值*/
  4     enum _State
  5     {
  6         STAT_NONE,
  7         STAT_AFTER_LT,
  8         STAT_START_TAG,
  9         STAT_END_TAG,
 10         STAT_TEXT,
 11         STAT_PRE_COMMENT1,
 12         STAT_PRE_COMMENT2,
 13         STAT_COMMENT,
 14         STAT_PROCESS_INSTRUCTION,
 15     }state = STAT_NONE;
 16 
 17     thiz->read_ptr = xml;
 18     /*指针从头移动到尾*/
 19     for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
 20     {
 21         char c = thiz->read_ptr[0];
 22 
 23         switch(state)
 24         {
 25             case STAT_NONE:
 26             {
 27                 if(c == '<')
 28                 {
 29                     /*在“空”状态下,读入字符‘<’,进入“小于号之后”状态。*/
 30                     xml_parser_reset_buffer(thiz);
 31                     state = STAT_AFTER_LT;
 32                 }
 33                 else if(!isspace(c))
 34                 {
 35                     /*在“空”状态下,读入非‘<’非空白的字符,进入“文本”状态。*/
 36                     state = STAT_TEXT;
 37                 }
 38                 break;
 39             }
 40             case STAT_AFTER_LT:
 41             {
 42                 if(c == '?')
 43                 {
 44                     /*在“小于号之后”状态下,读入字符‘?’,进入“处理指令”状态。*/
 45                     state = STAT_PROCESS_INSTRUCTION;
 46                 }
 47                 else if(c == '/')
 48                 {
 49                     /*在“小于号之后”状态下,读入字符‘/’,进入“结束TAG”状态。*/
 50                     state = STAT_END_TAG;
 51                 }
 52                 else if(c == '!')
 53                 {
 54                     /*在“小于号之后”状态下,读入字符‘!’,进入“注释前一” 状态*/
 55                     state = STAT_PRE_COMMENT1;
 56                 }
 57                 else if(isalpha(c) || c == '_')
 58                 {
 59                     /*在“小于号之后”状态下,读入有效的ID字符,进入“起始TAG”状态。*/
 60                     state = STAT_START_TAG;
 61                 }
 62                 else
 63                 {
 64                 }
 65                 break;
 66             }
 67             case STAT_START_TAG:
 68             {
 69                 /*进入子状态*/
 70                 xml_parser_parse_start_tag(thiz);
 71                 state = STAT_NONE;
 72                 break;
 73             }
 74             case STAT_END_TAG:
 75             {
 76                 /*进入子状态*/
 77                 xml_parser_parse_end_tag(thiz);
 78                 state = STAT_NONE;
 79                 break;
 80             }
 81             case STAT_PROCESS_INSTRUCTION:
 82             {
 83                 /*进入子状态*/
 84                 xml_parser_parse_pi(thiz);
 85                 state = STAT_NONE;
 86                 break;
 87             }
 88             case STAT_TEXT:
 89             {
 90                 /*进入子状态*/
 91                 xml_parser_parse_text(thiz);
 92                 state = STAT_NONE;
 93                 break;
 94             }
 95             case STAT_PRE_COMMENT1:
 96             {
 97                 if(c == '-')
 98                 {
 99                     /*在“注释前一” 状态下,读入字符‘-’, 进入“注释前二” 状态。*/
100                     state = STAT_PRE_COMMENT2;
101                 }
102                 else
103                 {
104                 }
105                 break;
106             }
107             case STAT_PRE_COMMENT2:
108             {
109                 if(c == '-')
110                 {
111                       /*在“注释前二” 状态下,读入字符‘-’, 进入“注释” 状态。*/
112                     state = STAT_COMMENT;
113                 }
114                 else
115                 {
116                 }
117             }
118             case STAT_COMMENT:
119             {
120                 /*进入子状态*/
121                 xml_parser_parse_comment(thiz);
122                 state = STAT_NONE;
123                 break;
124             }
125             default:break;
126         }
127 
128         if(*thiz->read_ptr == '/0')
129         {
130             break;
131         }
132     }
133     return;
134 }

解析并没有在此结束,原因是像“起始TAG”状态和“处理指令”状态等,它们不是原子的,内部还包含一些子状态,如TAG名称,属性名和属性值等,它们需要进一步分解。在考虑子状态时,我们可以忘掉它所处的上下文,只考虑子状态本身,这样问题会得到简化。下面看一下起始TAG的状态机。

假设我们要解析下面这样一个起始TAG:
<mime-type xmlns=”http://www.freedesktop.org/standards/shared-mime-info” type=”all/all”>

我们应该怎样去做呢?还是按前面的方法,让一个指针指向缓冲区的头部,然后移动指针,直到指向缓冲区的尾部。在这个过程中,指针可能指向,TAG名称,属性名和属性值。由此我们可以定义出状态机的主要状态:

1. “TAG名称”状态
2. “属性名”状态
3. “属性值”状态

为了方便处理,再引两个中间状态,“属性名之前”状态和“属性值之前”状态。

状态转换函数:

初始状态为“TAG名称”状态
1. 在“TAG名称”状态下,读入空白字符,进入“属性名之前”状态。
2. 在“TAG名称”状态下,读入字符‘/’或‘>’,进入“结束”状态。
3. 在“属性名之前”状态下,读入其它非空白字符,进入“属性名”状态。
4. 在“属性名”状态下,读入字符‘=’,进入“属性值之前”状态。
5. 在“属性值之前”状态下,读入字符‘“’,进入“属性值”状态。
6. 在“属性值”状态下,读入字符‘”’,成功解析属性名和属性值,回到“属性名之前”状态。
7. 在“属性名之前”状态下,读入字符‘/’或‘>’,进入“结束”状态。

由于处理指令(PI)里也包含了属性状态,为了重用属性解析的功能,我们把属性的状态再提取为一个子状态。这样,“起始TAG”状态的图形表示如下:


下面我们看代码实现:

  1 static void xml_parser_parse_attrs(XmlParser* thiz, char end_char)
  2 {
  3      int i = 0;
  4     enum _State
  5     {
  6         STAT_PRE_KEY,
  7         STAT_KEY,
  8         STAT_PRE_VALUE,
  9         STAT_VALUE,
 10         STAT_END,
 11     }state = STAT_PRE_KEY;
 12 
 13     char value_end = '/"';
 14     const char* start = thiz->read_ptr;
 15 
 16     thiz->attrs_nr = 0;
 17     for(; *thiz->read_ptr != '/0' && thiz->attrs_nr < MAX_ATTR_NR; thiz->read_ptr++)
 18     {
 19         char c = *thiz->read_ptr;
 20 
 21         switch(state)
 22         {
 23             case STAT_PRE_KEY:
 24             {
 25                 if(c == end_char || c == '>')
 26                 {
 27                     /*在“属性名之前”状态下,读入字符‘/’或‘>’,进入“结束”状态。*/
 28                     state = STAT_END;
 29                 }
 30                 else if(!isspace(c))
 31                 {
 32                     /*在“属性名之前”状态下,读入其它非空白字符,进入“属性名”状态。*/
 33                     state = STAT_KEY;
 34                     start = thiz->read_ptr;
 35                 }
 36             }
 37             case STAT_KEY:
 38             {
 39                 if(c == '=')
 40                 {
 41                     /*在“属性名”状态下,读入字符‘=’,进入“属性值之前”状态。*/
 42                     thiz->attrs[thiz->attrs_nr++] = (char*)xml_parser_strdup(thiz, start, thiz->read_ptr - start);
 43                     state = STAT_PRE_VALUE;
 44                 }
 45 
 46                 break;
 47             }
 48             case STAT_PRE_VALUE:
 49             {
 50                 /*在“属性值之前”状态下,读入字符‘“’,进入“属性值”状态。*/
 51                 if(c == '/"' || c == '/'')
 52                 {
 53                     state = STAT_VALUE;
 54                     value_end = c;
 55                     start = thiz->read_ptr + 1;
 56                 }
 57                 break;
 58             }
 59             case STAT_VALUE:
 60             {
 61                 /*在“属性值”状态下,读入字符‘”’,成功解析属性名和属性值,回到“属性名之前”状态。*/
 62                 if(c == value_end)
 63                 {
 64                     thiz->attrs[thiz->attrs_nr++] = (char*)xml_parser_strdup(thiz, start, thiz->read_ptr - start);
 65                     state = STAT_PRE_KEY;
 66                 }
 67             }
 68             default:break;
 69         }
 70 
 71         if(state == STAT_END)
 72         {
 73             break;
 74         }
 75     }
 76 
 77     for(i = 0; i < thiz->attrs_nr; i++)
 78     {
 79           thiz->attrs[i] = thiz->buffer + (size_t)(thiz->attrs[i]);
 80     }
 81     thiz->attrs[thiz->attrs_nr] = NULL;
 82 
 83     return;
 84 }
 85 
 86 记得在XML里,单引号和双引号都可以用来界定属性值,所以上面对此做了特殊处理。
 87 
 88 static void xml_parser_parse_start_tag(XmlParser* thiz)
 89 {
 90       enum _State
 91       {
 92           STAT_NAME,
 93           STAT_ATTR,
 94           STAT_END,
 95       }state = STAT_NAME;
 96 
 97       char* tag_name = NULL;
 98       const char* start = thiz->read_ptr - 1;
 99 
100       for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
101       {
102           char c = *thiz->read_ptr;
103 
104           switch(state)
105           {
106               case STAT_NAME:
107               {
108                   /*在“TAG名称”状态下,读入空白字符,属性子状态。*/
109                   /*在“TAG名称”状态下,读入字符‘/’或‘>’,进入“结束”状态。*/
110                   if(isspace(c) || c == '>' || c == '/')
111                   {
112                       state = (c != '>' && c != '/') ? STAT_ATTR : STAT_END;
113                   }
114                   break;
115               }
116               case STAT_ATTR:
117               {
118                     /*进入“属性”子状态*/
119                   xml_parser_parse_attrs(thiz, '/');
120                   state = STAT_END;
121 
122                   break;
123               }
124               default:break;
125           }
126 
127           if(state == STAT_END)
128           {
129               break;
130           }
131       }
132 
133       for(; *thiz->read_ptr != '>' && *thiz->read_ptr != '/0'; thiz->read_ptr++);
134 
135       return;
136 }

处理指令的解析和起始TAG的解析基本上是一样的,这里只是看一下代码:

 1 static void xml_parser_parse_pi(XmlParser* thiz)
 2 {
 3     enum _State
 4     {
 5       STAT_NAME,
 6       STAT_ATTR,
 7       STAT_END
 8     }state = STAT_NAME;
 9 
10     char* tag_name = NULL;
11     const char* start = thiz->read_ptr;
12 
13     for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
14     {
15         char c = *thiz->read_ptr;
16 
17         switch(state)
18         {
19             case STAT_NAME:
20             {
21                 /*在“TAG名称”状态下,读入空白字符,属性子状态。*/
22                 /*在“TAG名称”状态下,‘>’,进入“结束”状态。*/
23                 if(isspace(c) || c == '>')
24                 {
25                     state = c != '>' ? STAT_ATTR : STAT_END;
26                 }
27 
28                 break;
29             }
30             case STAT_ATTR:
31             {
32                 /*进入“属性”子状态*/
33                 xml_parser_parse_attrs(thiz, '?');
34                 state = STAT_END;
35                 break;
36             }
37             default:break;
38          }
39 
40          if(state == STAT_END)
41          {
42             break;
43          }
44       }
45 
46       tag_name = thiz->buffer + (size_t)tag_name;
47 
48       for(; *thiz->read_ptr != '>' && *thiz->read_ptr != '/0'; thiz->read_ptr++);
49 
50       return;
51 }

注释,结束TAG和文本的解析非常简单,这里结合代码看看就行了:

“注释”子状态的处理:

 1 static void xml_parser_parse_comment(XmlParser* thiz)
 2 {
 3     enum _State
 4     {
 5       STAT_COMMENT,
 6       STAT_MINUS1,
 7       STAT_MINUS2,
 8     }state = STAT_COMMENT;
 9 
10     const char* start = ++thiz->read_ptr;
11     for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
12     {
13         char c = *thiz->read_ptr;
14 
15         switch(state)
16         {
17             case STAT_COMMENT:
18             {
19                 /*在“注释”状态下,读入‘-’,进入“减号一”状态。*/
20                 if(c == '-')
21                 {
22                     state = STAT_MINUS1;
23                 }
24                 break;
25             }
26             case STAT_MINUS1:
27             {
28                 if(c == '-')
29                 {
30                     /*在“减号一”状态下,读入‘-’,进入“减号二”状态。*/
31                     state = STAT_MINUS2;
32                 }
33                 else
34                 {
35                     state = STAT_COMMENT;
36                 }
37                 break;
38             }
39             case STAT_MINUS2:
40             {
41                 if(c == '>')
42                 {
43                     /*在“减号二”状态下,读入‘>’,结束解析。*/
44                     return;
45                 }
46                 else
47                 {
48                     state = STAT_COMMENT;
49                 }
50             }
51             default:break;
52         }
53     }
54 
55     return;
56 }

“结束TAG”子状态的处理:

 1 static void xml_parser_parse_end_tag(XmlParser* thiz)
 2 {
 3       char* tag_name = NULL;
 4       const char* start = thiz->read_ptr;
 5       for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
 6       {
 7         /*读入‘>’,结束解析。*/
 8         if(*thiz->read_ptr == '>')
 9         {
10           break;
11         }
12       }
13 
14       return;
15 }

“文本”子状态的处理:

 1 static void xml_parser_parse_text(XmlParser* thiz)
 2 {
 3   const char* start = thiz->read_ptr - 1;
 4   for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
 5   {
 6     char c = *thiz->read_ptr;
 7     /*读入‘>’,结束解析。*/
 8     if(c == '<')
 9     {
10       if(thiz->read_ptr > start)
11       {
12       }
13       thiz->read_ptr--;
14       return;
15     }
16     else if(c == '&')
17     {
18       /*读入‘&’,进入实体(entity)解析子状态。*/
19       xml_parser_parse_entity(thiz);
20     }
21   }
22 
23   return;
24 }
原文地址:https://www.cnblogs.com/dylan2011/p/2688536.html