WebService/Restful 的后处理技术

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

Java/C#

高级语言的用途极为广泛,用作WebService/Restful的后处理技术是很自然的事情,比如JAVA中的类库JsonPath\fastjson\jackson,C#中的类库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 restTemplate、Apache httpclient;有些代码简单,但稳定性不足,常见的有JourWon httpclientutil 、Arronlong httpclientutil。比如用Arronlong httpclientutil从restful取数,代码如下:

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));

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

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代码从Restful取JSON,并用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等,高级语言一般用JAVA或C#。具体SQL都很常见,这里不再列出。

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

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

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

Python

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

比如从Restful取JSON,并实现条件查询,代码如下:

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对象;第三方类库Pandas的dataframe对象可实现条件查询。

类似地,配合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

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

比如从Restful取JSON,并实现条件查询,代码如下:

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对象,再转为DataFrame。DataFrame可以存储多层数据,可用explode函数取出某层数据(如Orders),再用select函数取出所需字段,最后完成计算。

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

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

集算器  SPL

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

比如从Restful取JSON,并实现条件查询,可用如下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个字段:horseId、ownerColours、trainer,其中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数据格式方面,SPL和Scala表现最好,但后者要依赖不稳定的第三方类库,其他技术手段表现较差。