XML 的计算类库

XML 计算起来不够方便,通常要用计算类库完成,本文将比较四类 XML 的计算库,包括 dom4j、MySQL、Scala、集算器 SPL,重点考察这些工具在语法表达、部署配置、数据源方便的差异,详情点击XML 的计算类库

 

XML的优点在于灵活地表达数据,但计算起来不太方便,这种情况下就要用到计算类库。下面将比较几类常见的XML计算类库,重点是语法表达、部署配置、数据源方便的区别。

dom4j

XML历史悠久,各种语言下都有XML的计算类库,其中JAVA里就有十多种,比如dom4j/ JDOM/ Woodstox/ XOM/ Xerces-J/ Crimson等,这其中又以dom4j最为成熟。下面举例说明dom4的语法表达能力。

文件Employees_Orders.xml存储一批员工信息,以及属于员工的多个订单,部分数据如下:

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

<xml>

<row>

         <EId>2</EId>

         <State>"New   York"</State>

         <Dept>"Finance"</Dept>

         <Name>"Ashley"</Name>

         <Gender>"F"</Gender>

         <Salary>11000</Salary>

         <Birthday>"1980-07-19"</Birthday>

<Orders>[]</Orders>

</row>

<row>

         <EId>3</EId>

         <State>"New   Mexico"</State>

         <Dept>"Sales"</Dept>

         <Name>"Rachel"</Name>

         <Gender>"F"</Gender>

         <Salary>9000</Salary>

         <Birthday>"1970-12-17"</Birthday>

         <Orders>

                  <OrderID>32</OrderID>

                  <Client>"JFS"</Client>

                  <SellerId>3</SellerId>

                  <Amount>468.0</Amount>

                  <OrderDate>"2009-08-13"</OrderDate>

         </Orders>

         <Orders>

                  <OrderID>39</OrderID>

                  <Client>"NR"</Client>

                  <SellerId>3</SellerId>

                  <Amount>3016.0</Amount>

                  <OrderDate>"2010-08-21"</OrderDate>

                  </Orders>

         <Orders>

</row>

<xml>

针对该文件,用dom4j查询出所有价格在1000-3000,且客户名包含bro字样的订单。JAVA代码如下:

package org.example;

import org.dom4j.Document;

import org.dom4j.Node;

import org.dom4j.io.SAXReader;

import java.util.List;

public class App

{

    public   static void main (String[] args )throws Exception

    {

         SAXReader   saxReader = SAXReader.createDefault();

         Document   doc = saxReader.read("file:\\D:\\xml\\Employees_Orders.xml");

         List<Node>   list=doc.selectNodes("/xml/row/Orders[Amount>1000 and Amount<=3000   and contains(Client,'bro')]")

         int   i=0;

          System.out.println("--------------count of the current   resultSet="+list.size());

          for(Node n:list){

              String OrderID=n.selectSingleNode("./OrderID").getText();

              String Client=n.selectSingleNode("./Client").getText();

              String SellerId=n.selectSingleNode("./SellerId").getText();

              String Amount=n.selectSingleNode("./Amount").getText();

              String   OrderDate=n.selectSingleNode("./OrderDate").getText();

              System.out.println(++i+":"+OrderID+"\t"+Client+"\t"+SellerId+"\t"+Amount+"\t"+OrderDate);

        }

    }

}

上述代码中,/xml/row/Orders 是查询范围,Amount>1000 and Amount<=3000 and contains(Client,'bro')是查询条件(或称谓语)。这种查询语法称为XPathXQuery是超集),已经有二十多年的发展历史。XPath简洁易懂,学习成本低,函数丰富,可以满足多种多样的条件查询需求,常见的有数学函数如absfloor,字符串函数如comparesubstring,日期函数如year-from-datetimezone-from-time等。

dom4j(XPath)在条件查询方面的语法表达能力足够强,但条件查询只是数据计算的冰山一角,完整的数据计算丰富多彩,还应包括排序、去重、分组、聚合、集合、连接等。dom4j并不支持这些计算,总体的语法表达能力比较差。

dom4j在数据源方面的表现一般,虽然支持文件取数,但并不支持WebService/HTTP,而后者才是XML数据源的常态。

