作者:Norman Walsh 阿霏 译
如果你是一个XML编程新手,或许你想知道从何做起。使用XML来存储结构数据的益处是非常明显的,但是一旦你将一些数据存入XML文件中,你如何将它们取出来呢?在这篇文章中,我们尝试了几种选择并且给出了用Perl语言实现的具体解决方案(用Python、Java、C++和其它你喜欢的编程语言与此是一样的)。 我们将使用XML构建一个简单的文本处理程序来存储用户的偏爱设置和其它配置数据,这类事情在以前都是使用纯文本文件来完成的,这对于读者是比较熟悉的。
读取配置文件
许多应用程序都需要储存用户的偏爱设置和其它种类的配置信息,一个最普通的做法就是使用文本文件,几乎有多少个创建这些文本文件的程序员就有多少个文件的格式约定,但是有一个常见的风格就是Windows
INI文件格式。这个格式非常简单:文件被分成几个命名的部分,在每个部分中,名字与值是通过赋值关联起来的。 很多程序员都曾经写过代码来处理这样的文本文件,在许多编程语言中,这是很简单的,但也有些编程语言处理它们是比较困难的。不过算法总是一样的:循环读入文件的每一行,解析每次取得的字符串。在Windows系统中,有方便的函数来访问INI格式文件中的数据:GetProfileString()和SetProfileString()。
[Section1] name1=value1 name2=value2 [section2] someothername=someothervalue
例1.
一个简单的INI文件
在这篇文章中,我们建议一个XML版本的配置文件格式(见例2),并且使用Perl语言尝试几种从这种格式的文件中获取信息的方法。最终,我们将引入我们自己版本的getProfileString()和setProfileString()函数,它们用来提供对于XML格式配置文件的透明访问。 例2给出了例1中的一个简单配置文件的XML版本: <configuration-file> <section
name="section1"> <entry name="name1"
value="value1"/> <entry name="name2"
value="value2"/> </section> <section
name="section2"> <entry name="someothername"
value="someothervalue"/> </section> </configuration-file> 例2.
XML实现的一个简单INI文件 这个文件格式的DTD如图1:
图1.一个XML INI文件格式的DTD
这个DTD主要是为指定的文件格式服务,在实践中,我们将XML
INI文件当作简单的格式完好的文档。
简单性的假设
这篇文章中所提供的例子对于配置文件做了一些简单性的假设: 没有重复的部分(section)名。 使用的语言能够透明的解决编码问题。 配置文件已经适当的结构化了,我们无需担心其有效性,并且我们试图忽略文件的一些小变动(如:额外的属性)。
XML文件不是成行的文本文件 你首先尝试处理这种文件的方法可能就像处理非XML版本的一样--每次读一行,特别是比较匆忙的时候。这是一个处理XML数据非常脆弱的方法,换行在XML
INI文件中并不重要,像例4一样组织的文件也是合法的。 <configuration-file><section
name="section1"> <entry name="name1" value="value1"/><entry
name="name2" value="value2"/> </section><section
name="section2" > <entry name="someothername"
value="someothervalue"/> </section> </configuration-file> 例4
另一种换行的INI文件 对于这个文件,如果你想重新研制一个XML文件语法分析器,每次处理一行,那么就是白费力气了。
使用一般表达式
XML故意被设计成可以使用"底层技术"解决方案有效地被处理,在像Perl一样的特定文本处理器中都使用一般表达式。正如我们刚才所看到的,你不能一次一行地来处理文件,但是如果你的数据文件很小可以整个加载到内存中,那么你可以用一般表达式来解析这些数据。例5展示了使用一般表达式的getProfileString()版本: 1
|sub getProfileString { | my ($xmlfile, $section, $variable,
$default) = @_; | local (*F, $_); | 5 | open (F,
$xmlfile) || return undef; # Load the document | read (F, $_,
-s $xmlfile); | close (F); | | while
(/(<section[^>]*>)(.*?)<\/section\s*>/s){# Process
sections 10| my($sectstart) = $1; # Save start tag |
my($sectdata) = $2; | $_ = $'; | | if ($sectstart
=~ /name=([\"\'])$section\1/s) { # Process entrys 15| while
($sectdata =~ /<entry[^>]*?\/>/) { | my($entry) =
$&; | $sectdata = $'; | if ($entry =~
/name=([\"\'])$variable\1/s) { | if($entry =~
/value=([\"\'])(.*?)\1/s) { 20| return $2; | } else
{ | return ""; | } | } 25| } | return
$default; | } | } | 30| return
$default; |} 这段代码有四个重要特点: 第5行:整个文档被加载进内存。 这样避免了与一行一行地读XML文件相关的问题,并允许我们匹配那些包含在新行中难以预料位置的元素。 第9行:文档每次被处理一部分。 一般表达式/(<section[^>]*>)(.*?)<\/section\s*>/s
匹配整个部分,从起始标记到终止标记。值得注意 的是起始标记和终止标记是如何考虑空格可能性的,并且/s修正符是如何允许Perl跨行匹配的,如果< <section>
标记嵌套,这段代码将不能正常工作)。 匹配比较大的一般表达式有性能的冲击,尽管对于这样的小文件来说冲击并不重要,如果你想使用这个方 法来处理大文件,性能的冲击就值得考虑了。
关于一般表达式的详细讨论,参见Jeffrey Friedl的 Mastering Regular
Expressions。 第10行:每一部分的起始标记存于一个变量中。 通过将起始标记存在变量中这一方法,我们在以后使用属性时随时查看它。既然属性可以以任何次序出现 ,那么在某一步的快速匹配都可以导致形成一个非常有效的一般表达式。 第15行:一旦我们用名字找到了我们所寻找的部分,我们就在这部分中对每个实体(entry)进行相似的解析。
使用解析器来做这个工作 使用一般表达式来处理XML文件只能用于一些简单的情况,但是用这种方法处理大部分的XML文档都是很困难的。 这个任务应该留给一个特殊的工具--XML解析器来作,幸好大部分计算机语言都有了XML解析器,对于Perl来说比较流行的XML解析器是Clark
Cooper的XML::Parser模块,现在是2.22版本。 XML::Parser建构在James
Clark的expat之上,是一个基于事件的解析器。这就意味着你只需告诉解析器你所关注的部分,然后让解析器来完成工作。每次解析器遇到你注册进来的受关注的部分,它都会回调你的代码。 以下是另一个getProfileString()版本,这次使用了解析器来作这个复杂的工作: 1
|useXML::Parser; # Use the parser module | |my
$target_section = ""; # Setup global variables |my
$target_entry = ""; 5 |my $current_section = ""; |my
$entry_value = ""; | |sub getProfileString
{ |my($cfgfile, $section, $variable, $default) = @_; 10|
my $parser = new XML::Parser(ErrorContext => 2);# Create a
parser | $parser->setHandlers(Start =>
\&start_handler);#Report start-tag events | |
$target_section = $section; | $target_entry =
$variable; 15| $current_section = ""; | $entry_value =
$default; | | $parser->parsefile($cfgfile); # Run the
parser | 20| return
$entry_value; |} | |sub start_handler { # The
start-tag handler | my $parser_context = shift; 25| my
$element = shift; | my %attr = @_; | | if
($element eq 'section') { | $current_section =
$attr{'name'}; 30| } elsif ($element eq 'entry' &&
$current_section eq $target_section) { | if ($attr{'name'} eq
$target_entry) { | $entry_value = $attr{'value'}; |
$parser_context->finish(); | } 35| } |}
让我们仔细地来看一下发生了什么。使用解析器需要以下几步: 第1行:告诉Perl我们所使用的解析器模块。 第3行:设置一些包内的全局变量,使用这些变量来跟踪我们所看到的东西。这并不太优雅,但使这个例子相当简单。 第10行:XML::Parser模块将解析器展示成一个对象,这一行创建了解析器对象新的实例(ErrorContext告诉解析器如果这个XML文档不是格式完好的,解析器应该显示多少行)。 第11行:下一步告诉解析器我们关注什么事件。在这个例子中,我们只关心起始标记,所以我们告诉解析器每次遇到起始标记就调用start_handler函数。 第18行:建立起我们所关注的事件后,运行解析器,这个函数直到整个文档都被解析完之后返回。 第23行:每次解析器遇到一个起始标记,它都调用start_handler函数,传给它几个参数:解析器上下文(提供访问解析器的方法,你可以忽略)、元素名和一个保存属性的相关数组。
结论 在这篇文章中,我们研究了几个处理XML文档的方法并演示了使用XML解析器作这个复杂工作的益处。将这些例子放在手边,我希望你们已经准备好了如何利用这些例子处理你们的下一个XML项目了。
|