如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器。或者,可能有很多解析器可选,但是要么太慢,要么太耗内存,或者没有你需要的特定功能。或者开源解析器存在缺陷,或者开源解析器项目被取消诸如此类原因。上述原因都没有你将需要实现你自己的解析器的事实重要。
当你必需实现自己的解析器时,你会希望它有良好表现,灵活,功能丰富,易于使用,最后但更重要是易于实现,毕竟你的名字会出现在代码中。本文中,我将介绍一种用Java实现高性能解析器的方式。该方法不具排他性,它是简约的,并实现了高性能和合理的模块化设计。该设计灵感来源于VTD-XML ,我所见到的最快的java XML解析器,比StAX和SAX Java标准XML解析器更快。
两个基本解析器类型
解析器有多种分类方式。在这里,我只比较两个基本解析器类型的区别:
-
顺序访问解析器(Sequential access parser)
-
随机访问解析器(Random access parser)
顺序访问意思是解析器解析数据,解析完毕后将解析数据移交给数据处理器。数据处理器只访问当前已解析过的数据;它不能回头处理先前的数据和处理前面的数据。顺序访问解析器已经很常见,甚至作为基准解析器,SAX和StAX解析器就是最知名的例子。
随机访问解析器是可以在已解析的数据上或让数据处理代码向前和向后(随机访问)。随机访问解析器例子见XML DOM解析器。
顺序访问解析器只能让你在文档流中访问刚解析过的“窗口”或“事件”,而随机访问解析器允许你按照想要的方式访问遍历。
设计概要
我这里介绍的解析器设计属于随机访问变种。
随机访问解析器实现总是比顺序访问解析器慢一些,这是因为它们一般建立在某种已解析数据对象树上,数据处理器能访问上述数据。创建对象树实际上在CPU时钟上是慢的,并且耗费大量内存。
代替在解析数据上构建对象树,更高性能的方式是建立指向原始数据缓存的索引缓存。索引指向已解析数据的元素起始点和终点。代替通过对象树访问数据,数据处理代码直接在含有原始数据的缓存中访问已解析数据。如下是两种方法的示意图:
因为没找到更好的名字,我就叫该解析器为“索引叠加解析器”。该解析器在原始数据上新建了一个索引叠加层。这个让人想起数据库构建存储在硬盘上的数据索引的方式。它在原始未处理的数据上创建了指针,让浏览和搜索数据更快。
如前所说,该设计受VTD-XML的启发, VTD是虚拟令牌描述符(Virtual Token Descriptor)的英文缩写。因此,你可以叫它虚拟令牌描述符解析器。不过,我更喜欢索引叠加的命名,因为这是虚拟令牌描述符代表,在原始数据上的索引。
常规解析器设计
一般解析器设计会将解析过程分为两步。第一步将数据分解为内聚的令牌,令牌是一个或多个已解析数据的字节或字符。第二步解释这些令牌并基于这些令牌构建更大的元素。两步示意图如下:
图中元素并不是指XML元素(尽管XML元素也解析元素),而更大“数据元素”构造了已解析数据。在我XML文档中表示XML元素,而在JSON 文档中则表示JSON对象,诸如此类。
举例说明,字符串将被分解为如下令牌:
< myelement >
一旦数据分解为多个令牌,解析器更容易理解它们和判断这些令牌构造的大元素。解析器将会识别XML元素以 ‘<’令牌开头后面是字符串令牌(元素名称),然后是一系列可选的属性,最后是‘>’令牌。
索引叠加解析器设计
两步方法也将用于我们的解析器设计。输入数据首先由分析器组件分解为多个令牌。 然后解析器解析这些令牌识别输入数据的大元素边界。
你也可以增加可选的第三步骤—“元素导航步骤”到解析过程中。 若解析器从已解析数据中构造对象树,那么对象树一般会包含对象树导航的链接。当我们构建元素索引缓存代替对象树时,我们需要一个独立组件帮助数据处理代码导航元素索引缓存。
我们解析器设计概览参见如下示意图:
我们首先将所有数据读到数据缓存内。为了保证可以通过解析中创建的索引随机访问原始数据,所有原始数据必需放到内存中。
接着,分析器将数据分解为多个令牌。开始索引,结束索引和令牌类型都会保存于分析器中一个内部令牌缓存。使用令牌缓存使其向前和向后访问成为可能,上述情况下解析器需要令牌缓存。
第三步,解析器查找从分析器获取的令牌,在上下文中校验它们,并判断它们表示的元素。然后,解析器基于分析器获取的令牌构造元素索引(索引叠加)。解析器逐一获得来自分析器的令牌。因此,分析器实际上不需要马上将所有数据分解成令牌。而仅仅是在特定时间点找到一个令牌。
数据处理代码能访问元素缓存,并用它访问原始数据。或者,你可能会将数据缓存封装到元素访问组件中,让访问元素缓存更容易。
该设计基于已解析数据构建对象树,但它需建立访问结构—元素缓存,由索引(整型数组)指向含有原始数据的数据缓存。我们能使用这些索引访问存于原始数据缓存的数据。
下面小节将从设计的不同方面更详细地进行介绍。
数据缓存
数据缓存是含有原始数据的一种字节或字符缓存。令牌缓存和元素缓存持有数据缓存的索引。
为了随机访问解析过了的数据,内存表示上述信息的机制是必要的。我们不使用对象树而是用包含原始数据的数据缓存。
将所有数据放在内存中需消耗大块的内存。若数据含有的元素是相互独立的,如日志记录,将整个日志文件放在内存中将是矫枉过正了。相反,你可以拉大块的日志文件,该文件存有完整的日志记录。因为每个日志记录可完全解析,并且独立于其它日志记录的处理,所以我们不需要在同一时间将整个日志文件放到内存中。在我的文章—“使用缓存迭代访问数据流”中,我已经描述了如何遍历块中的数据流。
标记分析器和标记缓存
分析器将数据缓分解为多个令牌。令牌信息存储在令牌缓存中,包含如下内容:
-
令牌定位(起始索引)