JsonPath - 根據表達式路徑隨意解析

JsonPath

在xml的使用過程中,對於xml的解析我們知道可以使用xpath的方式,隨意的獲取到我們想要的屬性值。那麼在使用json時,我們能不能實現同樣的操作呢?

答案就是 json-path

基礎介紹

跟 XPath 類似,JsonPath 通過路徑來檢索JSON,對語法格式如下

語法

符號 描述
$ 表示json的根節點,表示根節點下的所有數據
. 表示子節點,如 $.store 表示根節點下的store節點下的所有數據
.. 可實現遞歸搜索,如 $..title 表示搜索json中所有key爲title屬性的值
* 可表示某一層節點,如 $.*.book 表示根節點下所有節點的book節點數據
@ 在表達式中使用,表示當前節點對象
['',''] 如 $..['author'] 表示所有節點中author節點的值
[,] 如 $..['0'] 表示所有節點中下標爲0的節點的值
[start:end] 如 $..book[2] 取json中book數組的第3個值
[?()] 過濾器表達式,表達式結果必須是boolean

過濾器表達式

通常的表達式格式爲:[?(@.age > 18)]

操作符 描述
== 等於符號,但數字1不等於字符1(note that 1 is not equal to ‘1’)
!= 不等於符號
< 小於符號
<= 小於等於符號
> 大於符號
>= 大於等於符號
=~ 判斷是否符合正則表達式,例如[?(@.name =~ /foo.*?/i)]
in 所屬符號,例如[?(@.size in [‘S’, ‘M’])]
nin 排除符號
size size of left (array or string) should match right
empty 判空符號

示例

{
    "store": {
        "book": [
            {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95
            },
            {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99
            },
            {
                "category": "fiction",
                "author": "Herman Melville",
                "title": "Moby Dick",
                "isbn": "0-553-21311-3",
                "price": 8.99
            },
            {
                "category": "fiction",
                "author": "J. R. R. Tolkien",
                "title": "The Lord of the Rings",
                "isbn": "0-395-19395-8",
                "price": 22.99
            }
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    },
    "expensive": 10
}

在線測試:http://jsonpath.herokuapp.com/?path=$.store.book%5B*%5D.author

JsonPath表達式 結果
$.store.book[*].author 獲取json中store下book下的所有author值
$..author 獲取所有的 author 的值
$.store.book.* 獲取json中store下book下的所有值
$.store..price 獲取json中store下所有price的值
$..book[2] 獲取json中book數組的第3個值
$..book[-2] 倒數的第二本書
$..book[0,1] 前兩本書
$..book[:2] 從索引0(包括)到索引2(排除)的所有圖書
$..book[1:2] 從索引1(包括)到索引2(排除)的所有圖書
$..book[-2:] 獲取json中book數組的最後兩個值
$..book[2:] 獲取json中book數組的第3個到最後一個的區間值
$..book[?(@.title)] 獲取json中book數組中包含title的所有節點
$.store.book[?(@.price < 10)] 獲取json中book數組中price<10的所有值
$..book[?(@.price <= $['expensive'])] 獲取json中book數組中price<=$['expensive']結果的所有值
$..book[?(@.author =~ /.*REES/i)] 獲取json中book數組中的作者以REES結尾的所有值(REES不區分大小寫)
$..* 逐層列出json中的所有值,層級由外到內
$..book.length() 獲取json中book數組的長度

使用

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
</dependency>

特別說明:下文中使用的 JSON_DATA 變量的值都爲上面所示的json範例

使用靜態方法直接讀

List<String> authors = JsonPath.read(JSON_DATA, "$.store.book[*].author");

如果需要多次讀,那麼這種方法不夠理想,因爲每次都會重新解析一次json數據

一次解析,多次使用

我們可以先將json數據一次解析,然後多次使用,提升性能。json-path提供瞭如ReadContext ,DocumentContext等類,我們可以隨意使用,其關係如下:

image

DocumentContext documentContext = JsonPath.parse(JSON_DATA);
// 或者
ReadContext ctx = JsonPath.parse(JSON_DATA);


List<String> author = ctx.read("$.store.book[?(@.isbn)].author");

類型轉換

在java中使用JsonPath時,當我們知道我們讀取過後的返回值是什麼類型時,JsonPath會嘗試將其轉換爲我們想要的類型

// 結果爲 "Nigel Rees" ,如果我們強制轉換爲List那麼會拋出 java.lang.ClassCastException 異常
List<String> list = JsonPath.parse(JSON_DATA).read("$.store.book[0].author")
 
// 正常
String author = JsonPath.parse(JSON_DATA).read("$.store.book[0].author")

我們在解析相應的json是可以設置解析過後的值自動轉換爲對應的類型的。默認情況下,MappingProvider SPI提供了一個簡單的對象映射器。

String JSON_DATA = "{\"date_as_long\" : 1411455611975}";
Date date = JsonPath.parse(JSON_DATA).read("$['date_as_long']", Date.class);

// 2014-09-23 15:00:11

如果我們需要轉換爲更加具體的對象,如一個POJO等,就需要我們配置更加詳細的json解析器JacksonMappingProviderGsonMappingProvider

Book book = JsonPath.parse(JSON_DATA).read("$.store.book[0]", Book.class);

Configuration conf = Configuration.builder().mappingProvider(new JacksonMappingProvider()).build();
TypeRef<List<String>> typeRef = new TypeRef<List<String>>(){};
List<String> titles = JsonPath.using(conf).parse(JSON_DATA).read("$.store.book[*].title", typeRef);

過濾

根據路徑過濾

List<Map<String, Object>> books =  JsonPath.parse(JSON_DATA).read("$.store.book[?(@.price < 10)]");

// [{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99}]

根據過濾器過濾

Filter cheapFictionFilter = Filter.filter(Criteria.where("category").is("fiction").and("price").lte(10D));
List<Map<String, Object>> books = JsonPath.parse(JSON_DATA).read("$.store.book[?]", cheapFictionFilter);

// [{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99}]

// 使用and或者or連接多個條件
Filter fooOrBar = filter(
   where("foo").exists(true)).or(where("bar").exists(true)
);
   
Filter fooAndBar = filter(
   where("foo").exists(true)).and(where("bar").exists(true)
);

自定義過濾器

ReadContext reader = JsonPath.parse(JSON_DATA);
Predicate booksWithIsbn = new Predicate() {
    @Override
    public boolean apply(PredicateContext context) {
        return context.item(Map.class).containsKey("isbn");
    }
};
reader.read("$.store.book[?].isbn", List.class, booksWithIsbn);

// ["0-553-21311-3","0-395-19395-8"]

注意:在自定義過濾器中,context.item(Map.class) 這句話。這句中的Map.class是根據預定的結果類型定義的,如果返回的是String類型值,那就改爲String.class

返回值

在JsonPath中,我們可以通過配置來指定本次讀取時是返回相應的值,還是返回符合結果路徑

Configuration configuration = Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(configuration).parse(JSON_DATA).read("$..author");

// ["$['store']['book'][0]['author']","$['store']['book'][1]['author']","$['store']['book'][2]['author']","$['store']['book'][3]['author']"]

Option.AS_PATH_LIST 表示返回路徑,同時,該類還有其他幾個參數:

  • DEFAULT_PATH_LEAF_TO_NULL:對應路徑的節點不存在時,返回null
[
   {
      "name" : "john",
      "gender" : "male"
   },
   {
      "name" : "ben"
   }
]

Configuration conf = Configuration.defaultConfiguration();
// 正常
String gender0 = JsonPath.using(conf).parse(json).read("$[0]['gender']");
// 異常 PathNotFoundException thrown
String gender1 = JsonPath.using(conf).parse(json).read("$[1]['gender']");
 
 
Configuration conf2 = conf.addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL);
// 正常
String gender0 = JsonPath.using(conf2).parse(json).read("$[0]['gender']");
// 正常 (返回 null)
String gender1 = JsonPath.using(conf2).parse(json).read("$[1]['gender']");
  • ALWAYS_RETURN_LIST:始終將結果包裝在List中
