IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

XML 问题 #14: 超越 DOM、SAX 和 XSLT 的限制

用于 XML 的 HaXml 编程模型

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

David Mertz, 博士 (mertz@gnosis.cx), 魔术师, Gnosis Software, Inc.

2001 年 10 月 01 日

处理 XML 数据时,可以考虑采用 Haskell 来替代 DOM、SAX 或 XSLT。库 HaXml 中将 XML 文档的表示创建成函数性语言 Haskell 中原始的递归数据结构。在对这些“数据化”的 XML 文档进行操作时,HaXml 采用了一套强大的高阶函数。 HaXml 的许多技术比如 DOM、SAX 或 XSLT 等常见技术中找到的更为灵巧、简练和强大。代码样本演示了这些技术。

操纵 XML 文档最常用的技术是 DOM、SAX 和 XSLT。令人苦恼的是这些技术之间缺乏统一的原则。您对 XML 可能想要的处理都会由一种主要方法来承担,但当您所想要做的跨越了每种技术做得最好的范围时,如何解决问题就非常不清晰。您很可能最终得到一个大杂烩应用程序,其中各种较小的转换用异构技术和工具串联在一起。

DOM 将 XML 每一样都融入到面向对象编程(OOP)框架中,在比其他任何专门编程语言更高的抽象层次上。DOM 是语言无关的,并且其“文档对象模型”是由许多通用编程语言的库提供的。在某种意义上,DOM 是语言 ― 它指定了所有的方法和参数 ― 而基本通用语言只不过是一点胶水。我的实用程序 xml_objectify, 曾在前面的专栏文章中讨论过,提供了一种将 XML 文档转换为更自然感觉的“OOP 化”对象(在 Python 中)的方法,主要是为了回应有点人造感觉的 DOM。

SAX 在其语言中性上与 DOM 相似,但它用事件驱动和过程化模型取代了 DOM 的 OOP 框架。SAX 在将 XML 文档作为流处理的能力上有非常好的特性,即根据遇到的每个元素和内容来进行处理。但随着其事件驱动原理带来了 SAX 的缺点,即没有由 XML 文档表示的数据结构的实际概念。我们可以在一个具体的应用程序中构建这样的结构,但即使如父/子这样简单的事物都必须以 SAX 库下面的编程语言的词汇表进行表示;SAX 本身对 XML 的大部分事情几乎一无所知。

最后,XSLT 是,从某种意义上说,与 XML 的结构最匹配的常见技术。可以反映这种匹配的是,XSLT 文档本身就是 XML 文档实例。XSLT 是一种专门 功能编程语言,允许您指定将 XML 文档转换成其它形式(特别是,但不仅是,转换成其它 XML 文档)。XSLT 除了有些冗长外,它的表达力也有所限制 ― 您可说的事情可以清楚地表达(是功能地,不是过程地),但您很快增加许多无法简单地在 XSLT 中表示的事情。

简单和典型的任务

让我举出一个实例,来说明常用技术的弱点。XSLT 是典型地描述将 XML 文档转换成输出的最直接的方法。例如,您可能想要创建 XML 文档的 HTML 表示。假设您有一个 XML 版本的 易经如清单 1 所示。


清单 1. XML 版本的 《易经》
<?xml version="1.0"?>
<IChing>
    <title>Some Hexagrams from the I Ching</title>
    <hexagram>
        <number>1</number>
        <name>Ch'ien / The Creative</name>
        <judgement>
            The Creative works sublime success,
            Furthering through perseverance.
        </judgement>
    </hexagram>
    <hexagram>
        <number>2</number>
        <name>K'un / The Receptive</name>
        <judgement>
            The Receptive brings about sublime success,
            Furthering through the perseverance of a mare.
        </judgement>
    </hexagram>
    <hexagram>
        <number>3</number>
        <name>Chun / Difficulty at the Beginning</name>
        <judgement>
            Difficulty at the Beginning works supreme success,
            Furthering through perseverance.
        </judgement>
    </hexagram>
</IChing>

要在一个 HTML 表中表示这个信息,您可能需要使用如清单 2 所示的 XSLT 指令。


