公共格式文件上的计算引擎

txt\csv\json\xml\xls 等公共格式的文件在工作中经常会用到,有时候需要对这些文件进行计算处理。能实现这一目标的工具表面看不少,但实际都有各自的缺陷。OpenCSV\JsonPath等类库擅长解析,但计算能力不足;spark 计算能力比较强,但架构过于沉重,学习曲线陡峭;SQLite\HSQLDB等内嵌数据库架构轻便,但入库的过程漫长复杂,延迟很致命;其他工具或不成熟,或代码冗长,或配置复杂,就不一一列举了。

计算公共格式文件,还有一个更好的选择:esProc SPL。

esProc SPL 是基于 JVM 的开源程序语言,提供了多种公共格式文件的解析函数,具有强大的计算、写入、应用集成能力,可以用一致的数据结构和代码计算不同的文件格式。

易用的规则文本读写函数

格式规则的文本类似数据表(二维结构),首行为列名,其他行每行是一条记录,列之间用固定符号分隔,其中,以逗号为分隔符的 csv 和以 tab 为分隔符的 txt 格式(tsv)最为常见。SPL 提供了 T 函数,一行代码就能解析:

A1=T("D:\\data\\Orders.csv")

外存文本读入内存后,SPL 用序表(二维结构化数据对象)存储数据。作为基础数据类型,SPL 为序表提供了丰富的访问语法和处理函数:

解析后的取第 3 条记录:A1(3)

取后 3 条记录:A1.m([-1,-2,-3])

取记录的字段值:A1(3).Amount*0.05

修改记录的字段值:A1(3).Amount = A1(3). Amount*1.05

取一列,返回集合:A1.(Amount)

取几列,返回集合的集合:A1.([CLIENT,AMOUNT])

先按字段取再按记录序号取:A1.(AMOUNT)(2)

先按记录序号取再按字段取:A1(2).AMOUNT

追加记录:A1.insert(200,"APPL",10,2400.4,date("2010-10-10"))

处理或计算后的序表,可用 export 函数方便地存为规则文本。比如:将追加记录后的序表 A1 存为带列名的 csv:

file("D:\\data\\result.csv").export@tc(A1)

写入时可指定分隔符:

file("D:\\data\\result.txt").export@t(A1; ":")

丰富的计算函数

对于解析后的序表,SPL 提供了丰富计算函数,可以轻松完成日常的 SQL 式计算。

过滤:s.select(Amount>1000 && Amount<=3000 && like(Client,"*s*"))

排序:s.sort(Client,-Amount)

去重:s.id(Client)

分组汇总:s.groups(year(OrderDate);sum(Amount))

关联:join(T ("D:/data/Orders.csv"):O,SellerId; T("D:/data/Employees.txt"):E,EId)

TopN:s.top(-3;Amount)

组内 TopN:s.groups(Client;top(3,Amount))

SPL 还提供了符合 SQL92 标准的语法,支持集合计算、case when、with、嵌套子查询等。

强大的不规则文本解析

SPL提供了功能强大的 import 函数,可以解析格式不规则的文本,包括特殊分隔符、特殊日期格式、首行非列名、剥离引号、去除空白、指定数据类型等。比如,分隔符为双横线的文本:

s=file("D:/Orders.txt").import@t(;,"--")

除了格式不规则,还有内容不规则的文本,通常无法直接解析成二维结构数据,SPL 提供了灵活的函数语法,只要简单处理就能够获得理想数据。比如文件每三行对应一条记录,其中第二行含多个字段,将该文件整理成二维结构数据,并按第 3 和第 4 个字段排序:


A

1

=file("D:\\data.txt").import@si()

2