Configuration conf = Configuration.defaultConfiguration();
// 正常
List<String> genders0 = JsonPath.using(conf).parse(json).read("$[0]['gender']");
// 異常 PathNotFoundException thrown
List<String> genders1 = JsonPath.using(conf).parse(json).read("$[1]['gender']");
  • SUPPRESS_EXCEPTIONS :確保不會從路徑評估傳播異常

如果選項ALWAYS_RETURN_LIST存在,將返回一個空列表

如果選項ALWAYS_RETURN_LIST不存在返回null

操作key

有時,我們解析一個json並不是爲了將其解析出來,用於其他。而是我們需要將json解析出來,然後去修改或者刪除其中的key。比如對一個json格式數據進行某些字段的脫敏處理。這是我們就需要用到其提供的 .set() 和 .put() 方法,同時還有 .delete()、.add() ...

這些方法的實現都在 JsonContext 中,其繼承關係如圖:

image

image

使用

要想對json進行增刪改,我們首先要提高一個路徑,用來讓JsonPath可以找到對應的key或者是節點。

先解析
// 首先解析json爲文檔
DocumentContext documentContext = JsonPath.parse(JSON_DATA);
根據提供的路徑直接修改
  • 路徑直接定義到具體的值
// 將所有書籍的作者修改爲dimples 
JsonPath p = JsonPath.compile("$.store.book[*].author");
documentContext.set(p, "dimples");
  • 路徑定義到節點

此時不能直接使用set()方法,因爲此時返回的不是具體值的列表,而是所有book子節點的列表。此時使用put方法爲佳,如下,就實現了與上面代碼一樣的效果

JsonPath p = JsonPath.compile("$.*.book");
documentContext.put(p, author, "dimples");
  • 不替換所有key

在上面的兩種寫法中,我們會替換book節點下的所有節點的author的值,那麼我們怎麼實現根據我們的需要修改值呢?

  1. 過濾器表達式
// 修改 author 值爲 Nigel Rees 的元素的值
JsonPath p = JsonPath.compile(StrUtil.format("$..[?(@.author == 'Nigel Rees')]");
documentContext.put(p, author, "dimples");
  1. 使用過濾器
DocumentContext documentContext = JsonPath.parse(DATA);
filter = Filter.filter(Criteria.where("author ").is("Nigel Rees"));
// 替換
documentContext.put("$..[?]","author","dimples",filter);

其中 ? 代表過濾器的佔位符,如果沒有 ? ,那麼配置filter將無效。

也可以使用set方法,原理類似

根據值條件獲取路徑,然後修改
Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
Filter filter = Filter.filter(Criteria.where("author").contains("Nigel Rees");
// 獲取滿足條件值的路徑
List<String> path = JsonPath.using(conf).parse(JSON_DATA).read("$..[?]", filter);
// 替換 (此處舉例,不做遍歷,只獲取第一個)
documentContext.set(path.get(0), "dimples");

也可以使用put方法,原理類似

參考資料

https://blog.csdn.net/weixin_42452045/article/details/92768660
https://blog.csdn.net/Dream_Weave/article/details/106421388

FastJson - json-path

alibaba-json-path 官方參考:https://github.com/alibaba/fastjson/wiki/JSONPath

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章