WebService/Restful 的后处理技术

WebService/Restful 的数据格式采用了 JSON 或 XML 这类通用、多层的结构化文本,但多层格式要比传统的二维格式复杂,取数后再处理的难度也大,本文比较五类 WebService/Restful 的后处理技术,其中集算器 SPL 的数据对象支持多层数据格式,语法表达能力强,可以简化 Json/XML 的计算。点击WebService/Restful 的后处理技术了解详情。

WebService/Restful广泛应用于程序间通讯,如微服务、数据交换、公共或私有的数据服务等。之所以如此流行,主要是因为WebService/Restful的数据格式采用了通用的结构化文本,而且支持多层,可承载足够丰富和足够通用的信息。但多层格式要比传统的二维格式复杂,取数后再处理的难度也大。下面将比较常见的几类WebService/Restful后处理技术,重点考察多层JSON的计算,也涉及数据源接口、 XML数据格式等方面。

Java/C#

高级语言的用途极为广泛,用作WebService/Restful的后处理技术是很自然的事情,比如JAVA中的类库JsonPath\fastjson\jacksonC#中的类库Newtonsoft\MiniJSON\SimpleJson。下面以语法表达能力最强的JsonPath为例说明其用法。

Restful网址返回员工及其订单,格式为多层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计算该JSON串,查询出所有价格在1000-2000,且客户名包含business字样的订单。关键代码如下:

String JsonStr=…        //省略JSON串获取的过程

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

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

代码中,@.Amount>1000 && @.Amount<2000是区间查询条件,@.Client =~ /.*?business.*?/i是模糊查询条件。可以看出JsonPath在语法表达方面的优点是代码较短,可以用类似SQL的语法实现区间查询。

再说语法表达方面的缺点。从细节看,JsonPath的语法不太成熟,模糊查询还要借助正则表达式,而不是易用的函数(比如SQL里的like函数)。从全局看,JsonPath的计算能力很弱,只支持最简单的计算比如条件查询和聚合,其他大部分常用计算都不支持,包括分组汇总、关联、集合计算等。JsonPath的计算能力虽然很弱,但在高级语言的类库中已经算最强了,jackson\fastjson等类库还不如它。如果只是最简单的维护工作,比如微服务客户端,用JsonPath较适合,如果要进行一般的计算处理,最好改用其他技术手段。

高级语言语法表达能力之所以孱弱,主要因为数据对象不够专业,无法描述JSON这种多层结构,也就无法据此构建专业的语法和丰富的函数。

取数接口方面,JsonPath自己没有实现接口,只能依靠第三方类库或直接硬编码取数。这样的类库较多,有些比较成熟,但结构过于沉重,常见的有Spring restTemplateApache httpclient;有些代码简单,但稳定性不足,常见的有JourWon httpclientutil Arronlong httpclientutil。比如用Arronlong httpclientutilrestful取数,代码如下:

String path= "http://127.0.0.1:6868/api/emp_orders";

String JsonStr= com.arronlong.httpclientutil.HttpClientUtil.get(com.arronlong.httpclientutil.common.HttpConfig.custom().url(path));

这些第三方类库殊途同归,底层都会封装JDKHttpURLConnection类,上面的代码等价于下面的硬编码:

String path = "http://127.0.0.1:6868/api/emp_orders";
  URL url = new URL(path);
  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  conn.setRequestMethod("GET");
  conn.setConnectTimeout(5000);
  StringBuilder builder = new StringBuilder();
  if (conn.getResponseCode() == 200) {
      System.out.println("connect   ok!");
      InputStream in =   conn.getInputStream();
      InputStreamReader isr = new   InputStreamReader(in);
      BufferedReader br = new   BufferedReader(isr);
      String line;
      while ((line = br.readLine()) !=   null) {
          builder.append(line);
      }
      br.close();
      isr.close();
      System.out.println("below is   content from webservice");
      System.out.println(builder);
  } else {
      System.out.println("connect   failed!");
  }

String JsonStr=builer.toString();

在数据格式方面,JsonPath(及前面列出的其他类库)只支持JSON,不支持XML,表现同样不好。

SQL

