对象标准化算法实现

104 阅读4分钟

嵌套对象扁平化, json 对象扁平化

背景

在将半结构化的数据存储到结构化的存储引擎中, 我们需要先将半结构化的数据转为结构化的数据再保存到数据库,这个过程中, 我们并不关心数据内容本身,相反, 我们更加侧重于数据的格式,比如说, 哪个字段是 str, 某某字段类型是 int xx 字段类型为 bool 等。这样的场景还是比较多的, 比如, 解析爬虫爬取回来的 JSON 格式的数据, 并且保存到 clickhouse 这类分析引擎中。比如数仓的 ETL , 将保存在 mongodb 的数据存储到 clickhouse, mysql 这类结构化存储引擎中等等。 这其中, 我们需要将 JSON 这种半结构化的数据转为结构化之后保存到结构化存储引擎才方便后续的业务使用。

怎么将半结构化的数据转为结构化呢?

我们可以为每一个业务都写一个对应的结构体去解析对应的数据。就满足业务来说, 这已经足够了。

这种场景下, 如果数据嵌套的层数越多,那么这个结构体就会非常地复杂, 比如, 我们解析 外卖平台的商品数据时, 定义一个结构体没个几百行下不来。

定义这么复杂的结构体其实对于我们来说意义不大, 因为我们并不需要使用这些结构体来做和业务有关的数据运算, 比如扣库存等等操作。

面对这种类型的嵌套类型的数据, 我们只需要展开就方便我们后续的业务操作了。

我们将这种展开叫做对象标准化。

实现思路

在讲具体的实现之前,我们需要先知道, 我们展开的一个对象时如何的。

{
  "foo": "baz",
  "key": "value"
}

可以看到, 展开的对象有两个字段,fookey, 字段值的类型都是 str。 这种情况下最简单, 直接赋值就好。

展开的结果列表为:

[
  {
    "foo": "baz",
    "key": "value"
  }
]

为什么展开的结果是一个列表呢?


展开过程_

  1. 解析一开始的结果是一个空对象的列表, [{}]
  2. 对于基础类型, 值已经不能再展开了,直接赋值即可
  3. 对于 map 类型,需要将里面的每个属性赋值到解析出的临时对象列表中的每个对象
  4. 对于 list 类型, 处理 list 中的每个元素时,都需要完整地拷贝整个临时的对象列表,并且为拷贝的每个对象都赋值当前的元素。
  5. 整个解析过程是递归的。
{
  "user": {
    "name": "xx",
    "age": 18
  },
  "list": [1, 2, 3]
}

解析到 user 时, 一看, 值类型为 map, 需要深入解析 user 的值,然后解析到 name , 其值类型为 str, 可以直接赋值了, age 属性同理。 解析到这里, user 属性已经完成了解析。

到这里, user 返回的结果列表为:

[{ "user.name": "xx", "user.age": 18 }]

然后解析到了 list 属性, 值类型为列表, 对于列表类型的解析参考第四点。

在处理 list 中的每个元素时, 我们都需要将上层已经解析好的结果都拷贝一份, 这里我们拷贝的是 [{"user.name":"xx","user.age":18}],

处理列表的第一个元素时 1, 拷贝整个列表, 然后, 列表中每个元素都赋值 1, 这里得到的结果为 [{"user.name":"xx","user.age":18, "list":1}]

同理, 元素 23 做法一样。处理结果分别为:

[{"user.name":"xx","user.age":18, "list":2}]

[{"user.name":"xx","user.age":18, "list":3}]

整个 list 属性遍历完成之后, 得到的结果列表如下:

[
  {
    "user.name": "xx",
    "user.age": 18,
    "list": 1
  },
  {
    "user.name": "xx",
    "user.age": 18,
    "list": 2
  },
  {
    "user.name": "xx",
    "user.age": 18,
    "list": 3
  }
]

至此, 一个相对复杂的对象解析完成。

在实现中, 对于 基础类型:

func (c *dataEtl) parsePrimitive(data interface{}, prefix string, currentMap map[string]interface{},depth int,) (result []map[string]interface{}) {
    // 拷贝已经遍历完成的临时结果
 tmp := cpm(currentMap)
    tmp[prefix] = data
 return append(result, tmp)
}

对于 map 类型

// 解析 map 类型
func (c *dataEtl) parseMap(data interface{}, prefix string, currentMap map[string]interface{}, depth int) (result []map[string]interface{}) {
 var newList = []map[string]interface{}{cpm(currentMap)}
 for mr := reflect.ValueOf(data).MapRange(); mr.Next(); {
  _key := mr.Key().Interface()
  _v := mr.Value().Interface()

  switch reflect.TypeOf(_v).Kind() {
  case
   reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   reflect.Bool, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.String:
   // 如果当前的值 数值,整型,布尔时,填充到所有已经遍历的对象
   for i := range newList {
    newList[i][c.separator.AppendToPrefix(prefix, _key)] = _v
   }
            // 如果值还是对象类型, 继续处理
  case reflect.Map, reflect.Slice, reflect.Struct, reflect.Array:
   var copyList = make([]map[string]interface{}, 0)
   for _, _v2 := range newList {
    var _res = c.normalize(_v, c.separator.AppendToPrefix(prefix, _key), _v2, depth+1)
    copyList = append(copyList, _res...)
   }
   newList = copyList
  default:

  }
 }
 return newList
}

对于 list 类型

func (c *dataEtl) parseSlice(data interface{},prefix string,currentMap map[string]interface{}, depth int,
) (result []map[string]interface{}) {
 var newList []map[string]interface{}
 var s = reflect.ValueOf(data)
 for i := 0; i < s.Len(); i++ {
  var _res = c.normalize(s.Index(i).Interface(), prefix, cpm(currentMap), depth+1)
  newList = append(newList, _res...)
 }
 return newList
}

总结

以上主要是简要说明了对象展开的过程, 完整的实现请参考