系统程序员成长计划-文本处理(一)

系统程序员成长计划-文本处理(一)

状态机(1)

o 有穷状态机的形式定义

有穷状态机是一个五元组 (Q,Σ,δ,q0,F),其中:
Q是一个有穷集合,称为状态集。
Σ是一个有穷集合,称为字母表。
δ: Q xΣQ称为状态转移函数。
q0 是初始状态。
F 是接受状态集。

教科书上是这样定义有穷自动机的,这个形式定义精确的描述了有穷状态机的含义。但是大部分人(包括我自己)第一次看到它时,反复的读上几遍,仍然不知道它在说什么。幸好通过一些实例,我们可以很容易明白有穷状态机的原理。

自动门是一个典型的有穷状态机:

它有“开”和“关”两种状态,这就是它的状态集,也就是上面所说的Q。

人可以从自动门进来或出去,当人进来或出去的时候,自动门会自动打开,如果在规定的时间内没有人进出,自动门会自动关上。人的进来、出去和超时三个事件是自动门的字母表,也就是上面所说的Σ。而自动门在当前状态下,对事件的响应,会引起状态的变化,这就是状态转换函数,也就是上面所说的δ。

自动门刚安装好的时候,我们可以认为它是关上的,所以关闭状态是自动门的初始状态。

在理想情况下,自动门会一直运行,所以它没有接受状态,接受状态集F是空集。

有穷状态机的形式定义很精确,文字描述比较通俗,而图形表示则比较直观。通用建模语言(UML)里的状态图是状态机的常用图形表示方法。简单的状态图包括一些状态,用圆角方框表示,里面有状态的名称。状态之间的转换,用箭头表示,上面可以加转换条件。自动门的状态机可以用下图表示:

有穷状态机很简单,在生活中可以找出很多这样的例子。但是教科书里讲得太复杂了,一会儿证明确定性有穷状态机和非确定性有穷状态机的等价性,一会儿证明正则表达式的正则运算是封闭的,一会儿又来个泵引理。花了很长时间,我才明白这些原理,但两年之后,我又把它们忘得一干二净。

主要原因是工作中没有机会运用它们,这些理论的证明于编程没有太大用处,不过状态机本身却是文本处理利器,由于程序员在很多场合下都是在与文本数据打交道,所以状态机是程序员必备的工具之一。这里我们将一起学习如何用状态机来处理文本数据,后面我们也会提到状态机的其它用途,不过不是本节的重点。

 

 

 

 

状态机(2)

o 用有穷状态机解一道面试题。

刚毕业的时候,我到一家外企面试,面试题里有这样一道题:

统计一篇英文文章里的单词个数。

有多种方法可以解这道题,这里我们选择用有穷状态机来解,做法如下:

先把这篇英文文章读入到一个缓冲区里,让一个指针从缓冲区的头部一直移到缓冲区的尾部,指针会处于两种状态:“单词内”或“单词外”,加上后面提到的初始状态和接受状态,就是有穷状态机的状态集。缓冲区中的字符集合就是有穷状态机的字母表。

如果当前状态为“单词内”,移到指针时,指针指向的字符是非单词字符(如标点和空格),那状态会从“单词内”转换到“单词外”。如果当前状态为“单词外”, 移到指针时,指针指向的字符是单词字符(如字母),那状态会从“单词外”转换到“单词内”。这些转换规则就是状态转换函数。

指针指向缓冲区的头部时是初始状态。

指针指向缓冲区的尾部时是接受状态。

每次当状态从“单词内”转换到“单词外”时,单词计数增加一。
这个有穷状态机的图形表示如下:

下面我们看看程序怎么写:

int count_word(const char* text)
{
	/*定义各种状态,我们不关心接受状态,这里可以不用定义。*/
    enum _State
    {
        STAT_INIT,
        STAT_IN_WORD,
        STAT_OUT_WORD,
    }state = STAT_INIT;

    int count = 0;
    const char* p = text;

    /*在一个循环中,指针从缓冲区头移动缓冲区尾*/
    for(p = text; *p != '/0'; p++)
    {
        switch(state)
        {
            case STAT_INIT:
            {
                if(IS_WORD_CHAR(*p))
                {
		    /*指针指向单词字符,状态转换为单词内*/
                    state = STAT_IN_WORD;
                }
                else
                {
		    /*指针指向非单词字符,状态转换为单词外*/
                    state = STAT_OUT_WORD;
                }
                break;
            }
            case STAT_IN_WORD:
            {
                if(!IS_WORD_CHAR(*p))
                {
                    /*指针指向非单词字符,状态转换为单词外,增加单词计数*/
                    count++;
                    state = STAT_OUT_WORD;
                }
                break;
            }
            case STAT_OUT_WORD:
            {
                if(IS_WORD_CHAR(*p))
                {
                    /*指针指向单词字符,状态转换为单词内*/
                    state = STAT_IN_WORD;
                }
                break;
            }
            default:break;
        }
    }

    if(state == STAT_IN_WORD)
{
        /*如果由单词内进入接受状态,增加单词计数*/
        count++;
    }

    return count;
}

用状态机来解这道题目,思路清晰,程序简单,不易出错。

这道题目只是为了展示一些奇技淫巧,还是有一些实际用处呢?回答这个问题之前,我们先对上面的程序做点扩展,不只是统计单词的个数,而且要分离出里面的每个单词。

int word_segmentation(const char* text, OnWordFunc on_word, void* ctx)
{
    enum _State
    {
        STAT_INIT,
        STAT_IN_WORD,
        STAT_OUT_WORD,
    }state = STAT_INIT;

    int count = 0;
    char* copy_text = strdup(text);
    char* p = copy_text;
    char* word = copy_text;

    for(p = copy_text; *p != '/0'; p++)
    {
        switch(state)
        {
            case STAT_INIT:
            {
                if(IS_WORD_CHAR(*p))
                {
                    word = p;
                    state = STAT_IN_WORD;
                }
                break;
            }
            case STAT_IN_WORD:
            {
                if(!IS_WORD_CHAR(*p))
                {
                    count++;
                    *p = '/0';
                    on_word(ctx, word);
                    state = STAT_OUT_WORD;
                }
                break;
            }
            case STAT_OUT_WORD:
            {
                if(IS_WORD_CHAR(*p))
                {
                    word = p;
                    state = STAT_IN_WORD;
                }
                break;
            }
            default:break;
        }
    }

    if(state == STAT_IN_WORD)
    {
        count++;
        on_word(ctx, word);
    }

    free(copy_text);

    return count;
}

状态机不变,只是在状态转换时,做是事情不一样。这里从“单词内”转换到其它状态时,增加单词计数,并分离出当前的单词。至于拿分离出的单词来做什么,由传入的回调函数决定,比如可以用来统计每个单词出现的频率。

但如果讨论还是限于英文文章,这个程序的意义仍然不大,现在来做进一步扩展。我们考虑的文本不再是英文文章,而是一些文本数据,这些数据由一些分隔符分开,我们把数据称为token,现在我们要把这些token分离出来。

typedef void (*OnTokenFunc)(void* ctx, int index, const char* token);

#define IS_DELIM(c) (strchr(delims, c) != NULL)
int parse_token(const char* text, const char* delims, OnTokenFunc on_token, void* ctx)
{
    enum _State
    {
        STAT_INIT,
        STAT_IN,
        STAT_OUT,
    }state = STAT_INIT;

    int   count     = 0;
    char* copy_text = strdup(text);
    char* p         = copy_text;
    char* token     = copy_text;

    for(p = copy_text; *p != '/0'; p++)
    {
        switch(state)
        {
            case STAT_INIT:
            case STAT_OUT:
            {
                if(!IS_DELIM(*p))
                {
                    token = p;
                    state = STAT_IN;
                }
                break;
            }
            case STAT_IN:
            {
                if(IS_DELIM(*p))
                {
                    *p = '/0';
                    on_token(ctx, count++, token);
                    state = STAT_OUT;
                }
                break;
            }
            default:break;
        }
    }

    if(state == STAT_IN)
    {
        on_token(ctx, count++, token);
    }

    on_token(ctx, -1, NULL);
    free(copy_text);

    return count;
}

用分隔符分隔的文本数据有很多,如:

环境PATH,它由‘:’分开的多个路径组成。如:
/usr/lib/qt-3.3/bin:/usr/kerberos/bin:/backup/tools/jdk1.5.0_18/bin/:/usr/lib/ccache:/usr/local/bin:/bin:/usr/bin:/home/lixianjing/bin

