Json字段選取器介紹和實現

在這裏插入圖片描述
最近爲了工作方便寫了一個小工具,這個小工具作用很簡單,就是從一個json字符串中篩出你想要的部分。

介紹

背景是這樣的,我們爲了線上調試方便,有個工具可以模擬發起一次數據請求,然後將結果以json的形式展示到頁面上。但問題是這個數據包含的信息非常多,動不動就上千行(如上圖),但每次debug的時候,只想看裏面特定的幾個字段,平常只能依賴於瀏覽器搜索工具一行一行搜,可能想看的字段會間隔好幾屏,一行行看即低效還容易漏。 如果要看JsonArray的數據,我之前是拷貝出來,然後用grep把字段篩出來,但這樣又丟失了層級信息。。。。。如果我們想把某些字段列一起用於數據分析的話,就更難了,只能人肉篩選記錄。。。

我這個工具採用很簡單的語法來標識目標json的層級結構,以及每一層中你想要的字段。語法類似yaml的層級結果,用相同的縮減標識同一層,每一層的關鍵詞是你想要的字段key,不區分大小寫,爲了更方便使用,也支持正則表達式。
當然這裏有幾個特殊規則:
1.如果當前層級是個jsonArray的話字段後面需要加後綴:[]來標識出來(後續我可能會在中括號中支持範圍)。
2. 第一行必須隨便寫個字段,保留這個字段的目的還是怕一上來就是個JsonArray。
3. 目前暫時不能加空行,尤其是多行之間,會導致篩選有問題。

示例如下,也可以試用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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章