部署配置方面是dom4j (XPath)的优点,只需在Maven加入dom4jjaxen即可。

MySQL

历史较久的关系型数据库大多支持XML计算,比如DB2OracleMSSQLMySQL,其中MySQL在实际项目中应用最广。

对于前面的条件查询,可用如下SQL+JAVA代码实现:

package org.example;

import java.io.File;

import java.io.FileInputStream;

import java.sql.*;

public class App

{

    public   static void main(String[] args) throws Exception,

              ClassNotFoundException {

          Class.forName("com.mysql.cj.jdbc.Driver");

          Connection conn = DriverManager

                  .getConnection(

                        "jdbc:mysql://127.0.0.1:3307/test?&useSSL=false&serverTimezone=UTC",

                        "root",   "runqian");

          Statement statement = conn.createStatement();

          statement.execute("drop table if exists testtable");

          statement.execute("CREATE TABLE testtable (testxml MEDIUMTEXT)   ENGINE=InnoDB DEFAULT CHARSET=UTF8");

          statement.execute("insert into testtable   values('"+readFile("D:\\xml\\Employees_Orders.xml")   +"')");

        String   conditionSQL="" +

                  "with recursive old as (" +

                  "select extractvalue(testxml,'/xml/row/Orders[Amount>1000 and   Amount<=3000 and contains(Client,\"bro\")]/OrderID') oneLine1,  " +

                  "    extractvalue(testxml,'/xml/row/Orders[Amount>1000 and   Amount<=3000 and contains(Client,\"bro\")]/Client') oneLine2,  " +

                  "    extractvalue(testxml,'/xml/row/Orders[Amount>1000 and   Amount<=3000 and contains(Client,\"bro\")]/SellerId')   oneLine3, " +

                  "    extractvalue(testxml,'/xml/row/Orders[Amount>1000 and Amount<=3000   and contains(Client,\"bro \")]/Amount') oneLine4, " +

                  "    extractvalue(testxml,'/xml/row/Orders[Amount>1000 and   Amount<=3000 and contains(Client,\"bro\")]/OrderDate') oneLine5  " +

                  "  from testtable" +

                  ")," +

                  "N as ( " +

                  "  select 1 as n " +

                  "  union select n + 1 from   N, old" +

                  "  where n <=   length(oneLine1) - length(replace(oneLine1,' ',''))" +

                  ")" +

                  "select substring_index(substring_index(oneLine1,' ', n),' ',   -1) OrderID," +

                  "    substring_index(substring_index(oneLine2,' ', n),' ', -1) Client,  " +

                  "    substring_index(substring_index(oneLine3,' ', n),' ', -1) SellerId,  " +

                  "    substring_index(substring_index(oneLine4,' ', n),' ', -1) Amount,  " +

                  "    substring_index(substring_index(oneLine5,' ', n),' ', -1) OrderDate  " +

                  "from N, old";

          ResultSet results = statement.executeQuery(conditionSQL);

          printResult(results);

        if   (conn != null)

              conn.close();

    }

    public   static void printResult(ResultSet rs) throws Exception{

        int   colCount=rs.getMetaData().getColumnCount();

          System.out.println();

          for(int i=1;i<colCount+1;i++){

              System.out.print(rs.getMetaData().getColumnName(i)+"\t");

        }

          System.out.println();

          while(rs.next()){

              for (int i=1;i<colCount+1;i++){

                System.out.print(rs.getString(i)+"\t");

            }

              System.out.println();

        }

    }

    public   static String readFile(String fileName)throws Exception{

        File   file = new File(fileName);

        Long   fileLength = file.length();

        byte[]   fileContent = new byte[fileLength.intValue()];

          FileInputStream in = new FileInputStream(file);

          in.read(fileContent);

          in.close();

        return   new String(fileContent, "UTF-8");

    }

 

}

上面代码的逻辑:先在MySQL中创建表testtable,再从Employees_Orders.xml读入xml字符串,然后将xml字符串作为一条记录插入testtable,最后用SQL查询testtable。部分计算结果如下:

OrderID   Client       SellerId    Amount   OrderDate      

49    "SPLI"       5       1050.6     "2010-09-03" 

