XML压缩和传输性能的改善

XML应用
——XML 压缩和传输性能的改善
 
XML 是一种为清晰和易用而设计的文本标记格式,没有考虑简洁性。和任何设计一样, XML 也有一些弱点,其中之一就是把应用程序数据转化成 XML 表示或者相反所需要的开销。这种开销可能成为许多应用程序总处理代价的主要部分,尤其是那些交换大量数据而内部处理相对较少的应用程序。和其它形式的数据表示相比, XML 文档往往很大。因此有些时候,带宽和存储空间会非常重要。本文中将讨论 XML 的非文本表示所涉及到的一些问题,并介绍为此目的正在开发的几种方法
传输 XML 文档时带宽和处理的权衡
文档大小
数据的 XML 表示往往要比相同数据的二进制表示大得多,主要有两个原因:
简单数据值的文本表示通常要比相同值的二进制表示大一些。 XML 是为了清晰性和互操作性而不是简洁性设计的一种文本标记格式。一旦这种冗余与 XML 应用程序中常常使用的相当长的元素和属性名结合起来, XML 文档中标记成分的大小可能远远超出文档中实际数据成分的大小。单纯为了格式化的目的而增加的空白内容进一步增加了文档大小。
较大的文档大小意味着传输数据的 XML 表示和等价的二进制表示相比需要更多的带宽。更大的尺寸也意味着更高的处理代价,因为通信过程中涉及到的开销大部分和数据量有关。
 
处理开销
和简单的二进制数据表示相比, XML 也需要更多的处理开销。从输入端来说, XML 文档处理程序必须识别输入文档文本中代表不同标记形式的多种类型的字符组合。处理程序还需要验证每个文档是结构良好的 XML ,因此在处理标记时必须检查状态的转换。名称空间虽然是可选的但其应用越来越普遍,它要求通过相关的前缀跟踪名称空间的定义,并识别和解除引用( dereference )标记中的元素名和属性名所用的前缀。最后,文本 XML 数据可能需要转化成类型化的二进制值,以便接收它的应用程序能够使用。
XML 文档输出也有类似的问题。无论输入还是输出都需要和 XML 文档文本所用的特定字符编码相互转化,进一步增加了复杂性。 XML 处理程序通常被设计成能够处理多种不同的可能编码。因此即使两个应用程序间的文档交换总是使用某种预先确定的编码,仍然要付出保证通用性所造成的开销。
 
跳出文本的局限
XML 仅仅依据文本定义,因此严格地讲文本之外的任何东西都不可能是 XML 。另一方面,使用 XML 交换数据的应用程序可能更关心传递的数据,而不是严格的 XML 表示。根据所愿在多大程度上坚持 XML 的文本特性,可以选择多种不同的技术减少文档大小、提高处理速度或者两者兼备。
 
文本至上:一般的文本转换
最严格遵循 XML 文本特性的技术是一般基于文本的转换。这种类型的转换主要和文档大小有关。文本压缩算法多年来一直是大量研究项目的课题,目前已经非常成熟。这种类型的任何算法都能方便地用于 XML 文档的文本表示。这类算法不大可能改善处理速度,因为实际上在普通的 XML 文本处理之外,它们在应用程序间数据流的两端增加了一层转换:在发送端压缩文本,然后在接收端解压缩。
 
数据至上:为特定 XML 应用程序量身定做的格式
为特定 XML 应用程序专门设计的格式是和基于文本的压缩相对的另一个极端。这些格式可能和数据的纯二进制表示等价,和文本 XML 相比有可能同时压缩数据的大小并减少处理开销。这种方法的主要缺点是必须根据应用程序使用的文档结构量身定做,发送方和接收方必须事先就具体的结构达成一致,并且实现适当的编码程序 / 解码程序。
针对应用程序定制编码的多数方法都是基于交换文档的 W3C XML Schema 规范。模式中包含的类型和结构信息用于生成定制的编码程序 / 解码程序代码,可能包括能够代表文档数据内容并直接与编码程序 / 解码程序交互的对象。 Fast Web Services 是从模式定制编码的一个例子,它建立在 ASN.1 结构化数据表述的基础上。
这种类型的模式编码很难与处理一般 XML 文档的其他方法比较。为了测试各种文档,首先必须为每个文档定义模式,然后生成这种模式的编码程序 / 解码程序代码。用当前的实现完成这种数据表示和标准 XML 文本形式的转化一般是不可能的,因此还需要编写某种自定义的转化程序,以便把标准文本 XML 文档转化成可以使用这种编码的形式。由于这些原因,我的测试结果中没有包括这种方法的例子,但是在您看过这些结果之后,我还将再提一提模式编码的使用。
XML压缩

