json 的计算类库

Json 比普通的二维数据计算起来更困难,这种情况下要用计算类库完成,本文将比较四类 Json 的计算库,包括 JsonPath、SQLite、Scala、集算器 SPL,重点考察这些工具在语法表达、部署配置、数据源方便的差异,详情点击json 的计算类库

Json不仅体积小巧,而且能用多层结构灵活表达数据关系,但多层结构比二维结构复杂,计算起来不太方便,为了解决这个矛盾,json计算类库应运而生。下面将比较几类常见的json计算类库,重点是语法表达、部署配置、数据源方便的区别。需要说明的是,Gson/ fastjson/ jackson等类库侧重解析维护,缺乏计算能力,不在此次比较之列。

JsonPath

JsonPath的目标是做json上的“XPath”,虽然当下离目标尚有距离,但已经实际应用在不少项目中,且经常和维护类的json类库搭配使用。

下面举例说明JsonPath的语法表达能力。文件EO.json存储一批员工信息,以及属于员工的多个订单,部分数据如下:

[{

      "_id": {"$oid":   "6074f6c7e85e8d46400dc4a7"},

      "EId": 7,"State":   "Illinois","Dept": "Sales","Name":   "Alexis","Gender": "F","Salary":   9000,"Birthday": "1972-08-16",

      "Orders": [

         {"OrderID":   70,"Client": "DSG","SellerId":   7,"Amount": 288,"OrderDate": "2009-09-30"},

         {"OrderID":   131,"Client": "FOL","SellerId":   7,"Amount": 103.2,"OrderDate": "2009-12-10"}

    ]

}

{

      "_id": {"$oid":   "6074f6c7e85e8d46400dc4a8"},

      "EId": 8,"State": "California", ...

}]

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

package org.example;

import com.jayway.jsonpath.Configuration;

import com.jayway.jsonpath.JsonPath;

import java.io.File;

import java.io.FileInputStream;

import java.util.ArrayList;

public class App1

{

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

    {

        String   str=file2str("D:\\json\\EO.json");

        Object   document = Configuration.defaultConfiguration().jsonProvider().parse(str);

          ArrayList l=JsonPath.read(document, "$[*].Orders[?(@.Amount>500   && @.Amount<2000 && @.Client =~ /.*?bro.*?/i)]");

          System.out.println(l);

    }

    public   static String file2str(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");

    }

}

代码中,@.Amount>500 && @.Amount<2000是区间查询条件,@.Client =~ /.*?bro.*?/i是模糊查询条件,可以看出JsonPath的优点是查询语句较短,缺点是不太成熟,比如模糊查询还要借助正则表达式,而不是更易用的函数(比如SQL里的like)。事实上,JsonPath只支持最简单的计算,比如条件查询和聚合,其他大部分常用计算都不支持,包括分组汇总、关联、集合计算等。

JsonPath的不成熟还体现在数据源方面。从上述代码可以看出,即使最基本的文件数据源,JsonPath也需要硬编码访问,其他数据源就更不支持了。

部署配置方面是JsonPath唯一的优点,只需在Maven加入json-path即可。

类似的计算库还有几个,虽然功能略有区别,但由于底层原理类似,导致成熟度都不高。比如fastJsonJsonPath的基础上补充了like函数,提高了易用性但降低了稳定性。

SQLite

SQLite是嵌入式内存数据库,由于轻量小巧集成方便,常被嵌入编程语言中。虽然体积很小,但SQLite的能力并不差,支持json计算就是其中之一。

比如前面的条件查询,可用如下JAVA代码实现:

package test;

 

import java.io.File;

import java.io.FileInputStream;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.ResultSet;

import java.sql.Statement;

 

public class Main {

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

            Connection connection =   DriverManager.getConnection("jdbc:sqlited/ex1");

              Statement statement = connection.createStatement();

              statement.execute("create table datatable ( path string , data   json1)");

              String sql="insert into datatable values('1',   json('"+file2str("D:\\json\\EO.json") +"'))";

              statement.execute(sql);

              sql="select value from(" +

                      "select value" +

                      "from datatable, json_tree(datatable.data,'$')" +

                      "where type ='object'and parent!=0" +

                      ")where json_extract( value,'$.Amount') >500  and json_extract(value,'$.Amount') <2000   and json_extract(value,'$.Client') like'%bro%'";

            ResultSet   results  = statement.executeQuery(sql);

              printResult(results);

              if(connection != null) connection.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();

            }

        }

}

上面代码先在SQLite中建立表datatable,之后从文件读入json,并当做一条记录插入datatable,最后用SQL语句进行条件查询。SQL中的json_tree函数可将多层结构的json解析为二维结构(类似表),json_extract用来从二维结构的json(类似记录)中取字段。

类似地,SQLite也可以实现分组汇总,SQL如下:

select strftime('%Y',Orderdate),sum(Amount) from(
          select json_extract(  value,'$.OrderDate')OrderDate,json_extract(value,'$.Amount')Amount
          from datatable, json_tree(  datatable.data, '$')
          where type = 'object' and   parent!=0
          )group by   strftime('%Y',Orderdate)

也可实现员工和订单之间的关联计算,SQL如下:

with base as (

         select   value,id,parent,type

         from   datatable, json_tree(datatable.data, '$')

),emp_orders as(

         select   orders.value o,emp.value e from base ordersArr,base orders,base emp

         where   ordersArr.parent=emp.id and orders.parent=ordersArr.id and emp.parent=0 and   emp.type='object'

)select json_extract( o,'$.OrderID'),json_extract(  o,'$.Client'),json_extract(o,'$.Amount'),json_extract(o,'$.OrderDate'), json_extract(  e,'$.Name'), json_extract(e,'$.Gender'),json_extract(e,'$.Dept')