122  "SPL"        8       2527.2     "2009-12-02" 

140  "OFS"       8       1058.4     "2010-12-18" 

上面的JAVA中,最难理解的是SQL查询部分。其中,用来解析XML的函数是extractvalue,这个函数支持XPath查询语法,可将查询结果(比如所有的订单日期)拼成一个空格分隔的大字符串。为了把这个大字符串拆成小字符串(比如每条记录对应一个订单日期),就需要用到复杂的递归with语句。

前面实现条件查询时,XML是完整未拆分的,其实也可以用拆分XML的办法,即:事先把XML文件拆成员工和订单两部分,再把每部分拆成多条记录并依次入库,最后针对订单表进行条件查询。这样虽然可以大幅简化条件查询SQL,但却使XML失去了灵活表达数据的意义。

前面实现条件查询时,只使用了SQL语句,其实也可以让JAVA参与计算,即:让SQL实现解析XML,让JAVA代码实现一行转N行。这同样会简化SQL,但难点并未消失,只是转移到JAVA上,而且JAVA不擅长条件查询,N行的数据要二次入库才好查询,这又额外增加了处理步骤。

虽然代码复杂,但MySQL的语法表达能力是足够的,可以实现大量的常用计算。比如对订单年份分组,对订单金额汇总。SQL如下:

with recursive old as (

         select   extractvalue(testxml,'/xml/row/Orders/OrderID') oneLine1,

                  extractvalue(testxml,'/xml/row/Orders/Client')   oneLine2,

                  extractvalue(testxml,'/xml/row/Orders/SellerId')   oneLine3,

                  extractvalue(testxml,'/xml/row/Orders/Amount')   oneLine4,

                  extractvalue(testxml,'/xml/row/Orders/OrderDate')   oneLine5

  from   testtable

),

N as (

         select   1 as n

         union   select n + 1 from N, old

  where n   <= length(oneLine1) - length(replace(oneLine1, '',''))

),

query as(

         select       substring_index(substring_index(oneLine1,   '', n),' ', -1) OrderID,

                  substring_index(substring_index(oneLine2,   '', n),' ', -1) Client,

                  substring_index(substring_index(oneLine3,   '', n),' ', -1) SellerId,

          substring_index(substring_index(oneLine4, '', n),' ', -1) Amount,

          STR_TO_DATE(substring_index(substring_index(oneLine5, '', n),' ',   -1),'"%Y-%m-%d"') OrderDate

         from   N, old)

select year(OrderDate),sum(Amount) from query group   by year(OrderDate)

再比如关联员工和订单,取部分字段,SQL代码就更复杂了(多次使用递归查询,导致效率也很低):

with recursive oldOrders as (

         select   extractvalue(testxml,'/xml/row/Orders/OrderID') oneLine1,

                  extractvalue(testxml,'/xml/row/Orders/Client')   oneLine2,

                   extractvalue(testxml,'/xml/row/Orders/SellerId')   oneLine3,

                  extractvalue(testxml,'/xml/row/Orders/Amount')   oneLine4,

                  extractvalue(testxml,'/xml/row/Orders/OrderDate')   oneLine5

 

  from   testtable

),

N as (

         select   1 as n

         union   select n + 1 from N, oldOrders

  where n   <= length(oneLine1) - length(replace(oneLine1, '',''))

),

Orders as(

         select       substring_index(substring_index(oneLine1,   '', n),' ', -1) OrderID,

                  substring_index(substring_index(oneLine2,   '', n),' ', -1) Client,

                  substring_index(substring_index(oneLine3,   '', n),' ', -1) SellerId,

                             substring_index(substring_index(oneLine4,   '', n),' ', -1) Amount,

          STR_TO_DATE(substring_index(substring_index(oneLine5, '', n),' ',   -1),'"%Y-%m-%d"') OrderDate

 

         from   N, oldOrders),

oldEmp as (

         select   extractvalue(testxml,'/xml/row/EId') oneLine1,

                  extractvalue(testxml,'/xml/row/Dept')   oneLine2,

                  extractvalue(testxml,'/xml/row/Name')   oneLine3,

                  extractvalue(testxml,'/xml/row/Gender')   oneLine4

  from   testtable),

