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 更为常用(使用实用程序如:PKZIP、Info-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 的压缩性能。试过的块大小有 1k、10k、100k 和 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
文件。但是,每次压缩定制编程并非总是值得或可能的。榨干更多字节也许是可达到的目标。