当考虑压缩文档时,通常首先考虑常用的压缩算法,如:Lempel-Ziv Huffman,以及在它们上面实现变化的一些常用实用程序。特别是,在类 Unix 平台上,首先想到的通常是实用程序 gzip;在其它平台上,zip 更为常用(使用实用程序如:PKZIPInfo-ZIP WinZip)。现已证明 gzip 始终要优于 zip,但使用的人较少。这些实用程序实际上意在充分地减少 XML 文件的大小。但是,同样证明了通过两种方法单独或组合可以获得相当好的压缩率。

第一种技术使用 Burrows-Wheeler 压缩算法而不是顺序 Lempel-Ziv 算法。

第二种技术是利用 XML 文档非常特定的结构生成更可压缩的表示。

本文中获取或创建了四个基本文档用于比较的目的。第一个是莎士比亚的戏剧 哈姆雷特作为 XML 文档(请参阅 参考资料)。标记中包括如 <PERSONA> <SPEAKER> <LINE> 等标记,这些十分自然地映射到人们可能在印刷拷贝中遇到的排版形式。为了对 XML 标记如何有助于文档大小和可压缩性作比较,我从 hamlet.xml 派生了一个文档 hamlet.txt ,仅仅只是删去所有 XML 标记而保留其内容。这种派生是 不可逆的且是一种信息的绝对丢失。

另外两个文件是 Apache Weblog 文件(一组简洁的面向行的记录)及从这个文件创建的 XML 文档。因为源文档是日志文件,在转换中无信息丢失,而从 XML 重新创建原始格式的文档非常繁琐。
可逆转换
XML 文档涉及压缩时有效率相当低的形式, bzip2 通过对字符串重新分组就稍微减轻了这种低效性。就本质而言, XML 文档是十分不同部分的混合物 不同类型的标记、属性和元素体。如果能获取每个相对一致的事物的集合并且在已转换的文件中将它们相互紧密分组,标准压缩程序将会有很多工作要处理。例如:如果 Weblog 中每个 <host> 标记体出现在另一个附近,包含主机 IP 地址的那块东西就非常易于压缩。技巧在于:找到将 XML 文档转换成包含 所有相同信息的一种方法,而以一种对压缩程序友好的风格构造布局。
实用程序 xml2struct.py struct2xml.py 恰恰能做我们所希望的。
"struct" 文档的常用格式如下:
原始 XML 文档中出现的标记列表,由新行字符分隔。
章节分隔符: 0x00 (空字节)
总体文档结构的紧凑表示,每个开始标记由单一字节表示,内容的出现由 0x02 字节标记。
另一个章节分隔符: 0x00 (空字节)
在文档结构示意图中显示的所有元素的内容,按元素类型分组。每个单独的内容项由 0x02 字节分隔,而新类型元素的开始由 0x01 字节分隔(最后的一个并非严格必需的,但它使逆向转换更简便)。
 
下面是实现和逆转所描述转换的完整 Python 代码。
import sys
import xml.sax

from xml.sax.handler import *

 

class StructExtractor(ContentHandler):

    """Create a special structure/content form of an XML document"""

    def startDocument(self):
        self.taglist = []
        self.contentdct = {}
        self.state = []             # stack for tag state

        self.newstate = 0           # flag for continuing chars in same elem

        self.struct = []            # compact document structure
 
    def endDocument(self):
        sys.stdout.write('/n'.join(self.taglist))

                                    # Write out the taglist first

        sys.stdout.write(chr(0))    # section delimiter /0x00
        sys.stdout.write(''.join(self.struct))

                                    # Write out the structure list

        sys.stdout.write(chr(0))    # section delimiter /0x00
        for tag in self.taglist:    # Write all content lists

            sys.stdout.write(chr(2).join(self.contentdct[tag]))

            sys.stdout.write(chr(1)) # delimiter between content types

 
    def startElement(self, name, attrs):
        if not name in self.taglist:
            self.taglist.append(name)
            self.contentdct[name] = []
            if len(self.taglist) > 253:

                raise ValueError, "More than 253 tags encountered"

        self.state.append(name)     # push current tag
        self.newstate = 1           # chars go to new item

                                    # single char to indicate tag

        self.struct.append(chr(self.taglist.index(name)+3))

 
    def endElement(self, name):
        self.state.pop()            # pop current tag off stack
        self.newstate = 1           # chars go to new item
        self.struct.append(chr(1)) # /0x01 is endtag in struct
 
    def characters(self, ch):
        currstate = self.state[-1]
        if self.newstate:           # either add new chars to state item

            self.contentdct[currstate].append(ch)

            self.newstate = 0
            self.struct.append(chr(2))

                                    # /0x02 content placeholder in struct

        else:                       # or append the chars to current item

            self.contentdct[currstate][-1] += ch

 

