RecordPath指南
概述
Orchsym Studio提供了一套非常强大的组件,能够提取,处理,路由,转换和提供任何格式的数据。这是因为Orchsym Studio框架本身与数据无关。它不关心您的数据是100字节JSON消息还是100千兆字节视频。Orchsym Studio有许多模式以处理不同类型的数据。
通常由Orchsym Studio处理的一类数据是面向记录(record-oriented)的数据。当我们说面向记录的数据时,我们经常(但并不总是)谈论结构化数据,如JSON,CSV和Avro。还有许多其他类型的数据也可以表示为“记录”或“消息”。为此,我们开发了一组Controller Services来解析这些不同的数据格式,并通过使用RecordReader API以一致的方式表示数据。这使得以任何数据格式编写的数据都能被同样处理,只需要借助RecordReader生成一个表示数据的Record对象。
这里谈论的一条“记录”是一个抽象概念,它允许我们以相同的方式处理数据,无论哪种格式。记录由一个或多个字段组成。每个字段都有一个名称和与之关联的类型。使用记录Schema描述记录的字段。Schema指示哪些字段构成特定类型的记录。字段的类型将是以下之一:
String
Boolean
Byte
Character
Short
Integer
Long
BigInt
Float
Double
Date - 表示没有Time组件的Date
Time - 表示没有Date组件的时间
Timestamp - 表示日期和时间
Embedded Record - 可以通过允许字段为类型记录本身来表示分层数据,例如JSON。
Choice - 字段可以是几种类型中的任何一种。
Array - 数组的所有元素都具有相同的类型。
Map - 所有Map键都是String类型。值的类型相同。
一旦数据流转换为记录,RecordWriter API就允许我们将这些记录序列化为字节流,以便它们可以传递到其他系统。
当然,如果我们不打算对中间的数据做任何处理,那么读取和写入这些数据就没什么意义了。Orchsym Studio提供了数个组件,它们为路由,查询和转换面向记录的数据提供了一些非常强大的功能。通常,为了执行期望的功能,组件将需要来自用户的输入以便确定记录中的哪些字段或者记录中的哪些值应该被操作。
现在来讲Orchsym Studio的RecordPath语言。RecordPath旨在成为一种简单易用的特定于域的语言(DSL),用于指定在配置组件时我们关心或想要访问的记录中的哪些字段。
RecordPath结构
Orchsym Studio中的记录由(可能)许多字段组成,并且这些字段中的每一个实际上可能本身就是记录。这意味着Record可以被认为具有分层或嵌套的结构。比如我们谈到“内部记录inner Record”是“外部记录outer Record” 的孩子。那么,内部记录的孩子肯定是最外层记录的后代。类似地,我们可以将外部记录称为内部记录的一个祖先。
子操作符
RecordPath语言的结构使我们能够轻松地访问最外层记录的字段,或子记录或后代记录的字段。为了实现这一点,我们用斜线字符分隔孩子的名字(/
),我们称之为子(child
)操作符。例如,假设我们有一个由两个字段组成的记录: name
和
details
。另外,假设 details
是一个字段,它本身就是一个记录,有两个字段: identifier
和 address
。
此外,让我们考虑一下 address
本身就是一个包含5个字段的记录 :
number
, street
, city
, state
,和
zip
。出于说明目的,在JSON中编写的示例可能如下所示:
{
"name": "John Doe",
"details": {
"identifier": 100,
"address": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
}
}
}
我们可以参考一下 zip
使用RecordPath的字段:
/details/address/zip
。这表示,我们想要使用 details
“根”记录的字段。然后我们想访问到 address
子域记录和 zip
记录。
后裔操作符
除了提供到达的明确路径之外 zip
字段,有时可能有用的参考 zip
字段不知道完整路径。在这种情况下,我们可以使用后裔(descendant
)
操作符(//
) 而不是子(child
) 操作符 (/
)达到同样的目标 zip
,如上所述,我们可以通过简单地使用路径来实现这一点 //zip
。
但是, child
操作员和 descendant
操作符有很大的区别: descendant
运算符可以匹配多个字段,而 child
运算符最多匹配一个字段。为了帮助理解这一点,请看以下记录:
{
"name": "John Doe",
"workAddress": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
},
"homeAddress": {
"number": "456",
"street": "116th Avenue",
"city": "New York",
"state": "NY",
"zip": "11697"
}
}
现在,如果我们使用RecordPath /workAddress/zip
,我们将访问 zip
值为“10020” 的字段。RecordPath /homeAddress/zip
将访问 zip
值为“11697” 的字段。但是,RecordPath //zip
将访问这两个字段。
过滤器
通过上面的示例和解释,我们可以轻松地访问记录中的特定字段。但是,在实际场景中,数据很少像上面的示例那样简单。通常,我们需要过滤掉或改进我们访问的字段。我们可能想要这样做的例子是当我们访问一个Array字段并且只想访问数组中的一些元素时;
当我们访问Map字段并想要访问Map中的一个或几个特定条目时;
或者当我们想要仅在符合某些标准的情况下访问记录时。我们可以通过在方括号内为RecordPath提供我们的标准(使用
[
and ]
字符)来实现这一点。我们将在下面讨论这些情形。
函数用法
除了从“记录”中检索字段之外,如上面过滤器部分所述,我们有时需要优化我们要选择的字段。或者我们可能想要返回字段的修改版本。为此,我们需要使用函数功能。函数的语法是
<function name> <左括号> <args>
<右括号>,其中<args>表示用逗号分隔的一个或多个参数。参数可以是字符串文字(例如
'hello'
)或数字文字(例如 48
),或者可以是相对或绝对的RecordPath(例如 ./name
or
/id
)。此外,我们可以在过滤器中使用函数。例如,我们可以使用RecordPath
/person[ isEmpty('name') ]/id
来检索id名字为空的任何人的领域。可在以下函数
部分中找到可用功能及其相应文档的列表。
Arrays
当我们访问一个Array字段时,该字段的值可能是一个包含多个元素的数组,但我们可能只需要其中的一些元素。例如,我们可能只想访问第一个元素; 只有最后一个元素; 或者也许是第一,第二,第三和最后一个元素。我们可以简单地通过使用方括号内的元素的索引来访问特定元素(索引是从0开始的)。那么让我们考虑上面记录的另一种取法:
{
"name": "John Doe",
"addresses": [
"work": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
},
"home": {
"number": "456",
"street": "116th Avenue",
"city": "New York",
"state": "NY",
"zip": "11697"
}
]
}
我们现在可以使用RecordPath /addresses[0]
访问 addresses
数组中的第一个元素。我们可以使用RecordPath /addresses[1]
访问第二个元素。但有时候,我们可能不知道数组中将存在多少个元素。因此我们可以使用负数索引从数组末尾开始向后计数。例如,我们可以将最后一个元素作为
/addresses[-1]
或者倒数第二个元素作为 /addresses[-2]
。如果我们想访问几个元素,我们可以使用以逗号分隔的元素列表,例如
/addresses[0, 1, 2, 3]
。或者,要访问元素0到8,我们可以使用 范围
操作符 (..
) ,如 /addresses[0..8]
。我们也可以混合这些,使用语法
/addresses[0..-1]
甚至 /addresses[0, 1, 4, 6..-1]
访问所有元素。当然,并非此处访问的所有索引都匹配上面的记录,因为该
addresses
数组只有2个元素。这种情况下,不匹配的索引将会被跳过。
Maps
与Array字段类似,Map字段实际上可能包含几个不同的值。RecordPath使我们能够根据键选择一组值。我们通过在方括号内使用带引号的字符串来完成此操作。举个例子,让我们重新访问上面的原始记录:
{
"name": "John Doe",
"details": {
"identifier": 100,
"address": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
}
}
}
但是,现在让我们考虑与Record关联的Schema表明该 address
字段不是Record而是 Map
字段。在这种情况下,如果我们尝试使用RecordPath
/details/address/zip
访问`zip`, 则RecordPath将不匹配,因为该
address
字段不是一个Record,因此没有任何名为 zip
的子
Record。相反,它是一个Map字段,其键和值都是String类型。不幸的是,在查看JSON时,这可能看起来有点令人困惑,因为JSON并不真正拥有类型定义。但是,当我们将JSON转换为Record对象以便对数据进行操作时,这种区别就很重要。
在上面列出的情况下,我们仍然可以使用RecordPath 访问 zip
字段。但我们现在必须使用稍微不同的语法:
/details/address['zip']
。这告诉RecordPath我们想要访问 details
根字段。然后我们想访问它的 address
字段。由于 address
字段是Map字段,我们可以使用方括号来指示我们要指定Map的键,然后我们可以在引号中指定键名。
此外,我们可以使用以逗号分隔的列表来选择多个Map的键:
/details/address['city', 'state', 'zip']
。如果需要,我们还可以使用Wildcard
运算符 ( \*
): /details/address[*]
来选择所有字段
。Map字段不包含任何排序,因此无法通过数字索引来访问键值。
谓词
到目前为止,我们已经讨论了两种不同类型的过滤器。它们中的每一个都允许我们从允许多个值的字段中选择一个或多个元素。但是,通常情况下,我们需要限制选择哪些记录字段。例如,如果我们想要选择
zip
字段,但仅限于 address
不是纽约州的字段,该怎么办?上面的例子没有给我们任何方法来做到这一点。
RecordPath provides the user the ability to specify a Predicate. A
Predicate is simply a filter that can be applied to a field in order to
determine whether or not the field should be included in the results.
Like other filters, a Predicate is specified within square brackets. The
syntax of the Predicate is
<Relative RecordPath> <Operator> <Expression>
. The
Relative RecordPath
works just like any other RecordPath but must
start with a .
(to reference the current field) or a ..
(to
reference the current field’s parent) instead of a slash and references
fields relative to the field that the Predicate applies to. The
Operator
must be one of:
RecordPath为用户提供了指定谓词的功能。谓词本质上是一个过滤器,用于确定该字段是否应该包含在查询结果中。与其他过滤器一样,谓词在方括号内指定。谓词的语法是
<Relative RecordPath> <Operator> <Expression>
。 Relative RecordPath
工作方式与任何其他RecordPath一样,但必须以 .
(访问当前字段)或 ..
(访问当前字段的父节点)开头,不能以斜杠和引用字段开头。Operator
必须是下面其中一种:
Equals (
=
)Not Equal (
!=
)Greater Than (
>
)Greater Than or Equal To (
>=
)Less Than (
<
)Less Than or Equal To (
<=
)`Expression` 可以是一个文字值,例如50或Hello或可以是另一种RecordPath。
为了说明这一点,我们以下面的记录为例:
{
"name": "John Doe",
"workAddress": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
},
"homeAddress": {
"number": "456",
"street": "Grand St",
"city": "Jersey City",
"state": "NJ",
"zip": "07304"
},
"details": {
"position": "Dataflow Engineer",
"preferredState": "NY"
}
}
现在我们可以使用谓词来仅选择州不是纽约的字段。例如,我们可以使用
/*[./state != 'NY']
选择所有 state
州不是“纽约”的记录。请注意
details
记录将不会返回,因为它没有名为 state
的字段。所以在这个例子中,RecordPath将只选择 homeAddress
域。一旦我们选择了该域,我们就可以继续使用我们的RecordPath。正如我们上面指出的,我们可以选择
zip
字段: /*[./state != 'NY']/zip
。 这个RecordPath将会只选择 zip
来自 homeAddress
的记录。
我们还可以将一个字段中的值与另一个字段中的值进行比较。例如,我们可以使用RecordPath
选择人员首选州的地址
/*[./state = /details/preferredState]
。在此示例中,此RecordPath将检索
workAddress
中 state
字段匹配 preferredState
值的记录。
另外,我们可以使用父运算符编写一个RecordPath,来访问州为“NJ”的任何记录的“city”字段(..
):
/*/city[../state = 'NJ']
。
函数
在上面的函数用法部分中,我们描述了如何以及为什么在RecordPath中使用函数。在这里,我们将描述不同功能,它们的功能以及它们的工作方式。函数可以分为两组:独立函数,可以是一个RecordPath的‘根’,例如
substringAfter( /name, ' ' )
和过滤函数,它们将被用作过滤器,例如
/name[ contains('John') ]
。 独立函数也可以在过滤器中使用,但因为不返回
boolean
类型(true
或 false
)因此他本身不是一个完整的过滤器。例如,我们可以使用诸如
/name[ substringAfter(., ' ') = 'Doe']
,但我们不能简单地使用
/name[ substringAfter(., ' ') ]
,因为过滤器必须是布尔值。
除非另有说明,以下所有示例均按是操作以下记录:
{
"name": "John Doe",
"workAddress": {
"number": "123",
"street": "5th Avenue",
"city": "New York",
"state": "NY",
"zip": "10020"
},
"homeAddress": {
"number": "456",
"street": "Grand St",
"city": "Jersey City",
"state": "NJ",
"zip": "07304"
},
"details": {
"position": "Dataflow Engineer",
"preferredState": "NY",
"employer": "",
"vehicle": null,
"phrase": " "
}
}
独立函数
substring
substring函数返回String值的一部分内容。该函数需要3个参数:
需要被截取的值,基于0的起始索引(包括)和基于0的结束索引(不包括)。
起始索引和结束索引可以是: 0
来指示字符串的第一个字符,正整数或负整数来指示字符串中第n个索引。如果值是负整数,比如说
-n
, 那么这代表了最后第 n
个字附。 价值 -1
表示字符串中的最后一个字符。 所以,例如,
substring( 'hello world', 0, -1 )
意思是取字符串
hello
,并返回字符0到最后一个字符,因此返回值将是 hello world
。
RecordPath | 返回值 |
---|---|
substring( /name, 0, -1 ) | John Doe |
substring( /name, 0, -5 ) | John |
substring( /name, 1000, 1005 ) | <empty string> |
substring( /name, 0, 1005) | John Doe |
substring( /name, -50, -1) | <empty string> |
substringAfter
返回在第一次匹配之后的所有剩余内容。
RecordPath | 返回值 |
---|---|
substringAfter( /name, ' ' ) | Doe |
substringAfter( /name, 'o' ) | hn Doe |
substringAfter( /name, '' ) | John Doe |
substringAfter( /name, 'xyz' ) | John Doe |
substringAfterLast
返回在最后一次匹配之后的所有剩余内容。
RecordPath | 返回值 |
---|---|
substringAfterLast( /name, ' ' ) | Doe |
substringAfterLast( /name, 'o' ) | e |
substringAfterLast( /name, '' ) | John Doe |
substringAfterLast( /name, 'xyz' ) | John Doe |
substringBefore
返回在第一次匹配的前面的所有内容。
RecordPath | 返回值 |
---|---|
substringBefore( /name, ' ' ) | John |
substringBefore( /name, 'o' ) | J |
substringBefore( /name, '' ) | John Doe |
substringBefore( /name, 'xyz' ) | John Doe |
substringBeforeLast
返回在最后一次匹配的前面的所有内容。
RecordPath | 返回值 |
---|---|
substringBeforeLast( /name, ' ' ) | John |
substringBeforeLast( /name, 'o' ) | John D |
substringBeforeLast( /name, '' ) | John Doe |
substringBeforeLast( /name, 'xyz' ) | John Doe |
replace
用另一个String替换所有出现的String。
RecordPath | 返回值 |
---|---|
replace( /name, 'o', 'x' ) | Jxhn Dxe |
replace( /name, 'o', 'xyz' ) | Jxyzhn Dxyze |
replace( /name, 'xyz', 'zyx' ) | John Doe |
replace( /name, 'Doe', /workAddress/city ) | John New York |
replaceRegex
根据String值的内容计算正则表达式,并将任何匹配替换为另一个值。此函数需要3个参数:运行正则表达式的String,要运行的正则表达式
和替换值。替换值可以可选地使用反向访问,例如 $1
和 ${named_group}
RecordPath | 返回值 |
---|---|
replaceRegex( /name, 'o', 'x' ) | Jxhn Dxe |
replaceRegex( /name, 'o', 'xyz' ) | Jxyzhn Dxyze |
replaceRegex( /name, 'xyz', 'zyx' ) | John Doe |
replaceRegex( /name, '\s+.*', /workAddress/city ) | John New York |
replaceRegex(/name, '([JD])', '$1x') | Jxohn Dxoe |
replaceRegex(/name, '(?<hello>[JD])', '${hello}x') | Jxohn Dxoe |
concat
将所有参数连接在一起。
RecordPath | 返回值 |
---|---|
concat( /name, ' lives in ', /homeAddress/city ) | John Doe lives in Jersey City |
fieldName
有时候我们需要获得字段的名称,而不是值。要做到这一点,我们可以使用
fieldName
函数。
RecordPath | 返回值 |
---|---|
fieldName(//city/..) | workAddress and homeAddress |
//city[not(startsWith(fieldName(..), 'work'))] | Jersey City |
在上面的示例中,第一个RecordPath返回两个单独的字段名称:“workAddress”和“homeAddress”。相反,第二个
RecordPath返回"city”字段的值并使用 fieldName
函数作为谓词。第二个RecordPath找到一个“city”字段,其父节点的名称不以“work”开头。这意味着它将返回其父级为“homeAddress”的“city”字段的值,但不返回其父级为“workAddress”的“city”字段的值。
toDate
将字符串转换为日期. 例如,给定一个schema:
{
"type": "record",
"name": "events",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "eventDate", "type" : "string"}
]
}
以及如下记录:
{
"name" : "My Event",
"eventDate" : "2017-10-20'T'11:00:00'Z'"
}
以下记录路径将eventDate字段解析为Date:
toDate( /eventDate, "yyyy-MM-dd'T'HH:mm:ss'Z'")
toString
如果输入类型为“bytes”,则使用给定的字符集将值转换为String。例如,给定一个schema:
{
"type": "record",
"name": "events",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "bytes", "type" : "bytes"}
]
}
以及如下记录:
{
"name" : "My Event",
"bytes" : "Hello World!"
}
以下记录路径将字节字段解析为字符串:
toString( /bytes, "UTF-8")
toBytes
将String转换为byte[],使用给定的字符集。例如,给定一个schema:
{
"type": "record",
"name": "events",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "s", "type" : "string"}
]
}
以及如下记录:
{
"name" : "My Event",
"s" : "Hello World!"
}
以下记录路径将使用UTF-16编码将String字段转换为字节数组:
toBytes( /s, "UTF-16")
format
以给定格式将Date转换为String.
此函数的第一个参数必须是Date或Number,第二个参数必须是遵循Java SimpleDateFormat 的格式String。
例如,给定一个schema:
{
"type": "record",
"name": "events",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "eventDate", "type" : { "type" : "long", "logicalType" : "timestamp-millis" } }
]
}
以及如下记录:
{
"name" : "My Event",
"eventDate" : 1508457600000
}
以下记录路径表达式将日期格式化为String:
RecordPath | 返回值 |
---|---|
format( /eventDate, "yyyy-MM-dd'T'HH:mm:ss'Z'") | 2017-10-20’T'11:00:00’Z' |
format( /eventDate, "yyyy-MM-dd") | 2017-10-20 |
在将字段声明为String的情况下,必须在格式化之前调用toDate函数。
例如,给定一个schema:
{
"type": "record",
"name": "events",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "eventDate", "type" : "string"}
]
}
以及如下记录:
{
"name" : "My Event",
"eventDate" : "2017-10-20'T'11:00:00'Z'"
}
以下记录路径表达式将重新格式化日期字符串:
RecordPath | 返回值 |
---|---|
format( toDate(/eventDate, "yyyy-MM-dd'T'HH:mm:ss'Z'"), 'yyyy-MM-dd') | 2017-10-20 |
过滤函数
contains
如果String中包含指定的内容,返回 true
,否则返回 false
。
RecordPath | 返回值 |
---|---|
/name[contains(., 'o')] | John Doe |
/name[contains(., 'x')] | <returns no results> |
/name[contains( ../workAddress/state, /details/preferredState )] | John Doe |
matchesRegex
使用正则表达式验证String值的内容,匹配则返回 true
,否则返回 false
。
此函数需要2个参数:运行正则表达式的String和要运行的正则表达式。
RecordPath | 返回值 |
---|---|
/name[matchesRegex(., 'John Doe')] | John Doe |
/name[matchesRegex(., 'John')] | <returns no results> |
/name[matchesRegex(., '.* Doe' )] | John Doe |
startsWith
如果String值以给定的子字符串开头则返回 true
,否则返回 false
。
RecordPath | 返回值 |
---|---|
/name[startsWith(., 'J')] | John Doe |
/name[startsWith(., 'x')] | <returns no results> |
/name[startsWith(., 'xyz')] | <returns no results> |
/name[startsWith(., '')] | John Doe |
endsWith
如果String值以给定的子字符串结尾则返回 true
,否则返回 false
。
RecordPath | 返回值 |
---|---|
/name[endsWith(., 'e')] | John Doe |
/name[endsWith(., 'x')] | <returns no results> |
/name[endsWith(., 'xyz')] | <returns no results> |
/name[endsWith(., '')] | John Doe |
not
反转传递给 not
函数的表达式或值。
RecordPath | 返回值 |
---|---|
/name[not(endsWith(., 'x'))] | John Doe |
/name[not(contains(., 'x'))] | John Doe |
/name[not(endsWith(., 'e'))] | <returns no results> |
isEmpty
如果给定的值为null或为空字符串,返回 true
RecordPath | 返回值 |
---|---|
/name[isEmpty(/details/employer)] | John Doe |
/name[isEmpty(/details/vehicle)] | John Doe |
/name[isEmpty(/details/phase)] | <returns no results> |
/name[isEmpty(.)] | <returns no results> |
isBlank
如果给定的值为null或者是空字符串或仅包含空格的字符串
(空格,制表符,回车符和换行符)则返回 true
。
RecordPath | 返回值 |
---|---|
/name[isBlank(/details/employer)] | John Doe |
/name[isBlank(/details/vehicle)] | John Doe |
/name[isBlank(/details/phase)] | John Doe |
/name[isBlank(.)] | <returns no results> |