JSONPathによるデータ抽出と更新

構文

  • $:ルートオブジェクトを表します
  • .:子ノードにアクセスします
  • ..:子ノードを再帰的に検索します
  • *:すべてのオブジェクトに一致するワイルドカード
  • @:現在のオブジェクトを表し、通常フィルタ式と組み合わせて使用します
  • ?():フィルタ式を使用します(他にも=~による正規表現や関数などがありますが、言語やライブラリごとにサポート状況が異なります。ここでは一般的で汎用的なものを記載します)
    • ==:等しいかどうか
    • !=:等しくないかどうか
    • <:より小さいかどうか
    • <=:より小さいか等しいかどうか
    • >:より大きいかどうか
    • >=:より大きいか等しいかどうか
    • &&:論理AND
    • ||:論理OR
    • !:論理NOT
    • in:対応するリスト値に含まれているかどうか
    • not in:対応するリスト値に含まれていないかどうか
  • ():式を使用し、通常フィルタ式と組み合わせて使用します
  • []:子ノードにアクセスします(マップオブジェクトの場合は文字で、配列オブジェクトの場合はインデックスでアクセスします)
    • ['key']:そのノードのキーを取得し、.keyと同等です
    • ['key1', 'key2']:そのノードのキーkey1、key2を取得します(カンマで区切ります)(または、この形式はin式の中で、一致する値がkey1またはkey2であるかどうかを意味します)
    • [0]:配列の最初の要素を取得します
    • [-1]:配列の最後の要素を取得します
    • [0, 1]:配列の最初と2番目の要素を取得します(カンマで区切ります)
    • [start:end:step]:スライスを使用して値を取得します(インデックスは0から開始し、左閉右開区間です。つまり、endのインデックスの値は取得されません。startが指定されていない場合は最初から、endが指定されていない場合は最後まで、stepが指定されていない場合はデフォルトで1です)
      • [0:3]:最初の3つの要素を取得します
      • [2:]:3番目から最後までの要素を取得します(最初と2番目は除きます)
      • [:5]:最初の5つの要素を取得します
      • [3:8:2]:4番目から開始し、2つごとに取得します(つまり、4、6、8番目の要素)
      • [::3]:先頭から末尾まで、3つごとに取得します
      • [1::2]:2番目から末尾まで、2つごとに取得します
      • [:20:5]:最初の20個の要素を、5つごとに取得します

Pythonでのjsonpathライブラリの使用

1. インストール

pip install jsonpath

注:jsonpath関連のPythonライブラリは他にもjsonpath-ngjsonpath-rwjmespathなどがあり、より豊富な機能を提供しています。ここではjsonpathを使用してデモンストレーションを行い、興味がある方は他の関連ライブラリのドキュメントを自分で探してみてください。

2. 主な関数

jsonpath.jsonpath(obj, expr, result_type='VALUE')を使用します:

  • obj:JSONデータ
  • expr:JSONPath式
  • result_type:返り値の形式(デフォルトはVALUE)
    • VALUE:対応する値を返します
    • IPATH:一致したパスを返します(インデックス -> リスト形式)
    • 上記以外の任意の値:一致したパスを返します($[]形式 -> 文字列形式)

3. テスト

import jsonpath

# テストデータ
json_data = {
    "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,
        }
    }
}

# 一致するものがない場合、返り値はFalse(空リストではない)になります
print(jsonpath.jsonpath(json_data, "$.wahaha"))
# すべてのprice値を返します:[8.95, 12.99, 8.99, 22.99, 19.95]
print(jsonpath.jsonpath(json_data, "$..price"))
# book配列内でpriceが10から20の間にあるprice値を返します:[12.99]
print(jsonpath.jsonpath(json_data, "$..book[?((@.price >= 10) && (@.price <= 20))].price"))
# book配列内でpriceが10から20の間にあるpriceのパスを返します:[['store', 'book', '1', 'price']]
print(jsonpath.jsonpath(json_data, "$..book[?((@.price >= 10) && (@.price <= 20))].price", result_type='IPATH'))
# book配列内でpriceが10から20の間にあるpriceのパスを返します:["$['store']['book'][1]['price']"]
print(jsonpath.jsonpath(json_data, "$..book[?((@.price >= 10) && (@.price <= 20))].price", result_type='PATH'))

