首页 > 知识&问答
如何构建自己的XML解析器
发布时间:2024-10-23 15:53:52 / 浏览量:
如何构建自己的XML解析器最近,我需要编写一个脚本来解析XML文件并提取各种信息。我确定Perl有很多出色的/卓越的/优异的/杰出的的XML模块,但是我不想经历麻烦,因为它必须找到并安装它(以及它的依赖树)。此外,我确定自己正在处理格式良好的XML,而我所做的只是提取字段,因此不需要进行错误检查,XSLT,XInput和所有其他花哨的东西。我只是将自己的XML解析器滚动了大约100行。这不是幻想。它做出了各种假设,会导致它在转转环境中出现乱码,但是我想我会展示它是如何构建的。
外环
“<”和“>”是保留字符,只能出现在标签的开头和结尾。如果您习惯于使用lex风格的解析器,那么您会想到“啊,所以我应该阅读直到看到“<”或“>”。但是在Perl中,您可以阅读输入,直到看到“>”为止。”,那么您至少知道有一个标签。因此,解析器的轮廓为:
$/=“>”;
而(<>)
{
…
}
注意使用$/设置输入记录分隔符。这意味着如果我们的输入文件是
<?xmlversion=“1.0”吗?>
<地址>
<friend/>
<name>John<nickname>Spike</nickname>Smith</name>
<streetAddress>枫树大道123号。</streetAddress>
</地址>
那么$_的连续值将是
“<?xmlversion=”1.0“?>”
“<地址>”
“<friend/>”
“<名称>”
“约翰<昵称>”
“穗</nickname>”
“***</name>”
“<streetAddress>”
“123MapleAve。</streetAddress>”
“</address>”
换句话说,$_始终以完整的XML标记结尾,该标记可能以其他文本开头。因此,首要任务是将标记与它前面的文本分开:
我的$text;
我的$tag;
m{^(。*)<(。*?)>$}s;
$text=$1;
$tag=$2;
(对于那些不记得的人,m{pattern}等效于/pattern/。)使用“s”修饰符可以使点匹配任何内容,包括换行符。
数据表示
既然我们已经有了一些内容,那么考虑如何表示XML文件中的数据将是一个好主意。XML是一组嵌套的元素,每个元素都有一个名称,可选属性和内容(介于<foo>和</foo>之间的东西)。内容可以是文本数据或其他元素。我选择了一个相当简单的数据结构来保存元素的数据,形式为“((名称,属性,内容……)”。因此,以上示例中的streetAddress元素将变为:
(“streetAddress”,“”,“123MapleAve.”)
该名称元素包含三个项目:字符串“约翰”和“***”,和<昵称>元素。它可以表示为:
(“名称”,
“”,
“百度”,
[“昵称”,
“”,
“长钉”,
],
“树叶”,
)
最终,我们希望将整个文件存储在像这样的树形列表中。
语境
当我们看到像<foo>这样的打开标记时,我们将开始对其进行解析。我们读取的所有内容都会放入该<foo>元素内,直到看到结束标记为止,这时我们已经完成了<foo>标记,并应返回到我们之前处理过的任何元素(包含<foo>元素)。自然地,这暗示了上下文堆栈。
在我的代码中,我做了一些不好的事情。我使用了两个具有相同名称的变量。@context是上下文堆栈(堆栈自然使用数组表示),而$context是引用数组的引用,它引用了我们当前正在查看的任何元素。
在任何给定时刻,$上下文是在到数组的引用(名称,属性,内容...)上述格式和@context是这样的引用到阵列堆叠,描述在其内部的当前元素为元素嵌入式。
也就是说,在某个时候,$context将是
[“昵称”,“”]
那时,@context将是
(
[“地址”,””],
[“name”,“”,“John”]
)
在主循环的这一节中,我们将标记与之前的文本分开了。现在我们看到应该将文本部分附加到$context指向的数组后面(现在不用担心$context的设置方式;我们稍后会担心):
我们的@tree=(“xml”,“”);#解析的XML树
我们的$context=\@tree;#上下文堆栈
我们的@context=();#当前上下文
$/=“>”;
而(<>)
{
最后,如果$_eq“”;#文件结尾
#XML元素前面可能有文本。
我的$text;
我的$tag;
m{^(。*)<(。*?)>$}s;
$text=$1;
$tag=$2;
如果($textne“”)
{
推送@{$context},$text;
}
…
}
解析标签
现在我们只需要处理$tag中的文本。我们需要关注三种类型:
<foo>:一个开始标记
</foo>:结束标记
<foo/>:单例标签
开头标签和单例标签还可以在标签名称之后具有属性:
<名称添加=“2007-07-19”>
<addressformat=“us-postal”category=“personal”>
在我的脚本中,我不必担心属性,因此我选择只将它们存储为未解析的原始字符串。如果它们对您很重要,我建议将它们表示为将每个属性的名称映射为其值的散列。
我们可以使用正则表达式(还有什么?)来解析标签。这很复杂,使用“x”标志是个好主意,它允许我们在正则表达式中嵌入空格(包括换行符)和注释:
如果($tag=〜m{
^(/?)#结束标签
(\S+)#标记名称
#可选属性
(
#<space><attr-name>=“<attr-value>”
(?:\s+
[\w:]+#属性名称
=
\“[^\”]*\“#属性值
)*
)
\s*
(/?)#可选的单斜杠斜杠
$
}xs)
{
#这是常规的<foo>,</foo>或<foo/>标记。
我的$closing=($1ne“”);#这是结束标签吗?
我的$name=$2;#标签名
我的$attrs=$3;#属性分配
我的$singleton=($4ne“”);#这是一个单例标签吗?
…
}
现在,$name是标签的名称,$attrs是(未解析的)属性字符串,$closing和$singleton是布尔型标志,分别告诉我们这是关闭标签还是单例标签。
关闭标签最容易处理。我们已经完成了元素的解析,解析后的数据位于$context中,因此我们要做的就是将其附加到其父元素并弹出@context堆栈以返回到该元素:
如果($结束)
{
#当前上下文的结尾
#将$context附加到@context中的最后一项
推送@{$context[$#context]},$context;
#从堆栈中还原先前的上下文
$context=pop@context;
下一个;
}
这给我们留下了开始标签和单例标签。像<foo/>这样的单例元素等效于<foo></foo>。也就是说,单身人士没有孩子。但是,无论哪种情况,我们都需要启动一个新的上下文:
#将旧的上下文保存到上下文堆栈中
推送@context,$context;
#开始一个新的上下文
$context=[$name,$attrs];
当然,如果我们正在查看一个单例标记,那么我们已经知道它没有内容,应该立即关闭。并且由于我们已经完成了关闭标签的工作,因此我们已经知道如何关闭标签:
如果($singleton)
{
推送@{$context[$#context]},$context;
$context=pop@context;
}
最后,我们可以解决三种标签类型中最困难的一种:打开标签。
对于这些,我们需要将旧的上下文保存到堆栈中并启动一个新的上下文(我们已经完成),然后……实际上,这是我们在此阶段需要做的所有事情。我们无法添加元素的内容,因为我们尚未从输入文件中读取它们。
还记得在顶部,当我们将标签与标签之前的文本分开,并将其附加到@{$context}之后吗?现在,我们了解$context的设置方式:看到开始标记时,我们创建了一个新的匿名数组,以便循环的后续迭代中可以放置其文本。
就是这样!您可以阅读完整的脚本,其中包括用于打印已解析树的&dumptree函数和用于查找元素的&lookup函数。
得到教训
输入记录不必以换行符结尾。如果有更方便的记录终止符或分隔符,请使用$/以使您的生活更轻松的方式读取输入。
Perl的正则表达式功能强大。不必象lex那样费力地尝试先读取“<”,然后读取标记名称,然后再读取属性,然后再读取“>”。只需阅读整个内容,然后在正则表达式内用括号括起来的表达式提取有趣的内容即可。
如果您要解决一个难题,请尝试将其分解为不那么困难的问题,然后将其分解为更简单的子问题。首先要照顾容易的部分。这使您可以进行简化的假设(如果我们知道我们不在看一个结束标记,那么我们就需要保存旧的上下文并开始一个新的上下文)。一旦处理了足够多的简单操作,您可能会发现难题已经简化为一点,您根本不需要做任何事情。