N1 as (

         select   1 as n

         union   select n + 1 from N1, oldEmp

  where n   <= length(oneLine1) - length(replace(oneLine1, '',''))

),

Emp as(

         select       substring_index(substring_index(oneLine1,   '', n),' ', -1) EId,

                  substring_index(substring_index(oneLine2,   '', n),' ', -1) Dept,

                  substring_index(substring_index(oneLine3,   '', n),' ', -1) Name,

                             substring_index(substring_index(oneLine4,   '', n),' ', -1) Gender

         from   N1, oldEmp)

select Orders.OrderID,Emp.Name  from Orders,Emp where   Orders.OrderID=Emp.EId

在数据源方面,MySQL表现很弱,不支持WebService\HTTP取数。即使最基本的文件数据源,也需要硬编码才能读取,并在建表入库之后才能计算。

在配置部署方面,MySQL还是非常方便的,只需引入驱动jar包就够了。

Scala

Scala是优秀的结构化计算语言,由于流行范围较广,衍生出大量的第三方库函数,使用Sparkdatabricks这两个函数库,就可以实现XML计算。

对于前面的条件查询,可用如下Scala代码实现:

package test

import com.databricks.spark.xml.XmlDataFrameReader

import org.apache.spark.sql.SparkSession

import org.apache.spark.sql.functions._

object xmlTest {

  def   main(args: Array[String]): Unit = {

    val spark   = SparkSession.builder()

        .master("local")

        .getOrCreate()

    val df =   spark.read

        .option("rowTag", "row")

        .option("inferSchema","true")

        .xml("D:\\xml\\Employees_Orders.xml")

    val Orders   =   df.select(explode(df("Orders"))).select("col.OrderID","col.Client","col.SellerId","col.Amount","col.OrderDate")

    val   condition=Orders.where("Amount>1000 and Amount<=3000 and Client   like'%S%' ")

      condition.show()

  }

}

上面代码先将XML读为多层的DataFrame对象,再用explode函数取出所有订单,之后用where函数完成条件查询。

类似地,Scala可以实现分组汇总,代码如下:

//先去掉OrderDate两端多余的引号

 val   ordersWithDateType= Orders.withColumn("OrderDate",   regexp_replace(col("OrderDate"), "\"",""))
  val   groupBy=ordersWithDateType.groupBy(year(ordersWithDateType("OrderDate"))).agg(sum("Amount"))

同样地,可实现员工和订单之间的关联计算,代码如下:

val df1=df.select(df("Name"),df("Gender"),df("Dept"),explode(df("Orders")))

val   relation=df1.select("Name","Gender","Dept","col.OrderID","col.Client","col.SellerId","col.Amount","col.OrderDate")

从上面代码可以看出,Scala语法表达能力较强,可以完成常用的计算,且代码简短易懂,比MySQL容易掌握。在实现关联计算时,Scala无需预先建立两个二维表,只要直接从多层数据取值,因此代码逻辑比MySQL大幅简化,代码长度比MySQL大幅缩短,且执行效率较高。

Scala的代码之所以简短易懂,主要因为DataFrame支持多层数据,方便表达XML的结构,基于DataFrame的函数也更容易进行多层数据的计算。

在数据源方面,Scala同样表现优秀,不仅有专用函数读取文件中的XML,也支持读取WebService/HTTP等数据源中的XML

在配置部署方面,Scala只需引入databricksSpark(无需部署Spark服务)函数库即可实现XML计算。

集算器 SPL

集算器 SPL是专业的开源结构化计算语言,可以用统一的语法和数据结构计算各类数据源,其中就包括XML集算器 SPL的原理和Scala类似,但与Scala不同的是,集算器 SPL更“轻”,语法更简单。

对于前面的条件查询,只需如下SPL代码即可实现:


A

1

=xml(file("D:\\xml\\Employees_Orders.xml").read(),"xml/row")

2

=A1.conj(Orders)

3

=A2.select(Amount>100 && Amount<=3000   && like@c(Client,"*bro*"))

上面代码先将XML读为多层的序表对象(类似ScalaDataFrame),再用conj函数合并所有订单,之后用select函数完成条件查询。

