代码调用SPSS执行分析
一、 背景
由于项目需要使用到最优尺度回归算法对数据进行分析,但是该算法是闭源的,而SPSS内置了该分析算法,所以就打算通过代码操控SPSS Statistics来实现功能,而SPSS网上资源很少,目前我找到了相关java、R、python的外部接口的资料,由于本身是java程序员,继而对java调用spss进行了研究。由于我们的项目是桌面端的使用C#编写,后面大致研究了一下C#通过cmd调用java并获取结果。
二、 开发环境
- idea 2020
- 无maven
- jdk 1.8
- SPSS Statistics 24
这里需要注意一下 ,SPSS是在其安装目录\IBM\SPSS\Statistics\24里内置了JRE以及spssjavaplugin.jar,这样我们只需要在客户电脑上安装SPSS就有了代码的运行环境了
三、 问题与解决
1. 如何通过java调用spss?
SPSS给我们提供了spssjavaplugin.jar,其中提供了api供我们访问调用,但是有一个问题是这个jar包是不能移动的,这里猜测他使用了相对路径调用了spss的一些组件。以下是我的代码:
// 这一部分是通过命令行获取一些自变量、因变量以及excel数据的参数,可以忽略
Gson gson = new Gson(); // GSON框架
// 这里直接用了数据做测试
JsonRootBean jsonRootBean = gson.fromJson("{'varibles':['BJHF','CJTLX','SYYLX','CCCDJLT74','DZCLL'],'depVarible':'LCYAVG','excelPath':'C:\\\\Users\\\\xxc\\\\Desktop\\\\TrainData.xlsx'}".replace("'","\""), JsonRootBean.class);
// 自变量
String[] varibles = new String[jsonRootBean.getVaribles().size()];
System.arraycopy(jsonRootBean.getVaribles().toArray(),0,varibles,0,varibles.length);
// 因变量
String depVarible = jsonRootBean.getDepVarible();
// 我是用的Excel格式的数据文件,c#将数据转换成excel,然后调用java去分析返回结果
String excelPath = jsonRootBean.getExcelPath();
// 这里合并了一下两个数组用于后面字符串拼接
String[] allVaribles = combine(varibles,new String[] {depVarible});
// 这是最优尺度回归的命令
// 中间部分其实才是真正的命令,而第一二行是指定命令的执行结果输出到指定的地方,这里的OXML可以让我们在工作空间去提取,也就是让下面的第三步得以执行,其中XMLWORKSPACE要与下面的handle变量
String command = "OMS SELECT TABLES" +
"/DESTINATION FORMAT=OXML XMLWORKSPACE='desc_table'\n" +
"VIEWER = NO.\n" + "GET DATA \n" +
" /TYPE=XLSX \n" +
" /FILE='" + excelPath + "' \n" +
" /SHEET=name 'TrainData' \n" +
" /CELLRANGE=FULL \n" +
" /READNAMES=ON \n" +
" /DATATYPEMIN PERCENTAGE=95.0 \n" +
" /HIDDEN IGNORE=YES. \n" +
"EXECUTE. \n" +
"CATREG VARIABLES=" + String.join(" ",allVaribles) + "\n" +
" /ANALYSIS=" + depVarible + "(LEVEL=SPORD,DEGREE=2,INKNOT=2) WITH " + String.join("(LEVEL=SPORD,DEGREE=2,INKNOT=2) ",varibles).trim() + "(LEVEL=SPORD,DEGREE=2,INKNOT=2)" + " \n" +
" /MISSING=" + String.join("(LISTWISE) ",allVaribles) + "(LISTWISE) \n" +
" /MAXITER=100 \n" +
" /CRITITER=.00001 \n" +
" /PRINT=R COEFF ANOVA \n" +
" /INITIAL=NUMERICAL \n" +
" /PLOT=NONE \n" +
" /REGULARIZATION=NONE \n" +
" /RESAMPLE=NONE.\n" +
"OMSEND.";
// 第一步: 启动SPSS,后台运行
StatsUtil.start();
// 设置为false就不会将分析结果打印,默认true,正常用于调试查看结果
StatsUtil.setOutput(true);
// 第二步:执行命令
StatsUtil.submit(command);
// 第三步: 取值
// 这是OXML的表名,在命令中指定了
String handle = "desc_table";
// OXML的根路径
String context = "/outputTree";
// 通过XPath来获取到相关的数据
String xpath = "//pivotTable[@subType='ANOVA']" +
"/dimension[@axis='row']" +"/category[@text='Regression']" +
"/dimension[@axis='column']" +
"/category[@text='Sum of Squares']" +"/cell/@text";
// 获取处理结果
String[] result = StatsUtil.evaluateXPath(handle, context, xpath);
System.out.println(result[0]);
StatsUtil.deleteXPathHandle(handle);
// 第四步: 关闭SPSS
StatsUtil.stop();
上面的代码可能看的不是很明白,不过目前你只需要知道如何启动关闭SPSS,如何给SPSS传递命令即可。
2. 如何获取到相关算法的命令?
我们可以先将数据导入SPSS,然后通过人工操作的方式选择算法,设置变量进行分析,SPSS在显示分析结果的同时会显示执行的命令,如下:
我们可以将以上命令复制到java代码中,通过submit即可获得同样的结果(注意一下excel文件的位置)。
3. 为什么使用excel
我觉得excel比较容易读写,可以将数据库中的内容写入excel表格然后分析,如果你有更好的方式也可以,我尝试将数据转换为sav,但是网上不是很多相关资料。还有一点我们的项目是客户端的,所以可以通过excel的路径进行加载,而不需要网络传输。
4. spss命令
SPSS命令我也是不懂的,但是我通过看spss java plugin的文档结合spss命令的文档,得知了OMS的作用以及如何提取分析结果的内容。具体看我第1点的代码。如果感兴趣可以去看看IBM SPSS Java Plugin使用指南(官方英文版) ,SPSS 25 语法命令参考。
5. 外部jar问题
由于spssjavaplugin.jar是外部的不能移动的,于是我试图将它加入CLASSPATH,但是最终没有成功,目前我也不知道什么原因,如果有懂的老哥可以留言说一下。
- 我刚开始是打成jar包,然后java -jar 运行,这种情况只有我的jar包与plugin在同一个路径才有用,因为打jar包的时候我用idea配置的是不复制进jar包,而是通过MANIFEST.MF链接,解压可以发现里面的
Class-Path: spssjavaplugin.java
,也就是说我运行jar包的时候他会在当前路径找这个plugin然后加载到虚拟机。由此我做了更多的一些研究,我想通过绝对路径去引用这个jar包,但是我写了绝对路径然后重新打成jar包之后要么就是空指针异常,要么就是ClassDefNotFound.具体可以看java -jar命令运行jar包时指定外部依赖jar包,这里面的方法我都试了,都没有用,然后我找到了-jar参数运行应用时,设置classpath的方法,虽然没有解决问题,但是我大概知道原因了。
因为使用“-jar”选项(形如:java -jar xxx.jar )来运行一个可执行的jar包时,jar包会覆“-cp”的值。
换句话说,-jar 后面所跟的jar包的优先级别最高。如果指定了-jar选项,所有环境变量和命令行制定的搜索路径都将被忽略。JVM APPClassloader将只会以jar包为搜索范围.
2. 于是我想着查看idea是如何运行代码的,毕竟他能运行,然后我发现了第一行的执行命令
具体来看是这样的,可以复制看看,里面起作用的是-classpath(与-cp功能一样)选项将spssjavaplugin.jar放入了classpath中由jvm加载。可以注意到这里并不是调用jar的方式运行的,而是使用class文件,
"C:\Program Files\Java\jdk1.8.0_191\bin\java.exe" -Dvisualvm.id=257640514635900 -javaagent:C:\Users\xxc\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\212.5284.40\lib\idea_rt.jar=56314:C:\Users\xxc\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\212.5284.40\bin -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_191\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\rt.jar;C:\Users\xxc\IdeaProjects\statistics\out\production\statistics;C:\IBM\SPSS\Statistics\24\spssjavaplugin.jar;C:\Users\xxc\IdeaProjects\statistics\lib\gson-2.8.9.jar" com.company.Test1
于是我直接将build获得的class文件整个目录拷贝使用,而不使用jar包,然后通过命令指定classpath成功实现,这个statistics就是最终的项目,最终执行命令
C:\\IBM\\SPSS\\Statistics\\24\\JRE\\bin\\java -cp C:\\IBM\\statistics;C:\\IBM\\statistics\\gson.jar;C:\\IBM\\SPSS\\Statistics\\24\\spssjavaplugin.jar; com.company.Test1
这里使用的是SPSS中的JRE运行的,同时将gson和spssjavaplugin以及我的项目路径加入到了classpath,这样我运行java命令这些class就能都被加载,如果没有C:\\IBM\\statistics
那么就找不到我的启动类了。
6. C# 如何调用java
我是通过Process类调用cmd,然后cmd执行命令然后获取到输出这种方式实现的,具体看代码如下
// 执行命令返回输出的方法
private static string RunCmd(string command)
{
//實例一個Process類,啟動一個獨立進程
Process p = new Process();
//Process類有一個StartInfo屬性,這個是ProcessStartInfo類,包括了一些屬性和方法,下面我們用到了他的幾個屬性:
p.StartInfo.FileName = "cmd.exe"; //設定程序名
p.StartInfo.Arguments = "/c " + command; //設定程式執行參數
p.StartInfo.UseShellExecute = false; //關閉Shell的使用
p.StartInfo.RedirectStandardInput = true; //重定向標準輸入
p.StartInfo.RedirectStandardOutput = true; //重定向標準輸出
p.StartInfo.RedirectStandardError = true; //重定向錯誤輸出
p.StartInfo.CreateNoWindow = false; //設置不顯示窗口
p.Start(); //啟動
//p.StandardInput.WriteLine(command); //也可以用這種方式輸入要執行的命令
//p.StandardInput.WriteLine("exit"); //不過要記得加上Exit要不然下一行程式執行的時候會當機
string output = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
Console.WriteLine(err);
p.WaitForExit();
return output; //從輸出流取得命令執行結果
}
// 使用方法
Console.WriteLine(RunCmd("C:\\IBM\\SPSS\\Statistics\\24\\JRE\\bin\\java -cp C:\\IBM\\statistics;C:\\IBM\\statistics\\gson.jar;C:\\IBM\\SPSS\\Statistics\\24\\spssjavaplugin.jar; com.company.Test1")) ;
7. 如何传参
我是通过json进行交互的,但是因为cmd会屏蔽掉**"**,所以呢,我通过替换将双引号转换为单引号,然后java那边再转换回来即可。这样直接将转义后的json串放在命令的最后即可(需要与前面隔一个空格)
三、 当前问题
- -Xbootclasspath如何使用才能有效?
由于windows和linux分隔符以及其他的一些差异,一直尝试使用这个选项但是仍然不加载相关jar,可以通过-verbose:class
选项来查看加载的类和jar - 为什么要打成jar包?
通过查资料知道了jar包的问题,jar包能将这些文件以及变成一个文件,方便传输以及运行,但是在我这个场景下似乎不那么友好 - CLASSPATH环境变量在什么时候才能生效?
我尝试通过java运行class文件,我将spssjavaplugin的路径配在了环境变量中,仍然无效,配置如下:
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;C:\IBM\SPSS\Statistics\24\spssjavaplugin.jar;
- 有没有C#能够直接调用的API?
三、参考资料
IBM SPSS Java Plugin使用指南(官方英文版)
SPSS 25 语法命令参考
-jar参数运行应用时,设置classpath的方法
SPSS.NET第三方开源代码