清单 2. 《易经》 HTML 表的 XSLT 指令。
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/TR/xhtml1/strict">
    <xsl:output method="html" indent="yes" encoding="UTF-8"/>
    <xsl:template match="IChing">
      <html>
        <head><title><xsl:value-of select="title"/></title></head>
        <body><table border="1"><xsl:apply-templates/></table></body>
      </html>
    </xsl:template>
    <xsl:template match="hexagram">
        <tr><xsl:apply-templates/></tr>
    </xsl:template>
    <xsl:template match="number">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="name">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="judgement">
        <td><xsl:apply-templates/></td>
    </xsl:template>
    <xsl:template match="*"></xsl:template>
</xsl:stylesheet>

清单 2 中的 XSLT 指令看起来简单和直接:您只需创建一个模板来描述您希望每个元素所进行的格式化的方式。怎样会更容易?当您想要对输出进行过滤或计算时,问题就出现了 ― 这些问题没有包含在 XSLT 可用的几个比较中。例如,可能您想(按占八卦习俗)仅显示偶数编号的六角星形,或仅显示质数编号的六角星形。使用 XSLT,这么简单的事情都无法做到。

基于这一点,您可能需要转向 DOM 或 SAX 来完成任务。这是可行的,但您首先不得不完全丢弃以前 XSLT 转换所做的工作。DOM 或 SAX 与 XSLT 是完全不同的模型;它们没有与 XSLT 共享有意义的代码或概念。对于诸如我的玩具样式表这样简单的事情,几乎不成问题;对于大规模质量转换,您可能会丢失很多。

而且,对于输出,无论是 SAX 还是 DOM 都没有特别灵巧或可维护的模型。为了输出所描述的简单的(已过滤的)HTML 表,您只是必须用 print 语句或 printf() 函数(或常用语言使用的任何东西)分散到应用程序代码中。这些输出语句本身埋藏在测试元素类型和其他条件的条件块中。在这些代码中,无法简述输出如何流动或确保 print "</tr>" 达到与前面发生过的每个 print "<tr>" 相对应。当然命令或 OOP 代码也没有保证 HTML 或 XML 有格式良好的输出(至于有效更不必说)。

您可以在 DOM 上面构建“写入器”方法。这种方法的一些流行的示例是 Java 的 org.w3c.dom.html 包和 Python 的 xml.dom 节点方法 .writexm1() 。这些方法获取 DOM 树,并将它输出到敏感的 HTML 中(确保完好的格式)。然而,我的感觉是(从我已经看到的),“敏感的”只限于几个配置选项 ― 对于输出任意的 HTML(XSLT 或 HaXml 可以做到的方式)还不够。在任何情况下,输出前,您仍须对 DOM 树进行过程性操作。这与函数性风格相比,更不清晰且更易于出错。





回页首


统一的解决方案

一个既能声明性地表达输出(如 XSLT 那样),又能包括任意过滤器和计算(如 DOM 和 SAX 下面的实现语言那样)的系统,将是 XML 转换理想的系统。有了这样的系统,保证输出的良好格式,或者甚至有效性,就不可能使人痛苦。并且紧凑和直接的语法也会更好。

HaXml 满足了所有那些要求。实际上,HaXml 的能力超出了那些要求。利用象 Haskell 函数性编程语言中允许的高阶组合函数,您可以在指定的输出中任意组合多个过滤器。在 XSLT 中,每个 <xsl:template> 作为输入和输出之间的一种过滤器。但由 XSL 文件创建的过滤器的唯一实际组合是对所有过滤器的并集。相反,HaXml 让我们为输出的每个元素指定更多更细粒度的过滤器链。实际上,通过 XPath 和 XSLT 一起使用可以实现大致相同的功能,但 HaXml 更加简练并且确实更强大。

除了可以提供很多组合程序外,HaXml 允许在 Haskell 语言中包含任意计算作为过滤的一部分。还有一个优点是,您可以在更为连贯的块上指定输出,该块通过使输出词汇与过滤条件混合来允许可读。其结果,如清单 3 所示,比 XSLT 更简练,并且有较少的造成阅读困难的标点符号。


清单 3. 输出一个《易经》HTML 表的 HaXml 程序。
module Main where
import XmlLib
-- Concise XSLT-like specification of output
main = processXmlWith (hexagrams `o` tag "IChing")
hexagrams =
    html [
      hhead [htitle [keep /> tag "title" /> txt] ],
      hbody [htableBorder [rows `o` children `with` tag "hexagram"] ]
    ]
