乐趣区

Json字段选取器介绍和实现


最近为了工作方便写了一个小工具,这个小工具作用很简单,就是从一个 json 字符串中筛出你想要的部分。

介绍

背景是这样的,我们为了线上调试方便,有个工具可以模拟发起一次数据请求,然后将结果以 json 的形式展示到页面上。但问题是这个数据包含的信息非常多,动不动就上千行(如上图),但每次 debug 的时候,只想看里面特定的几个字段,平常只能依赖于浏览器搜索工具一行一行搜,可能想看的字段会间隔好几屏,一行行看即低效还容易漏。如果要看 JsonArray 的数据,我之前是拷贝出来,然后用 grep 把字段筛出来,但这样又丢失了层级信息。。。。。如果我们想把某些字段列一起用于数据分析的话,就更难了,只能人肉筛选记录。。。

我这个工具采用很简单的语法来标识目标 json 的层级结构,以及每一层中你想要的字段。语法类似 yaml 的层级结果,用相同的缩减标识同一层,每一层的关键词是你想要的字段 key,不区分大小写,为了更方便使用,也支持正则表达式。
当然这里有几个特殊规则:
1. 如果当前层级是个 jsonArray 的话字段后面需要加后缀 :[] 来标识出来(后续我可能会在中括号中支持范围)。

  1. 第一行必须随便写个字段,保留这个字段的目的还是怕一上来就是个 JsonArray。
  2. 目前暂时不能加空行,尤其是多行之间,会导致筛选有问题。

示例如下,也可以试用 demo。

json
  menu
    id
    popup
      menuitem:[]
        value

实现

如果你了解 json 数据格式的话,就知道它是一个层级嵌套的结构,而层级嵌套结构它其实很容易去转换成一种树形的结构。事实上现在市面上所有的 json 解析器,其实都是将这些数据转换成树形结构存储的。知道 json 是一个树形结构之后,我们是不是构造一个同构的子树,同构子树的含义树每一层包含更少的节点,但有的节点和原树的节点同构。

如何构造或者说描述这样一个同构的树形结构?这里我选用了类似 yaml 的描述,它采用了不同缩进来标识层级关系。

1
   2
     3
   4
     5
     6

比如这个,2 4 节点为 1 的子节点,3 是 2 的子节点,5 6 是 4 的子节点。有了描述语言,接下来的一步就是将描述语言转化为抽象语法树。这里我采用编译原理中的递归下降算法,用递归的方式构造每个节点的子节点。

为了方便,我首先将语法描述预处理下,主要是将缩进转化为层级深度,然后递归解析,解析代码如下。

public class Node {
    public int type = 0; //jsonObject or jsonArray 
    Map<String, Node> children = new HashMap<>();

    public Node(String[] keys, int[] deeps, int cur) {  // 解析逻辑直接放在构造函数中
        // 无子节点
        if (cur == keys.length - 1 || deeps[cur] >= deeps[cur+1]) {
            this.type = 0; // 无子节点
            return;
        }
        int childDeep = deeps[cur+1];
        for (int i = cur+1; i < keys.length; i++) {if (deeps[i] < childDeep) {break;} else if (deeps[i] > childDeep) {continue;}
            String key = keys[i];
            Node child = new Node(keys, deeps, i);  // 递归解析子节点 
            if (key.contains(":")) {key = key.split(":")[0];
                child.type = 1;  // ArrayList;
            }
            children.put(key, child);
        }
    }
}

整个解析完之后就是一颗抽象语法树。json 字符串我用 fastjson 解析后也是树形层级结构,因为我们新生成的语法树和 json 语法树是同构的关系,所以我们可以同时递归遍历新语法树和抽象语法树,并同时生成一个筛选后的 json 字符串,这样我们完成了匹配筛选的过程,代码如下。

   public Object getSelected(Object object) {
        // 无子节点
        if (children.size() == 0) {return object;}

        JSONObject res = new JSONObject(true);
        JSONObject json = (JSONObject)object;
        for (Map.Entry<String, Object> entry : json.entrySet()) {Node child = getChild(entry.getKey());
            if (child == null) {continue;}
            // json
            if (child.type == 0) {res.put(entry.getKey(), child.getSelected(json.get(entry.getKey())));
            }
            // jsonArray
            if (child.type == 1) {JSONArray arr = (JSONArray)entry.getValue();
                JSONArray newArr = new JSONArray();
                for (int i = 0; i < arr.size(); i++) {newArr.add(child.getSelected(arr.getJSONObject(i)));
                }
                res.put(entry.getKey(), newArr);
            }
        }
        return res;
    }

    public Node getChild(String content) {for (Map.Entry<String, Node> child : children.entrySet()) {
            // 这里我额外加入了正则表达式匹配,可以让选择器的功能更灵活  
            if (content.equalsIgnoreCase(child.getKey()) || Pattern.matches(child.getKey(), content)) {return child.getValue();
            }
        }
        return null;
    }

最后写个类封装下所有 API 即可。

public class JsonSelector {
    private Node startNode;
    private JsonSelector() {};
    // 编译生成语法树 
    public static JsonSelector compile(String txt) {
        // 预处理  
        txt = txt.replace("\t", " ");
        String[] arr = txt.split("\n");
        int[] deeps = new int[arr.length];
        String[] keys = new String[arr.length];
        for (int i = 0; i < arr.length; i++) {String str = arr[i];
            deeps[i] = getSpaceCnt(str);
            keys[i] = rmSpace(str);
        }
        JsonSelector selector = new JsonSelector();
        selector.startNode = new Node(keys, deeps, 0);
        return selector;
    }

    public String getSelectedString(String jsonStr) {JSONObject json = JSONObject.parseObject(jsonStr, Feature.OrderedField);
        JSONObject res = (JSONObject) startNode.getSelected(json);
        return res.toJSONString();}

    private static int getSpaceCnt(String str) {
        int cnt = 0;
        for (cnt = 0; cnt < str.length(); cnt++) {if (str.charAt(cnt) != ' ') {break;}
        }
        return cnt;
    }

    private static String rmSpace(String str) {String res = str.trim();
        int end = res.length();
        while(end > 0 && res.charAt(end - 1) == ' ') {end--;}
        return res.substring(0, end);
    }
}

本文来自 https://blog.csdn.net/xindoo

退出移动版