from emp_orders

从上面代码可以看出,SQLite语法表达能力较强,可以完成常用的计算。同时也应该看出来,SQLite代码冗长难懂,掌握起来难度较大。比如”select … from 表名,函数 where…”与常见的SQL语句结构上不同,程序员不易理解。再比如关联查询的代码很长,表之间的关系较为复杂,程序员很难看懂。

SQLite的代码之所以冗长难懂,是因为Json是多层数据,而SQL只擅长计算二维结构化数据,并不能直接计算Json。为了计算Json,必须把多层Json先降为二维结构才行,也就是用json_tree函数(包括代码中未出现的json_each)。用二维数据和二维计算语言(SQL)去模拟多层数据的计算,冗长难懂在所难免。

在数据源方面,SQLite表现很弱,需要硬编码才能读取最基本的文件数据源,并在建表入库之后才能计算。

在配置部署方面,SQLite还是非常方便的,只需引入一个jar包即可实现。

Scala

Scala是比较流行的结构化计算语言,也是较早支持Json计算的语言之一。Scala先从数据源读取Json,存储为DataFrame数据对象(或RDD),再用DataFrame的通用计算能力完成计算。

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

package test
  import scala.io.Source
  import org.apache.spark.sql.SparkSession
  import org.apache.spark.sql.functions.{asc, desc}
  import org.apache.spark.sql.types._
  import org.apache.spark.sql.functions._
  import org.apache.spark.sql.DataFrame
  object JTest {
    def main(args: Array[String]): Unit =   {
        val spark = SparkSession.builder()
        .master("local")
        .getOrCreate()
      val df=spark.read.json("D:\\data\\EO.json")
      val Orders =   df.select(explode(df("Orders"))).select("col.OrderID","col.Client","col.SellerId","col.Amount","col.OrderDate")
      val condition=Orders.where("Amount>500   and Amount<2000 and Client like'%bro%' ")
      condition.show()
      }
  }

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

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

val   groupBy=Orders.groupBy(year(Orders("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语法表达能力较强,可以完成常用的计算,且代码简短易懂,比SQLite容易掌握。在实现关联计算时,Scala并没有特意使用关联函数(虽然Scalajoin函数),而是直接从多层数据取值,这就使逻辑关系变得简单,代码长度显著缩短。

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

在数据源方面,Scala同样表现优秀,不仅有专用函数读取文件中的json,也支持读取MongoDBElasticsearchWebService等多种数据源中的Json

在配置部署方面,Scala的基本类库就支持json计算,无需额外配置(MongoDB等数据源取数须额外配置)。

集算器 SPL

集算器 SPL是专业的开源结构化计算语言,原理和Scala类似,可以用统一的语法和数据结构计算各类数据源,其中就包括json。但集算器 SPL更“轻”,语法更简单,且提供耦合性较低的JDBC接口。

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


A

1

=json(file("D:\\data\\EO.json").read())

2

=A1.conj(Orders)

3

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

上面代码先将Json读为多层的序表对象(类似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也支持类似SQL的用法,即无须脚本文件,直接将SPL代码嵌入JAVA,代码如下:

ResultSet result = statement.executeQuery("=json(file(\"D:\\data\\EO.json\").read()).conj(Orders).select(Amount>500   && Amount<=3000 && like@c(Client,\"*bro*\"))");

类似地,SPL可以实现分组汇总和关联计算,代码如下:


A

B

1

=json(file("D:\\data\\EO.json").read())


2

=A1.conj(Orders)


3

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

/条件查询

4

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

/分组汇总

5

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

/关联计算

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

SPL语法表达能力更强,经常可以简化多层json的计算,比如:文件JSONstr.jsonrunners字段是子文档,子文档有3个字段:horseIdownerColourstrainer,其中trainer含有下级字段trainerId ownerColours是逗号分割的数组。部分数据如下:

[

   {

      "race": {

            "raceId":"1.33.1141109.2",

            "meetingId":"1.33.1141109"

      },

      ...

        "numberOfRunners": 2,

      "runners":   [

          {     "horseId":"1.00387464",

                "trainer": {

                    "trainerId":"1.00034060"

                },

            "ownerColours":"Maroon,pink,dark blue."

            },

            {   "horseId":"1.00373620",

                "trainer": {

                    "trainerId":"1.00010997"

                },

            "ownerColours":"Black,Maroon,green,pink."

            }

      ]

   },

...

]

现在要按 trainerId分组,统计每组中 ownerColours的成员个数。可用下面的SPL实现本计算。


A

1

=json(file("/workspace/JSONstr.json").read())

2

=A1(1).runners

3

=A2.groups(trainer.trainerId; ownerColours.array().count():times)

在数据源方面,集算器 SPL表现优秀,不仅有专用函数读取文件中的json,也支持读取MongoDBElasticsearchWebService等多种数据源中的Json

最后说下集算器的配置。读写计算JsonSPL的基本功能,无需额外配置(MongoDB等数据源除外)

通过上述比较可以看出:在语法方面,集算器 SPL表达能力最强,可以简化多层Json的计算;Scala的表达能力较强,可以完成常用的计算;SQLite的表达能力虽然够用,但代码难写难读;JsonPath表达能力不足,无法完成常用计算。在数据源方面,集算器 SPLScala较为丰富, JsonPath表现较差,SQLite还不如JsonPath。在部署配置方面,SQLite最简单,其他三种也不难。