最近爲了工作方便寫了一個小工具,這個小工具作用很簡單,就是從一個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);
}
}