if __name__ == '__main__':

    parser = xml.sax.make_parser()
    handler = StructExtractor()
    parser.setContentHandler(handler)
    parser.parse(sys.stdin)
 

使用SAX 而不是 DOM 使这一转换相当节省时间,即使时间不是开发它的主要考虑事项。

逆向转换,
struct2xml.py
def struct2xml(s):
    tags, struct, content = s.split(chr(0))
    taglist = tags.split('/n')      # all the tags

    contentlist = []                # list-of-lists of content items

    for block in content.split(chr(1)):
        contents = block.split(chr(2))
        contents.reverse()          # pop off content items from end
        contentlist.append(contents)

    state = []                     # stack for tag state

    skeleton = []                   # templatized version of XML
    for c in struct:
        i = ord(c)
        if i >= 3:                  # start of element

            i -= 3                  # adjust for struct tag index offset

            tag = taglist[i]        # spell out the tag from taglist
            state.append(tag)       # push current tag
            skeleton.append('<%s>' % tag)

                                    # insert the element start tag

        elif i == 1:                # end of element
            tag = state.pop()       # pop current tag off stack

            skeleton.append('</%s>' % tag)

                                    # insert the element end tag

        elif i == 2:                # insert element content
            tag = state[-1]

            item = contentlist[taglist.index(tag)].pop()

            item = item.replace('&','&')

            skeleton.append(item)   # add bare tag to indicate content
        else:

            raise ValueError, "Unexpected structure tag: ord(%d)" % i

 
    return ''.join(skeleton)
 

if __name__ == '__main__':

    import sys
    print struct2xml(sys.stdin.read()),
如前所述,重新构建 XML 极大地帮助了 gzip 压缩。
 
压缩 SOAP 编码
如果需要在 Web 服务中传输 XML ,您可能会发现有效负载太长了。这种情况下您可以对 XML 内容使用多种文本压缩选项中的一种。
Web 服务交换的示例 XML 文档

<?xml version="1.0" encoding="UTF-8"?>

<PurchaseOrder Version="4010">

<PurchaseOrderHeader>

 <TransactionSetHeader X12.ID="850">

    <TransactionSetIDCode code="850"/>

    <TransactionSetControlNumber>12345</TransactionSetControlNumber>

 </TransactionSetHeader>
 <BeginningSegment>

    <PurposeTypeCode Code="00 Original"/>

    <OrderTypeCode Code="SA Stand-alone Order"/>

    <PurchaseOrderNumber>RET8999</PurchaseOrderNumber>

    <PurchaseOrderDate>19981201</PurchaseOrderDate>

   </BeginningSegment>
 <AdminCommunicationsContact>

    <ContactFunctionCode Code="OC Order Contact"/>

    <ContactName>Obi Anozie</ContactName>

 </AdminCommunicationsContact>
</PurchaseOrderHeader>
<PurchaseOrderDetail>
 <Name1InformationLOOP>
    <Name>

      <EntityIdentifierCode Code="BY Buying Party"/>

      <EntityName>Internet Retailer Inc.</EntityName>

      <IdentificationCodeQualifier Code="91 Assigned by Seller"/>

      <IdentificationCode>RET8999</IdentificationCode>

    </Name>
    <Name>

      <EntityIdentifierCode Code="ST Ship To"/>

      <EntityName>Internet Retailer Inc.</EntityName>

    </Name>

    <AddressInformation>123 Via Way</AddressInformation>

    <GeographicLocation>
      <CityName>Milwaukee</CityName>

      <StateProvinceCode>WI</StateProvinceCode>

      <PostalCode>53202</PostalCode>

    </GeographicLocation>
 </Name1InformationLOOP>
 <BaselineItemData>
    <QuantityOrdered>100</QuantityOrdered>
    <Unit Code="EA Each"/>
    <UnitPrice>1.23</UnitPrice>

    <PriceBasis Code="WE Wholesale Price per Each"/>

    <ProductIDQualifier Code="MG Manufacturer Part Number"/>

    <ProductID Description="Fuzzy Dice">CO633</ProductID>

 </BaselineItemData>
