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表现较好,但后者要依赖不稳定的第三方类库,其他技术手段表现较差。