文件名,它由‘/’分开的路径组成。如:
/usr/lib/qt-3.3/bin

URL中的参数,它‘&’分开的多个key/value对组成。
hl=zh-CN&q=limodev&btnG=Google+搜索&meta=&aq=f&oq=

所有这些数据都可以用上面的函数处理,所以这个小函数是颇具实用价值的。

 

 

 

 

 

 

 

状态机(3)

o INI解析器

上面我们看了只有中间两个状态的状态机,现在我们来看一个稍微复杂一点的状态机。

INI文件是Windows下常用的一种配置文件。它由多个分组组成,每个组有多个配置项,每个配置项又由名称和值组成。文件里还可以包含注释,注释通常以‘;’(或‘#’)开始,直到当前行结束。如XP下的win.ini:

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[MCI Extensions.BAK]
aif=MPEGVideo
aifc=MPEGVideo
aiff=MPEGVideo
asf=MPEGVideo
asx=MPEGVideo
au=MPEGVideo
m1v=MPEGVideo
m3u=MPEGVideo
mp2=MPEGVideo
mp2v=MPEGVideo
mp3=MPEGVideo
[annie]
CaptureFile=
VideoDevice=0
AudioDevice=0
FrameRate=333333
UseFrameRate=1
CaptureAudio=1
WantPreview=1
MasterStream=-1
[SciCalc]
layout=0

第一行是注释,后面有fonts、extensions和mci extensions三个空的分组,MCI Extensions.BAK、annie和SciCalc三个分组包含有一个或多个配置项。

对于这样一个文件,我们应该怎样去解析它呢?按照前面的方法,先把数据读入到一个缓冲区中,让一个指针指向缓冲区的头部,然后移动指针,直到指向缓冲区的尾部。在这个过程中,指针可能指向的注释、分组的组名、配置项的名称、配置项的值或者一些如换行符之类的格式信息。

由此,我们可以这样来定义INI的状态机:

状态集合:
1. 分组的组名状态
2. 注释状态
3. 配置项的名称状态
4. 配置项的值状态
5. 空白状态

状态转换函数:
1. 初始状态为“空白”状态。
2. 在“空白”状态下,读入字符‘[’,进入“分组组名”状态。
3. 在“分组组名”状态下,读入字符‘]’,分组组名解析成功,回到“空白”状态。
4. 在“空白”状态下,读入字符‘;’,进入“注释”状态。
5. 在“注释”状态下,读入换行字符,结束“注释”状态,回到“空白”状态。
6. 在“空白”状态下,读入非空白字符,进入“配置项的名称”状态。
7. 在“配置项的名称”状态下,读入字符‘=’, 配置项的名称解析成功,进入“配置项的值”状态。
8. 在“配置项的值”状态下,读入换行字符,配置项的值解析成功,回到“空白”状态。

INI状态机可以用下图来表示:

现在我们来看看程序实现:

static void ini_parse (char* buffer, char comment_char, char delim_char)
{
    char* p = buffer;
    char* group_start = NULL;
    char* key_start   = NULL;
    char* value_start = NULL;
    /*定义INI解析器的状态,初始状态为“空白”状态。*/
    enum _State
    {
        STAT_NONE = 0,
        STAT_GROUP,
        STAT_KEY,
        STAT_VALUE,
        STAT_COMMENT
    }state = STAT_NONE;

    for(p = buffer; *p != '/0'; p++)
    {
        switch(state)
        {
            case STAT_NONE:
            {
                if(*p == '[')
                {
                    /*在“空白”状态下,读入字符‘[’,进入“分组组名”状态。*/
                    state = STAT_GROUP;
                    group_start = p + 1;
                }
                else if(*p == comment_char)
                {
                    /*在“空白”状态下,读入字符‘;’,进入“注释”状态。*/
                    state = STAT_COMMENT;
                }
                else if(!isspace(*p))
                {
                    /*在“空白”状态下,读入非空白字符,进入“配置项的名称”状态。*/
                    state = STAT_KEY;
                    key_start = p;
                }
                break;
            }
            case STAT_GROUP:
            {
                /*在“分组组名”状态下,读入字符‘]’,分组组名解析成功,回到“空白”状态。*/
                if(*p == ']')
                {
                    *p = '/0';
                    state = STAT_NONE;
                    strtrim(group_start);
                    printf("[%s]/n", group_start);
                }
                break;
            }
            case STAT_COMMENT:
            {
                /*在“注释”状态下,读入换行字符,结束“注释”状态,回到“空白”状态。*/
                if(*p == '/n')
                {
                    state = STAT_NONE;
                    break;
                }
                break;
            }
            case STAT_KEY:
            {
                /*在“配置项的名称”状态下,读入字符‘=’, 配置项的名称解析成功,进入“配置项的值”状态。*/
                if(*p == delim_char || (delim_char == ' ' && *p == '/t'))
                {
                    *p = '/0';
                    state = STAT_VALUE;
                    value_start = p + 1;
                }
                break;
            }
            case STAT_VALUE:
            {
                /*在“配置项的值”状态下,读入换行字符,配置项的值解析成功,回到“空白”状态。*/
                if(*p == '/n' || *p == '/r')
                {
                    *p = '/0';
                    state = STAT_NONE;
                    strtrim(key_start);
                    strtrim(value_start);
                    printf("%s%c%s/n", key_start, delim_char, value_start);
                }
                break;
            }
            default:break;
        }
    }

    if(state == STAT_VALUE)
    {
        strtrim(key_start);
        strtrim(value_start);
        printf("%s%c%s/n", key_start, delim_char, value_start);
    }

    return;
}

ini文件有几个变种:
1. 支持默认分组,如果只有一个分组,省略分组的组名,linux下不少配置文件采用这种方式。
2. 注释符号,有的用‘;’,有的用‘#’,前者多用于Windows下,后面多用于Linux下。
3. 名称和值之间的分隔,有的用空格,有的用‘=’,有的‘:’。

不管哪种格式,它们的解析方法是一样的,在上面的程序中,我们使用了comment_char和 delim_char两个参数,分别表示注释符号和分隔符号。

 

 

 

 

 

 

 

 

状态机(4)

XML解析器

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

<?xml version="1.0" encoding="utf-8"?>
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="all/all">
  <!--Created automatically by update-mime-database. DO NOT EDIT!-->
  <comment>all files and folders</comment>
</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” 状态 、“文本” 状态、“注释”状态 和“处理指令”状态结束后,重新回到“空”状态下。

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

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

void xml_parser_parse(XmlParser* thiz, const char* xml)
{
	/*定义状态的枚举值*/
	enum _State
	{
		STAT_NONE,
		STAT_AFTER_LT,
		STAT_START_TAG,
		STAT_END_TAG,
		STAT_TEXT,
		STAT_PRE_COMMENT1,
		STAT_PRE_COMMENT2,
		STAT_COMMENT,
		STAT_PROCESS_INSTRUCTION,
	}state = STAT_NONE;

	thiz->read_ptr = xml;
	/*指针从头移动到尾*/
	for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
	{
		char c = thiz->read_ptr[0];

		switch(state)
		{
			case STAT_NONE:
			{
				if(c == '<')
				{
                    /*在“空”状态下,读入字符‘<’,进入“小于号之后”状态。*/
					xml_parser_reset_buffer(thiz);
					state = STAT_AFTER_LT;
				}
				else if(!isspace(c))
				{
                    /*在“空”状态下,读入非‘<’非空白的字符,进入“文本”状态。*/
					state = STAT_TEXT;
				}
				break;
			}
			case STAT_AFTER_LT:
			{
				if(c == '?')
				{
                    /*在“小于号之后”状态下,读入字符‘?’,进入“处理指令”状态。*/
					state = STAT_PROCESS_INSTRUCTION;
				}
				else if(c == '/')
				{
                    /*在“小于号之后”状态下,读入字符‘/’,进入“结束TAG”状态。*/
					state = STAT_END_TAG;
				}
				else if(c == '!')
				{
                    /*在“小于号之后”状态下,读入字符‘!’,进入“注释前一” 状态*/
					state = STAT_PRE_COMMENT1;
				}
				else if(isalpha(c) || c == '_')
				{
                    /*在“小于号之后”状态下,读入有效的ID字符,进入“起始TAG”状态。*/
					state = STAT_START_TAG;
				}
				else
				{
				}
				break;
			}
			case STAT_START_TAG:
			{
                /*进入子状态*/
				xml_parser_parse_start_tag(thiz);
				state = STAT_NONE;
				break;
			}
			case STAT_END_TAG:
			{
                /*进入子状态*/
				xml_parser_parse_end_tag(thiz);
				state = STAT_NONE;
				break;
			}
			case STAT_PROCESS_INSTRUCTION:
			{
                /*进入子状态*/
				xml_parser_parse_pi(thiz);
				state = STAT_NONE;
				break;
			}
			case STAT_TEXT:
			{
                /*进入子状态*/
				xml_parser_parse_text(thiz);
				state = STAT_NONE;
				break;
			}
			case STAT_PRE_COMMENT1:
			{
				if(c == '-')
				{
                    /*在“注释前一” 状态下,读入字符‘-’, 进入“注释前二” 状态。*/
					state = STAT_PRE_COMMENT2;
				}
				else
				{
				}
				break;
			}
			case STAT_PRE_COMMENT2:
			{
				if(c == '-')
				{
                    /*在“注释前二” 状态下,读入字符‘-’, 进入“注释” 状态。*/
					state = STAT_COMMENT;
				}
				else
				{
				}
			}
			case STAT_COMMENT:
			{
                /*进入子状态*/
				xml_parser_parse_comment(thiz);
				state = STAT_NONE;
				break;
			}
			default:break;
		}

		if(*thiz->read_ptr == '/0')
		{
			break;
		}
	}

	return;
}

解析并没有在此结束,原因是像“起始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”状态的图形表示如下:

下面我们看代码实现:

static void xml_parser_parse_attrs(XmlParser* thiz, char end_char)
{
	int i = 0;
	enum _State
	{
		STAT_PRE_KEY,
		STAT_KEY,
		STAT_PRE_VALUE,
		STAT_VALUE,
		STAT_END,
	}state = STAT_PRE_KEY;

	char value_end = '/"';
	const char* start = thiz->read_ptr;

	thiz->attrs_nr = 0;
	for(; *thiz->read_ptr != '/0' && thiz->attrs_nr < MAX_ATTR_NR; thiz->read_ptr++)
	{
		char c = *thiz->read_ptr;

		switch(state)
		{
			case STAT_PRE_KEY:
			{
				if(c == end_char || c == '>')
				{
                    /*在“属性名之前”状态下,读入字符‘/’或‘>’,进入“结束”状态。*/
					state = STAT_END;
				}
				else if(!isspace(c))
				{
                    /*在“属性名之前”状态下,读入其它非空白字符,进入“属性名”状态。*/
					state = STAT_KEY;
					start = thiz->read_ptr;
				}
			}
			case STAT_KEY:
			{
				if(c == '=')
				{
                    /*在“属性名”状态下,读入字符‘=’,进入“属性值之前”状态。*/
					thiz->attrs[thiz->attrs_nr++] = (char*)xml_parser_strdup(thiz, start, thiz->read_ptr - start);
					state = STAT_PRE_VALUE;
				}

				break;
			}
			case STAT_PRE_VALUE:
			{
                /*在“属性值之前”状态下,读入字符‘“’,进入“属性值”状态。*/
				if(c == '/"' || c == '/'')
				{
					state = STAT_VALUE;
					value_end = c;
					start = thiz->read_ptr + 1;
				}
				break;
			}
			case STAT_VALUE:
			{
                /*在“属性值”状态下,读入字符‘”’,成功解析属性名和属性值,回到“属性名之前”状态。*/
				if(c == value_end)
				{
					thiz->attrs[thiz->attrs_nr++] = (char*)xml_parser_strdup(thiz, start, thiz->read_ptr - start);
					state = STAT_PRE_KEY;
				}
			}
			default:break;
		}

		if(state == STAT_END)
		{
			break;
		}
	}

	for(i = 0; i < thiz->attrs_nr; i++)
	{
		thiz->attrs[i] = thiz->buffer + (size_t)(thiz->attrs[i]);
	}
	thiz->attrs[thiz->attrs_nr] = NULL;

	return;
}

记得在XML里,单引号和双引号都可以用来界定属性值,所以上面对此做了特殊处理。

static void xml_parser_parse_start_tag(XmlParser* thiz)
{
	enum _State
	{
		STAT_NAME,
		STAT_ATTR,
		STAT_END,
	}state = STAT_NAME;

	char* tag_name = NULL;
	const char* start = thiz->read_ptr - 1;

	for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
	{
		char c = *thiz->read_ptr;

		switch(state)
		{
			case STAT_NAME:
			{
                /*在“TAG名称”状态下,读入空白字符,属性子状态。*/
                /*在“TAG名称”状态下,读入字符‘/’或‘>’,进入“结束”状态。*/
				if(isspace(c) || c == '>' || c == '/')
				{
					state = (c != '>' && c != '/') ? STAT_ATTR : STAT_END;
				}
				break;
			}
			case STAT_ATTR:
			{
                /*进入“属性”子状态*/
				xml_parser_parse_attrs(thiz, '/');
				state = STAT_END;

				break;
			}
			default:break;
		}

		if(state == STAT_END)
		{
			break;
		}
	}

	for(; *thiz->read_ptr != '>' && *thiz->read_ptr != '/0'; thiz->read_ptr++);

	return;
}

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

static void xml_parser_parse_pi(XmlParser* thiz)
{
	enum _State
	{
		STAT_NAME,
		STAT_ATTR,
		STAT_END
	}state = STAT_NAME;

	char* tag_name = NULL;
	const char* start = thiz->read_ptr;

	for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
	{
		char c = *thiz->read_ptr;

		switch(state)
		{
			case STAT_NAME:
			{
                /*在“TAG名称”状态下,读入空白字符,属性子状态。*/
                /*在“TAG名称”状态下,‘>’,进入“结束”状态。*/
				if(isspace(c) || c == '>')
				{
					state = c != '>' ? STAT_ATTR : STAT_END;
				}

				break;
			}
			case STAT_ATTR:
			{
                /*进入“属性”子状态*/
				xml_parser_parse_attrs(thiz, '?');
				state = STAT_END;
				break;
			}
			default:break;
		}

		if(state == STAT_END)
		{
			break;
		}
	}

	tag_name = thiz->buffer + (size_t)tag_name;

	for(; *thiz->read_ptr != '>' && *thiz->read_ptr != '/0'; thiz->read_ptr++);

	return;
}

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

“注释”子状态的处理:

static void xml_parser_parse_comment(XmlParser* thiz)
{
    enum _State
    {
        STAT_COMMENT,
        STAT_MINUS1,
        STAT_MINUS2,
    }state = STAT_COMMENT;

    const char* start = ++thiz->read_ptr;
    for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
    {
        char c = *thiz->read_ptr;

        switch(state)
        {
            case STAT_COMMENT:
            {
                /*在“注释”状态下,读入‘-’,进入“减号一”状态。*/
                if(c == '-')
                {
                    state = STAT_MINUS1;
                }
                break;
            }
            case STAT_MINUS1:
            {
                if(c == '-')
                {
                     /*在“减号一”状态下,读入‘-’,进入“减号二”状态。*/
                    state = STAT_MINUS2;
                }
                else
                {
                    state = STAT_COMMENT;
                }
                break;
            }
            case STAT_MINUS2:
            {
                if(c == '>')
                {
                    /*在“减号二”状态下,读入‘>’,结束解析。*/
                    return;
                }
                else
                {
                    state = STAT_COMMENT;
                }
            }
            default:break;
        }
    }

    return;
}

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

static void xml_parser_parse_end_tag(XmlParser* thiz)
{
    char* tag_name = NULL;
    const char* start = thiz->read_ptr;
    for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
{
    /*读入‘>’,结束解析。*/
        if(*thiz->read_ptr == '>')
        {
            break;
        }
    }

    return;
}

“文本”子状态的处理:

static void xml_parser_parse_text(XmlParser* thiz)
{
    const char* start = thiz->read_ptr - 1;
    for(; *thiz->read_ptr != '/0'; thiz->read_ptr++)
    {
        char c = *thiz->read_ptr;
        /*读入‘>’,结束解析。*/
        if(c == '<')
        {
            if(thiz->read_ptr > start)
            {
            }
            thiz->read_ptr--;
            return;
        }
        else if(c == '&')
        {
           /*读入‘&’,进入实体(entity)解析子状态。*/
            xml_parser_parse_entity(thiz);
        }
    }

    return;
}

实体(entity)子状态比较简单,这里不做进一步分析了,留给读者做练习吧。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值