4. 最適化(個人用、参考まで)

問題1:メソッドが結果を見つけられない場合、返り値はFalseです
問題2:クエリ機能しかなく、更新機能がありません

これらの問題を解決するために、簡単なラッパーを作成できます。

import jsonpath
from typing import Any

class JsonHandler:
    @staticmethod
    def fetch_value(data_obj: Any, path_expression: str, default_value: Any = [], 
                  raise_exception: bool = False) -> Any:
        """
            JSONPath式に基づいて値を取得します
        :param data_obj: JSONデータ
        :param path_expression: JSONPath式
        :param default_value: 結果が見つからない場合に返されるデフォルト値(raise_exceptionがFalseの場合に有効)
        :param raise_exception: 結果が見つからない場合に例外をスローするかどうか(デフォルトはFalse)
        :return: 具体的なデータ
        """
        result = jsonpath.jsonpath(data_obj, path_expression)
        # 結果が見つからない場合、返り値はFalseになるため、ここで見つからない場合の処理を行います
        if not result:
            message = f"取得失敗:JSONPath式 '{path_expression}' に一致するオブジェクトが見つかりません"
            # 見つからない場合に直接エラーを報告したい場合は、例外をスローします
            if raise_exception:
                raise AttributeError(message)
            # そうでない場合はデフォルト値を返します
            else:
                print(f"{message}。デフォルト値 '{default_value}' を返します")
                return default_value
        return result

    @staticmethod
    def update_value(data_obj: Any, path_expression: str, new_value: Any, 
                  raise_exception: bool = False) -> None:
        """
            JSONPath式に基づいて対応する値を更新します
        :param data_obj: JSONデータ
        :param path_expression: JSONPath式
        :param new_value: 更新後の値
        :param raise_exception: 結果が見つからない場合に例外をスローするかどうか(デフォルトはFalse)
        :return: None
        """
        # この値は関数の最初のパラメータ名と一致する必要があります
        _json_object_param_name = "data_obj"
        # IPATHでない場合、返り値は["$['store']['book'][1]['price']"]のような形式になります
        result = jsonpath.jsonpath(data_obj, path_expression, result_type="PATH")
        if not result:
            message = f"更新失敗:JSONPath式 '{path_expression}' に一致するオブジェクトが見つかりません"
            if raise_exception:
                raise ValueError(message)
            else:
                print(message)
                return

        for item in result:
            # この形式"$['store']['book'][1]['price']"から$を取り除くと、
            # Pythonで辞書の値を設定する方法(dict_obj[key] = value)になります
            # したがって、最初の$を取り除いたものが、データを変更するためのパスになります
            # 以下で文字列形式を使用しているため、設定する値が文字列の場合は、手動でクォートを追加する必要があります
            if isinstance(new_value, str):
                code = f"{_json_object_param_name}{item[1:]} = '{new_value}'"
            else:
                code = f"{_json_object_param_name}{item[1:]} = {new_value}"
            # exec関数を呼び出して文字列形式のコードを実行し、代入操作を行い、更新の効果を達成します
            exec(code)

if __name__ == '__main__':
    json_data = {
        "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,
            }
        }
    }
    
    # まず、isbnを持つbookのpriceを取得します
    path_expr = "$..book[?(@.isbn)].price"
    print(JsonHandler.fetch_value(json_data, path_expr))
    # そして更新を行います
    JsonHandler.update_value(json_data, path_expr, 15.2)
    # 更新が実際に行われたかどうかもう一度確認します
    print(JsonHandler.fetch_value(json_data, path_expr))
    # 念のため、公式の元の関数でも確認できます
    print(jsonpath.jsonpath(json_data, path_expr))
    # 実際のデータが更新されたかどうかも確認できます
    print(json_data)

タグ: jsonpath Python JSON data-manipulation

6月26日 23:40 投稿