在结构化数据计算方面,关系型数据库有成熟的语法和丰富的函数,很多人会把多层数据转化为结构化数据(二维结构),再借助SQL的能力进行处理。

具体实现上有两种方式。第一种:先用高级语言从WebService/Restful取到JSON串;在同一段程序里,立刻在数据库建立含有JSON类型字段的表;之后用Insert  语句将JSON串插入该表;最后用含有JSON相关函数的SQL语句查询该表。

比如,用JAVA代码从RestfulJSON,并用SQLite实现条件查询,代码如下:

           String   JsonStr=…      //省略JSON串获取的过程  

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

              Statement statement = connection.createStatement();

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

              String sql="insert into datatable values('1', json('"+JsonStr   +"'))";

              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') >1000  and json_extract(value,'$.Amount')   <2000 and json_extract(value,'$.Client') like'%business%'";

            ResultSet results  = statement.executeQuery(sql);

              printResult(results);

              if(connection != null) connection.close();

上面代码实现条件查询时,虽然用到了SQL的能力,但主要还是借助了JSON函数json_extract(类似的函数还有json_tree等)。

除了条件查询,也可以实现分组汇总,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

从上面代码可以看出,这种方式的好处是结构轻便,时效性强,适合数据量少、无需历史数据、数据结构不固定的情况。坏处是代码冗长难懂,代码难度与JSON串复杂度尤其是层数相关;大量借助JSON函数,很难发挥常见SQL的全部能力;JSON函数用法特殊,短时间难以掌握。比如”select … from 表名,函数 where…”,这种写法与常见的SQL语句结构上不同,程序员不易理解。再比如关联查询的代码很长,表之间的关系较为复杂,程序员很难看懂。另外,有些老版本的数据库不支持JSON函数,有些数据库虽然支持JSON函数,但用法与SQLite完全不同(比如Oracle)。

上面代码之所以冗长难懂,主要因为SQL的数据对象是二维结构,不直接支持多层数据的计算,硬要用二维结构去计算多层数据,必然面临大量困难。

 

第二种方式:同样用ETL工具或高级语言从WebService/Restful取出JSON串;再将JSON串拆分为多个二维表,分别写入数据库表;最后用不含JSON函数的通用SQL对库表进行计算。ETL工具一般用informatica\datastage\kettle等,高级语言一般用JAVAC#。具体SQL都很常见,这里不再列出。

这种方式的缺点是结构沉重、时效性差,适合数据量较大、定时追加、数据结构变化不大的情况。但这种方式有个最大的好处,无需JSON相关函数,可以充分借助常见SQL的能力,且SQL难度与JSON串的复杂度无关。

取数接口方面,ETL工具大多支持WebService/Restful取数,表现较好;JAVA/C#等高级和语言要硬编码或用第三方类库,代码复杂且学习成本高。

XML数据格式方面,第一种方式要用数据库存储XML SQLite不支持XML数据类型,但Oracle\MSSQL数据库等支持,且OracleMSSQLXML函数不通用,总体来说支持力度较差且混乱。对第二种方式来说,ETL工具大多支持XML,表现较好;高级语言要硬编码实现,支持较差。

Python

Python有许多优秀的第三方类库,有用于访问HTTPrequests,用于数学统计的numpy,以及最重要的,用于结构化数据计算的PandasPandas支持多种数据源,其中就包括JSON格式。将这些第三方类库组合起来,就可以处理来自WebService/Restful的数据。

比如从RestfulJSON,并实现条件查询,代码如下:

import requests

import numpy as np

import pandas as pd

from pandas import json_normalize

resp=requests.get(url="http://127.0.0.1:6868/api/emp_orders")

JsonOBJ=resp.json()

df=json_normalize(JsonOBJ, record_path=['Orders'])

#dataframe不能自动识别日期类型

df['OrderDate']=pd.to_datetime(df['OrderDate'])

result=df.query('Amount>1000 and Amount<2000   and contains("business")')

上面代码中,第三方类库requests可访问URL,并支持将字符串转为JSON对象;第三方类库Pandasdataframe对象可实现条件查询。

类似地,配合numpy类库也可实现分组汇总:

result=df.groupby(dfu['OrderDate'].dt.year)['Amount'].agg([len,   np.sum])

以及员工和订单之间的关联计算:

df=json_normalize(JsonOBJ,record_path=['Orders'],meta=['Name','Gender','Dept'])

result=df[['Name','Gender','Dept','OrderID','Client','SellerId','Amount','OrderDate']]

可以看到Python在语法表达方面的优点是代码简练,结构化数据计算能力较强。同时也应该看到,dataframe是二维数据对象,不能按层级取数,不支持多层数据的计算,要用json_normalize函数将多层数据转为二维数据才能计算,这个转换过程相当于ETL工具和高级语言将JSON解析为多个二维表的过程,如果层级太多,转换过程可能比后续的计算过程更复杂。Python还有个缺点,无法用官方类库实现WebService/Restful的后处理,必须依赖多个第三方类库才行。第三方类库不会为彼此负责,在版本兼容性和自身的稳定性上都存在不小的风险。

在数据格式方面,Pandas不支持XML,且没有json_normalize这么方便的函数将多层XML转为二维dataframe,程序员只能硬编码转换,实现过程很繁琐。

Scala

SparkScala最重要的类库,除了用作大数据框架,也可以单独作为WebService/Restful的后处理技术。一般的做法是,Spark先从数据源读取Json/xml,再转换为SparkDataFrame数据对象,之后便可利用DataFrame完成计算。

比如从RestfulJSON,并实现条件查询,代码如下:

package test

import org.apache.spark.sql.SparkSession

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

object JTest {

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

    val spark   = SparkSession.builder()

        .master("local")

        .getOrCreate()

    val result   = scala.io.Source.fromURL("http://127.0.0.1:6868/api/emp_orders").mkString

    val   jsonRdd = spark.sparkContext.parallelize(result :: Nil)

    val   df=spark.read.json(jsonRdd)

    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'%business%' ")

      condition.show()

}

类似地,也可以实现分组汇总:

    val   groupBy=Orders.groupBy(year(Orders("OrderDate"))).agg(count("OrderID"),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")

上面代码中,JSON串先转为RDD对象,再转为DataFrameDataFrame可以存储多层数据,可用explode函数取出某层数据(如Orders),再用select函数取出所需字段,最后完成计算。

Scala在语法风格上优点较多,DataFrame可存储多层数据,可以与JSON结构很好地契合,可以用点号直观地按层级取数,可以方便地计算JSON数据。

在数据格式方面,Spark虽然对JSON支持良好,但并不支持XML。要想让Spark支持XML,必须引入另一个类库databricks。两个类库虽然能配合使用,但稳定性会差许多。

集算器  SPL

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

比如从RestfulJSON,并实现条件查询,可用如下SPL代码实现:


A

1

=json(httpfile("http://127.0.0.1:6868/api/emp_orders").read())

2

=A1.conj(Orders)

3

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

上面代码先读取字符串,再用json函数转为多层的序表对象,再用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(httpfile(\"http://127.0.0.1:6868/api/emp_orders\").read()).conj(Orders).select(Amount>1000   && Amount<=3000 && like@c(Client,\"*bro*\"))");

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


A

B

3


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的计算,比如:JSON串的runners字段是子文档,子文档有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串的过程)

2

=A1(1).runners

3

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

最后说下数据格式,SPL既支持 JSON也支持 XML,且有较强的语法一致性。比如取天气预报WebService的接口描述文件,再根据接口描述查询省份列表,并将返回的XML结果转化为序表:


A

1

=ws_client("http://www.webxml.com.cn/WebServices/WeatherWebService.asmx?wsdl")

2

=ws_call(A1,"WeatherWebService":"WeatherWebServiceSoap":"getSupportProvince")

 

通过上述比较可以看出:在语法风格方面,SPL可以简化多层Json的计算,表达能力最强;Scala的表达能力较强,支持多层JSON的计算;Python在二维数据方面与Scala相当,但不直接支持多层数据;SQL的表达能力虽然够用,但代码难写难读;JAVA/C#表达能力不足,无法完成常用计算。在XML数据格式方面,SPLScala表现最好,但后者要依赖不稳定的第三方类库,其他技术手段表现较差。