这段代码可在集算器IDE中调试/执行,也可存为脚本文件(比如condition.dfx),通过集算器JDBC接口在JAVA中调用,具体代码如下:

package Test;
  import java.sql.Connection;
  import java.sql.DriverManager;
  import java.sql.ResultSet;
  import java.sql.Statement;
  public class test1 {
      public static void main(String[]   args)throws Exception {
            Class.forName("com.esproc.jdbc.InternalDriver");
          Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
          Statement statement =   connection.createStatement();
          ResultSet result =   statement.executeQuery("call condition()");
          printResult(result);
          if(connection != null)   connection.close();
      }

}

类似地,SPL可以实现分组汇总,代码如下:

=A2.groups(year(OrderDate);sum(Amount))

        或关联计算:

=A1.new(Name,Gender,Dept,Orders.OrderID,Orders.Client,Orders.SellerId,Orders.Amount,Orders.OrderDate)

从上面代码可以看出,SPL语法表达能力更强,不仅可以完成常用的计算,且代码简短易懂,与JAVA集成时耦合性更低。SPL的序表类型支持多层数据,支持直观的点操作符,在实现关联计算时可直接从多层数据取值,代码更加简练。

SPL语法表达能力更强,经常可以简化多层XML的计算,下面试举一例。

文件book1.xml存储图书信息,其中作者节点有作者名、国籍这两个属性,且有些书有多个作者,部分数据如下:

<?xml version="1.0"?>

<library>

    <book   category="COOKING">

          <title>Everyday Italian</title>

          <author name="Giada De Laurentiis" country="it"   />

          <year>2005</year>

          <info>Hello Italian!</info>

      </book>

    <book   category="CHILDREN">

          <title>Harry Potter</title>

          <author name="J K. Rowling" country="uk"/>

          <year>2005</year>

          <info>Hello Potter!</info>

      </book>

    <book   category="WEB">

          <title>XQuery Kick Start</title>

        <author name="James   McGovern" country="us" />

        <author name="Per   Bothner" country="us"/>

          <year>2005</year>

          <info>Hello XQuery</info>

      </book>

    <book   category="WEB">

          <title>Learning XML</title>

          <author name="Erik T. Ray" country="us"/>

          <year>2003</year>

          <info>Hello XML!</info>

      </book>

</library>

将这个XML整理成结构化二维表,其中作者字段以“作者名[国籍]”的格式呈现,如果某本书有多个作者,则以逗号分隔。最后查询该表,选出2005年的图书。结果应当如下:

title

category

year

author

info

Everyday Italian

COOKING

2005

Giada De Laurentiis[it]

Hello Italian!

Harry Potter

CHILDREN

2005

J K. Rowling[uk]

Hello Potter!

XQuery Kick Start

WEB

2005

James McGovern[us],Per Bothner[us]

Hello XQuery

这道题有一定难度,用SPL来计算可以明显简化,具体代码如下:


A

1

=file("D:\\xml\\book1.xml")

2

=xml@s(A1.read(),"library/book").library

3

=A2.new(category,book.field("year").ifn():year,book.field("title").ifn():title,book.field("lang").ifn():lang,book.field("info").ifn():info,book.field("name").select(~).concat@c():name,book.field("country").select(~).concat(","):country)

4

=A3.new(title,category,year,(lang,name.array().(~+"[")++country.array().(~+"]")).concat@c():author,info)

5

=A4.select(year==2005)

在数据源方面,SPL表现优秀,不仅有专用函数读取文件中的XML,也支持读取WebService/HTTP等数据源中的XML

部署配置方面,读写计算XMLSPL的基本功能,无需额外配置。如果要在JAVA中集成集算器,只需引入相关jar包,过程也很简单。

通过上述比较可以看出:在语法方面,集算器 SPL表达能力最强,可以简化多层XML的计算;Scala的表达能力较强,可以完成常用的计算;MySQL的表达能力虽然够用,但代码过于复杂,除非遇到简单结构的XML可拆分入库的场景;dom4j表达能力不足,无法完成常用计算,仅适用于单纯条件查询的场景。在数据源方面,集算器 SPLScala明显更实用。在部署配置方面,dom4jMySQL较简单,另两种也不难。