=A1.group((#-1)\3)

3

=A2.new(~(1):OrderID, (line=~(2).array("\t"))(1):Client,line(2):SellerId,line(3):Amount,~(3):OrderDate )

4

=A3.sort(_3,_4)

简化复杂计算

SPL 计算能力强,对于 SQL 和存储过程难以实现的有序运算、集合运算、关联计算、分步计算,SPL 通常可以轻松实现。比如,计算某支股票最长的连续上涨天数:


A

1

// 解析文件

2

=a=0,A1.max(a=if(price>price[-1],a+1,0))

再比如,找出销售额累计占到一半的前 n 个大客户,并按销售额从大到小排序:


A

B

1

//解析文件


2

=A1.sort(amount:-1)

/销售额逆序排序

3

=A2.cumulate(amount)

/计算累计序列

4

=A3.m(-1)/2

/最后的累计即总额

5

=A3.pselect(~>=A4)

/超过一半的位置

6

=A2(to(A5))

/按位置取值

SPL 有丰富的日期和字符串函数,能有效简化相关计算。

季度增减:elapse@q("2020-02-27",-3) // 返回 2019-05-27

N 个工作日之后的日期:workday(date("2022-01-01"),25) // 返回 2022-02-04

字符串类函数,判断是否全为数字:isdigit("12345") // 返回 true

取子串前面的字符串:substr@l("abCDcdef","cd") // 返回 abCD

按竖线拆成字符串数组:"aa|bb|cc".split("|") // 返回 ["aa","bb","cc"]

SPL 还支持年份增减、求季度、按正则表达式拆分字符串、拆出单词、按标记拆 HTML 等大量函数。

值得一提的是,为了进一步提高开发效率,SPL 还创造了独特的函数语法。比如用选项区分类似的函数,只过滤出符合条件的第 1 条记录,可使用选项 @1:

T.select@1(Amount>1000)

从后往前查找第 1 条记录,可以使用 @z:

T.select@z1(Amount>1000)

高效的大文本处理

对于超出内存的大文件,SPL 提供了方便的方法进行解析、计算、写入,相关函数也进行了精心的封装,命名和用法与小文本类似,学习曲线更平滑。比如,读入大文本,根据参数过滤,写入新文件:


A

1

=file("D:\\sales.txt").cursor@t()

2

=A1.select(OrderDate>=P_startDate && OrderDate<=P_endDate)

3

=file("D:\\sales.txt").export@t(A1)

函数 cursor 用来读大文件,用法类似 import,区别在于 cursor 函数生成的是游标类型,封装了内外存交换的细节,即每次从外存读一部分数据到内存,在内存完成计算,累积计算结果,再从外存继续读取。计算函数 select、写入函数 export 的计算对象是游标,与小文件的序表有本质区别,但封装的函数名相同(参数也相同),其目的也是降低学习成本。

与序表一样,游标也支持排序、分组汇总、关联、集合计算等大量计算。

SPL 支持多线程并行计算,可提高大文件处理的速度。比如,将上面例子改为并行计算,只要修改 A1 代码:

=file("D:\\sales.txt").cursor@m()

选项 @m 表示并行,默认使用配置文件中的线程数,也可实时指定。这句代码生成了多路并行的游标同时读取文件,后续的计算也会并行执行。

对于有序的大文件,SPL 还可以进一步提高计算性能。

热部署集成架构

SPL 提供了 JDBC 接口,可以被 Java 代码方便地集成。简单的 SPL 代码可以像 SQL 一样,直接嵌入 JAVA:

Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
String str="=T(\"D:/Orders.xls\").select(Amount>1000 && Amount<=3000 && like(Client,\"*s*\"))";
ResultSet result = statement.executeQuery(str);

复杂的 SPL 代码可以先存为脚本文件,再以存储过程的形式被 JAVA 调用,可有效降低计算代码和前端应用的耦合性。

Class.forName("com.esproc.jdbc.InternalDriver");  
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");  
CallableStatement statement = conn.prepareCall("{call scriptFileName(?, ?)}");  
statement.setObject(1, "2020-01-01");  
statement.setObject(2, "2020-01-31");  
statement.execute();  

SPL 是解释型语言,外置的代码无须编译就能执行,支持不停机热部署,适合变化的业务逻辑,运维复杂度低。

多格式统一计算

SPL 提供了丰富的解析函数,支持多种公共文件,解析结果是统一的数据对象序表,可以用统一的函数和语法进行计算,代码不变。

POI 是稳定成熟的 xls 解析库,但因为功能比较底层,导致代码过于繁琐。.SPL 对 POI 进行了高级封装,可以用大幅简化的函数读写各类 xls。将格式规则的行式 xls 读为序表,仍然用 T 函数读取:

=T("d:\\Orders.xls")

用序表生成格式规则的行式 xls,可用 xlsexport 函数。比如,将 A1 写入新 xls 的第一个 sheet,首行为列名:

=file("e:/result.xlsx").xlsexport@t(A1)

xlsexport 函数的功能丰富多样,可以将序表写入指定 sheet,或只写入序表的部分行,或只写入指定的列。xlsexport 函数还可以方便地追加数据,比如对于已经存在且有数据的 xls,将序表 A1 追加到该文件末尾,外观风格与原文件末行保持一致:

=file("e:/scores.xlsx").xlsexport@a(A1)

对格式不规则的行式 xls,SPL 提供了 xlsimport 函数,内置丰富而简洁的读取功能,

跳过前 2 行的标题区:file("D:/Orders.xlsx").xlsimport@t(;,3)

读取名为 "sales" 的特定 sheet:file("D:/Orders.xlsx").xlsimport@t(;"sales")

函数 xlsimport 还具有读取倒数 N 行、密码打开文件、读大文件等功能,这里不再详述。

对格式很不规则的 xls, SPL 提供了 xlscell 函数,可以读写指定 sheet 里指定片区的数据。

与规则的文本和 xls 等二维数据不同,Json 和 XML 是多层数据,一般的计算引擎无法兼顾,SPL 序表经过精心设计,同时支持二维数据和多层数据(前者是后者的特殊情况)。

例如,解析 Json 文件,进行条件查询:


A

B

1

=file("d:\\xml\\emp_orders.json").read()

读取 Json 字符串

2

=json(A1)

解析为 SPL 序表

3

=A2.conj(Orders)

合并下层记录

4

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

条件查询

点击 A2 格可以看到多层序表的结构,其中,EId、State 等字段存储简单数据类型,Orders 字段存储记录集合(二维表)。点击 Orders 中的某一行,可以展开观察数据:

1png

无论何种文件格式,只要解析为序表,就可以用同样的代码进行计算。例如,从文件读取 XML 字符串(与前面的 Json 同构),同样进行条件查询,只要修改前两行代码:


A

B

1

=file("d:\\xml\\emp_orders.xml").read()

读取 XML 字符串

2

=xml(A1,"xml/row")

解析为 SPL 多层序表