htableBorder = mkElemAttr "TABLE" [("BORDER",("1"!))]
rows f =
    let
      num = keep /> tag "number" /> txt
      nam = keep /> tag "name" /> txt
      jdg = keep /> tag "judgement" /> txt
    in
      if (condition (num f) (nam f) (jdg f))
      then hrow [hcol [num], hcol [nam], hcol [jdg]] f
      else []
condition num nam jdg = isPrime (makeInt num)
-- Supporting computations for rows condition
makeInt = stringToInt . unwrap      -- Turn a Content into an Integer
unwrap [(CString b c)] = c          -- Turn a Content into a String
stringToInt = revToInteger.reverse  -- Turn a String into an Integer
    where
    revToInteger = toInteger . revToInt
    revToInt []  = 0
    revToInt (d:ds) = digitToInt d + (10*(revToInt ds))
-- ordered search of Sieve of Eratosthenes
isPrime = ordSearch (sieve [2..])  
    where
    ordSearch (x:xs) n
        | x < n     = ordSearch xs n
        | x == n    = True
        | otherwise = False
    sieve (x:xs) = x : sieve [y | y <- xs, y `mod` x > 0]

在 XSLT 中,定义可以以任何次序出现。代码最初 20 行指定了输出格式,其中一些定义分拆到支撑函数中(仅为可读性)。在 Haskell 语法中,不管出现什么样的情况,函数出现在等号左边,定义出现在等号右边。 wherelet 子句指定了在其他语言中称为“内部函数”的函数。最初几行概念上与 XSLT 方法非常相似。但作为改进,HaXml 版本让我们在任何需要的地方定义特殊的过滤器。以下为过滤器的示例。

rows `o` children `with` tag "hexagram"

`o` 被富于幻想地称为“爱尔兰组合”,并且它的发音也可以为 ofrows 是为方便起见而定义的一个过滤器;您可以象使用任一标准过滤器那样复用 rows ,并且在无限的组合中。

程序的后半部分包含几个计算型函数。一个非常好的示例是一个六行的原始测试,充分演示了 Haskell 作为一种语言的强大和灵巧。由于程序是结构化的,函数 condition 可以在它所希望的 <number><name> 或者 <judgement> 元素的 Content 上一样好地进行任何测试。 Content 是一种特殊数据类型,所以您首先要做的就是 unwrap 包含在 Content 中的字符串。然后,可以将它转换为 Integer 并且测试它(或做其他您想要进行的操作)。

还有关于 HaXml 库的更详细信息 ― 学习 Haskell 本身需要一条学习曲线,使程序员习惯于命令式和 OOP 的编程的风格。但如果您将兴趣只限于可在 XSLT 中发掘其能力的话,示例程序的上半部分非常易于扩展(在使用类似的函数风格的同时,我会证明,运用比 XSLT 更容易的学习曲线)。不但它的语法上比 XSLT 更具可读性,而且您绕过了必须立即学习如何做程序下半部分中的那类事情(以及更多)的需要。





回页首


强制有效性

在上述示例中,XML 文档被当作一般树结构处理。对于大多数用途,这样做是最容易和最快速的方法。但 HaXml 提供了其他的完全独特的方法。如果您已经有一个计划使用的文档类型的 DTD,则可以从那个 DTD 生成一组原始 Haskell 数据结构。从那一点向前,可以编写在您的操纵中利用这个原始 DTD 的应用程序。从 DTD 生成 HaXml 数据结构涉及几个步骤。第一件事就是 创建数据结构,将之作为模块,就如同:

% DtdToHaskell MyFile.DTD MyFileDTD.hs

一旦数据结构模块可用,就可以编写处理必须符合 DTD 的 XML 文档的应用程序。这种应用程序通常至少包含简短的清单 4 中的那几行。


清单 4. 为 MyFile.DTD XML 文档定制的 HaXml 应用程序
import Xml2Haskell (readXml)
import MyFileDTD
[...]

从那里,可以使用 Haskell 提供的所有高阶技术来处理递归数据结构。很自然地,为了使用专门的输入 XML 文档,首先就要 readXml

读者在这一点上有所考虑是可以理解的,“这没有什么”。看起来 Haskell 与 DOM 方法没有区别 ― 或者甚至与 SAX 也没有区别 ― 您可以非常顺利地使用结构化数据,或者甚至针对 DTD 确认。

