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 }