</PurchaseOrderDetail>
</PurchaseOrder>

原来的例子只有 200 个字节长,而这个 XML 版本有 1721 字节长。

知名的 PK-ZIP 例程能够把这个 XML 文件压缩到 832 个字节。
GNU gzip 例程则把该文件压缩为 707 个字节。
bzip2 中的开发源代码例程把该文件压缩到 748 个字节。
所有这些压缩格式都不如专门的 EDI 格式紧凑,但 EDI 格式不容易理解。 bzip2 由于和 gzip 相比对很多文件有更好的压缩效率(在较慢的压缩速度下)而闻名,但是据我的观察上述结果并非个例,就是说对于 XML 的处理 gzip 要好于 bzip2
目前的多数平台和语言都提供压缩库,至少包含 PK-ZIP GNU gzip 压缩算法,可以在调用 Web 服务之前通过编程进行压缩。
一定要分析标准化( C14N )是否有助于具体实例的压缩。 C14N 是生成 XML 文档物理表示——称为标准形式——的标准化方法,以便解决 XML 语法在不改变含义的情况下所允许的细微变化。根据粗略的经验方法,如果 XML 是手工编辑的,属性的顺序和空格的使用可能有各种变化, C14N 可能会改进大型文档的压缩性能。但是如果 XML 是机器生成的或者使用了大量空白元素, C14N 可能是有害的。上述例子更接近后一种情况。我使用 PyXML 项目中的 C14N 模块进行了标准化处理。 Python 代码如下所示:

>>> from xml.dom import minidom

>>> from xml.dom.ext import c14n

>>> doc = minidom.parse('listing1.xml')

>>> c14n.Canonicalize(doc)

>>> f = open('listing1-canonical.xml', 'w')

>>> c14n.Canonicalize(doc, output=f)

>>> f.close()

得到的文件 listing1-canonical.xml 1867 个字节,使用 gzip 压缩后还有 714 个字节。未压缩的文本多出了 146 个字节,gzip 压缩后的结果多出了 7 个字节。主要的原因是空白元素在 C14N 之后用最冗长的形式表示。比如,下面这一行:

<Unit Code="EA Each"/>
就变成了

<Unit Code="EA Each"></Unit>    

要把 gzip 之类例程压缩后的 XML 绑定到 SOAP 中,有两种办法可供选择:
使用某种形式的附带工具。
对消息主体内容使用 Base64 这样的编码。
一般说来,如果对 XML 文件应用 gzip ,并且压缩后的结果采用 Base64 编码,在 SOAP 中联机传输,结果文件通常只有原来的一半大小。这可能足以满足在 XML Web 服务中节省空间的需要。
XML压缩的进一步研究:块级别算法和资源负载
用可逆地重新构造 XML 文档以改进压缩的技术。然而,对于大型 XML 文档和嵌入式处理,在压缩过程之前重新构造整个源文件似乎不太实际。因此在压缩改进和 CPU /内存需求方面对重构进行了进一步的研究和发掘。
使用方案
使用 xmlstruct 通信协议
技术概述
只需要对 xml2struct 的重新构造技术稍加更改就可以适应任意块大小。本文压缩文档中包含两个块级别重新构造的实现。
在原始算法中,包括了作为重新构造的 XML 文档第一个定界节的标记列表。该标记列表 在实际的文档解析期间在特别基础上生成 用作结构示意图中使用的单字节标记占位符的索引。使用字节索引值来代替标记的策略在某种程度上减少了重新构造后的文档的大小,但也限制算法只能处理不同标记数少于 254 DTD
在以下的块级别算法修订中,我假设可以独立使用标记表。本文压缩文档中提供了从 DTD 创建标记表的实用程序函数。假设通道两端都有必需的 DTD ,一切正常,若没有 DTD ,任何其它指定标记次序的格式也可奏效。
唯一需要的重大修改是增加新的(第一个)定界文档节以指示当前的元素嵌套情况。在原始文档级别格式中,每个开始标记与一个结束标记配对。但因为 XML 文档在块级别格式中是从任意位置断开的,因此有必要记录打开元素(块在其中开始)的堆栈。第一个被解析的块有这个第一节的空白字段;后续块可能有一个或多个字节列出尚未关闭的标记的索引。
重新构造的块的格式:
开始标记的列表:每个开始标记是单个字节( 字节值 >= 0x03 );将该列表压入标记列表堆栈以匹配相应的结束标记字节
节定界符: 0x00 (空字节)
块文档结构的紧凑表示法: 其中每个开始标记由单个字节表示,出现的内容由 0x02 字节表示;结束标记由 0x01 字节表示并且必须与相应的开始标记向后匹配
另一个节定界符: 0x00 (空值字节)
文档结构示意图中指示的所有元素的内容 按元素类型分组:每个单独的内容项由 0x02 字节定界,新类型元素的开始由 0x01 字节定界(最后一个元素没有严格要求,但它使逆向转换更容易)
要注意演示代码的两个限制。第一个限制完全是研究实现方便的结果:元素属性不是由当前解析器处理。第二个限制更为重要。只有当遇到结束标记时才刷新块。若单个元素的 PCDATA 内容不是始终都小于所用的块大小,则不强制块大小。通常情况下,输入 XML 块比指定的块大小略大一些,不过一旦用单字节标记占位符替代标记,则替换后的大小通常比块大小略小一些。一旦对块进行了压缩,则压缩的块大小明显小于输入块大小。
 