这里还有比满足表面的需求更多。Haskell,不象几乎其它所有编程语言,关于数据结构的类型是 完全安全的。在 Haskell 中,在会导致无效 XML 文档的生成的数据结构上执行任何计算是 不可能的。相反,在如 C/C++、 Java、Perl、Smalltalk 或者 Python 语言中能做得最好的,是在输入上进行一次全面检查(确认),而在输出进行另一次,并且希望每一样都正确无误。在如 Eiffel 等语言中,有可能在每个“加法器”和“删除器”上添加足够的约定限制,以确保维护了 DTD 有效性(或者,如果进行足够工作,在所有提到的语言中),但是这样做涉及每个 .addSpamTag() 方法内的定制编程。此外,在提到过的语言中,所有 DTD 可以做的是提供一张欺骗表供应用程序程序员查看。

使用 HaXml,从 DTD 中编程生成的数据结构包含了每一个有效性约束。需要提醒的是,仅仅加强约束并不能使一个应用程序程序员编写出正确的代码;但至少在编译时就捕获了可能导致无效文档的任何错误代码。另外一点,当然,是在编写强制有效性的定制应用程序时将比不需这样做而又要做更多编程工作时显得很简单。但对于“任务”很敏感需求,HaXml 完全能够为严格的目标提供最快速和最安全的路径。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 请参加有关本文的 论坛

  • Bijan Parsia 写了一篇非常有趣的评论,称为 Functional programming and XML,详见 XML.com。Parsia 在文中提到,通常,与 OOP 技术具有很多相似性相比,函数性编程风格更适合于处理 XML。他讨论了 HaXml 和许多其他工具。

  • HaXml 的原作者, Malcolm Wallace 和 Colin Runciman,写了一篇详细的 HaXml 论述, Haskell and XML:Generic Combinators or Type-based Translation。文章的语调和水平表明作者对 Haskell 和函数性编程都非常精通,超出了本专栏的要求,Wallace 和 Runciman 的论文包含了许多详细的信息,这里不再赘述。

  • 有关 Haskell 的信息,包括许多教程、论文以及各种编译器和解释器,请登录 Haskell 语言网站

  • 我自己编写了 Haskell 简明教程,适用于初学者,它被放在 developerWorks 上。

  • 您可以从 我的档案上下载本专栏这篇文章中提到的文件的 zip 压缩文档。

  • 本文中有关讨论转换的样本可在以下站点上查找:
  • 查找有关“XML 问题”专栏以前的文章:
    • XML 问题 #1介绍 Python xml_pickle 对象。
    • XML 问题 #2介绍如何使用 Python 的 xml_objectify。
    • XML 问题 #3介绍 DocBook。
    • XML 问题 #4继续介绍如何使用 DocBook 构建旧的文档档案。
    • XML 问题 #5说明如何通过 XSLT 将 XML 文档转换成 HTML。
    • XML 问题 #6比较了半打 XML 编辑器,同时特别着眼于那些适合于有大量文本的文档工具。
    • XML 问题 #7将 DTD 与 XML Schema 进行了权衡,并建议无论 W3C XML Schema 规范有多么成熟,开发者在什么情况下需要坚持使用 DTD。
    • XML 问题 #8讨论了由计算机科学家概念化出来的数据 模型的抽象理论是如何帮助我们开发特定的多表示数据流的。
    • XML 问题 #9讨论了公众域 sql2dtd 和 sql2xml 实用程序;这些实用程序可以不依赖 RDBMS 生成可移植 XML 结果集。
    • XML 问题 #10延伸了 David 的“可爱的 Python” #15 专栏中的一般文本索引器,包含了指定 XML 的查找和索引特点,并讨论了索引器是如何利用 XML 的层次节点结构的优势的。
    • XML 问题 #11重温模块和在该系列第一个专栏中介绍的模块。
    • XML 问题 #12讨论在相容和可逆的方式下,起动一个 XML 文档时,生成 SQL 语句的公共域实用程序,用此 SQL 语句来创建和填充数据库。
    • XML 问题 #13探索压缩 XML 文档的几种方法。



关于作者

David Mertz

David Mertz 的文章向我们展示了他在编程和理论方面令人惊异的敏锐的洞察力。可以通过 mertz@gnosis.cx 与 David 联系;在 http://gnosis.cx/publish/上详细地介绍了他的生活。非常欢迎对以前的、本篇和以后的专栏文章提出建议和意见。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款