结果量化

出于量化用途,我使用前面研究中所介绍的两个有代表性的相同 XML 文档。一个是面向散文的文档,莎士比亚的哈姆雷特的 XML 版。另一个 XML 源文档是从一个 1MB Apache 日志文件创建的,在文档中用 XML 标记环绕日志文件中的每个字段(和每个输入项)。尽管所用的标记不是特别详细,但文件还是扩充至大约 3MB

在下面的图中,前面的两个灰色条表示简单的文件级别压缩(使用 gzip bzip2 )所取得的压缩效果。图后部的绿色条和蓝色条表示不使用重新构造过程对块进行压缩所取得的压缩效果。

最后,中间的红色条表示结合了 zlib 库压缩的 xml2struct.py 的压缩性能。试过的块大小有 1k10k100k 1MB。首先是哈姆雷特的压缩比较图,然后是 weblog 的压缩比较图:

hamlet.xml weblog.xml 中都出现了同样的常规模式,但 weblog.xml 中的模式 更强。日志文件高度重复的结构使重新构造能发挥其最大优势。块大小比较小时,压缩比文件级别压缩差很多。块大小约为 10k 时,块级别压缩开始显得比较不错;块大小为 100k 时,块级别压缩就非常接近文件级别压缩技术了。

本文中图表最有趣的部分是重新构造块策略的压缩特征。重新构造始终比简单的块级别 zlib 的行为有改进。当块大小为 100k 左右时,重新构造比文件级别 gzip 要好得多,这是很好的结果。令人惊奇的结果是块级别 bzip2 压缩的行为。正如预期的那样,一旦块大小变大,是否使用块级别压缩并没有区别。不过块大小必须达到 1MB 才能完全消除差异。然而,块大小较小时,块级别 bzip2 表现得非常差。之前的重新构造并不能明显地改进这一点。事实上,在块大小为 1k 时,bzip2 始终比 zlib 差得多。

测试结果表明,对 XML 使用文本之外的表示,无论从数据大小还是处理开销上看都可以获得明显的好处。标准文本压缩技术可以极大减少文档的大小,代价是额外的处理开销。特定 XML 编码如 XBIS 可以显著降低处理的开销,并适当压缩文档大小。对于只交换已知类型文档的情况,针对特定文档结构量身定做的基于模式的编码在将来有可能提供更好的性能。
尽管这里呈现的实用程序是一种初步的尝试,即便是在这一早期形式中它也做得令人称奇的好 至少在某些情况下 -- 从压缩的 XML 文件中榨干最后那几个字节。经过一些改进和实验,我期望能获得几个百分点的降低。
某些商业实用程序尝试利用压缩文档的特定 DTD 知识的方式执行 XML 压缩。这些技术相当有希望获得附加压缩。 xml2struct.py XMill 作为简单的命令行工具的优点在于:您可以透明地应用于 XML 文件。但是,每次压缩定制编程并非总是值得或可能的。榨干更多字节也许是可达到的目标。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值