PythonでJSONからキー/値を高速検索!7つの実装レシピ【コピペOK】

現場のエンジニアやデータサイエンティストからよく聞くのが「巨大な JSON を走査するときに速度が出ない」「ネストが深すぎて値を取り出すコードがゴチャつく」という悩み。
標準ライブラリの json だけで済ませると、構造が複雑になるほど処理コストと可読性が悪化しがちです。そこで本記事では Python×JSON の“読み取り”にフォーカスし——

  • 超高速ライブラリ(orjson/msgspec など)の導入
  • JSONPath・JQ クラスの問い合わせ言語を使った検索
  • pandas でフラット化 → クエリで絞り込む鉄板テク

……などを “7 つの実装レシピ” としてまとめました。
すべてコピペ OK なので、自社 API レスポンス解析やログ基盤の前処理にぜひお役立てください!


目次

Python で JSON を高速検索するための全体像

まずは「どこを最適化すべきか」を押さえましょう。
JSON 解析のボトルネックは大きく 3 つあります。

  1. パース(文字列 → Python オブジェクト)
  2. 検索ロジック(キー走査・条件絞り込み)
  3. 結果の変換(再シリアライズ or テーブル化)

標準モジュール json は信頼性こそ抜群ですが、「1. パース」が遅め。
そこで ネイティブコードで最適化された orjson や msgspec が重宝されます。orjson は C++/SIMD 命令で高速化されており、純 Python 実装より 10 倍以上速いベンチマーク結果も報告されています。

さらに 2024 年頃からは msgspec が「orjson より速い/バリデーションまでゼロコスト」という理由で注目度急上昇。GitHub の README でも「orjson 単体の decode より速い」と明記されています。

検索ロジックの面では——

  • JSONPath(jsonpath-ng):XPath ライクなクエリでネストを抽象化
  • JQ(pyjq):Unix の grep 的にスライス/フィルタ/変形

といった“クエリ言語”を活用すると、コード量とバグ率を大幅に下げられます。


主なライブラリ性能比較

ライブラリパース速度*特徴対応 Python
orjson★★★★★C++ & SIMD。datetime/NumPy を自動処理3.8 – 3.12
msgspec★★★★★+スキーマバリデーション込みで orjson に匹敵3.8 – 3.12
ujson★★★★☆orjson よりやや遅いが純粋な encode/decode に特化3.7 – 3.12
rapidjson★★★★☆C++ RapidJSON バインディング。option 多彩3.7 – 3.12
jsonpath-ng検索効率:★★★☆JSONPath に準拠。条件更新にも対応3.6 – 3.12
pyjq検索効率:★★★★☆JQ 構文そのまま。複雑変形が得意3.7 – 3.12

*公開ベンチマークを平均化した筆者試算。実データ量・ストラクチャで変動します。


7 つの実装レシピ【コピペ OK】

以下の <pre><code> ブロックを丸ごと貼り付けて動かし、自分の JSON に合わせてパスや条件を差し替えてください。

レシピ 1:標準 `json` × 再帰ジェネレータで全キー検索

import json
from pathlib import Path

def find_key(obj, target):
    if isinstance(obj, dict):
        for k, v in obj.items():
            if k == target:
                yield v
            yield from find_key(v, target)
    elif isinstance(obj, list):
        for item in obj:
            yield from find_key(item, target)

data = json.loads(Path('sample.json').read_text())
for value in find_key(data, "userId"):
    print(value)

ポイント

  • ネスト構造を意識せず “全部走査” できる汎用関数。
  • ジェネレータ (yield) なので巨大ファイルでもメモリ効率◎。
  • 欠点は パース速度と Python レイヤーのループ回数。大量データでは遅い。

レシピ 2:`orjson` でパース 10 倍速 & フィルタ

import orjson

with open("big.json", "rb") as f:
    data = orjson.loads(f.read())

results = [u["email"] for u in data["users"] if u.get("active")]
print(results[:5])

ポイント

  • orjson.loads() はバイト列を直接受け取り、json より桁違いに速い。citeturn0search0
  • デコード後は普通の dict/list。あとは Python 流儀で検索可能。
  • 日付・NumPy array なども自動変換されるので前処理いらず。

レシピ 3:`msgspec` でパース+スキーマ検証を同時に

import msgspec
from typing import List

class User(msgspec.Struct):
    id: int
    name: str
    is_active: bool

decoder = msgspec.json.Decoder(type=List[User])

with open("users.json", "rb") as f:
    users: List[User] = decoder.decode(f.read())

active_names = [u.name for u in users if u.is_active]
print(active_names[:5])

ポイント

  • Struct で型を宣言すると ゼロコストでバリデーション
  • ベンチマークでは「orjson 単体の decode より速い」報告あり。citeturn1search5
  • 生成されたオブジェクトは dataclass 互換で IDE 補完も効く。

レシピ 4:`jsonpath-ng` で XPath ライクに抽出

from jsonpath_ng import jsonpath, parse
import json, pathlib

data = json.loads(pathlib.Path("store.json").read_text())
expr = parse("$.store.book[?(@.price > 20)].title")
titles = [match.value for match in expr.find(data)]
print(titles)

ポイント

  • $.store.book[*] のような JSONPath で条件フィルタを宣言的に記述。citeturn0search2
  • 入れ子アクセスを for ループで書く手間がゼロ。
  • アップデート/削除も可能で「検索&置換」タスクに強い。

レシピ 5:`pyjq` で JQ フル活用(複雑変形も一発)

import pyjq, json

data = json.loads(open("sales.json").read())
# 店舗ごとの売上合計を抽出
query = 'group_by(.store)[] | {store: .[0].store, sum: map(.amount) | add}'
results = pyjq.all(query, data)
print(results)

ポイント

  • JQ の濃密なパイプラインを Python からそのまま呼び出し。citeturn2search1turn2search3
  • map/reduce 相当を 1 文で書けるため、大量 JSON ➔ 集約には最強。
  • インストール時に C ライブラリ (libjq) が必要なので CI 環境設定は要注意。

レシピ 6:`pandas.json_normalize` でフラット化→クエリ

import pandas as pd, json

df = pd.json_normalize(json.load(open("reviews.json")), record_path=["items"])
top = df.query("rating >= 4.5 & lang == 'ja'").nlargest(10, "rating")
print(top[["author", "rating"]])

ポイント

  • JSON を DataFrame に展開query / loc で超高速フィルタ。
  • CSV 書き出し・可視化もワンライナー。
  • 欠点はネスト構造が深すぎると列名が長大になる点。

レシピ 7:マルチプロセスで巨大ファイルを分割検索

from multiprocessing import Pool
import orjson, pathlib, itertools

def chunk_reader(path):
    for line in open(path, "rb"):
        yield orjson.loads(line)

def search_chunk(records):
    return [r["id"] for r in records if r.get("score", 0) > 90]

def batched(iterable, n):
    it = iter(iterable)
    while chunk := list(itertools.islice(it, n)):
        yield chunk

with Pool() as pool:
    ids = itertools.chain.from_iterable(
        pool.map(search_chunk, batched(chunk_reader("events.jsonl"), 10_000))
    )
    print(sum(1 for _ in ids))

ポイント

  • JSON Lines (JSONL) 形式なら 1 行ごとに独立デコードできる。
  • 1000 万行級でも CPU コアに乗せてスケールアウトが容易。
  • orjson + multiprocessing で I/O がボトルネックに。SSD or gzip+pipe 併用推奨。

よくあるエラー & デバッグ TIPS

症状原因対策
RecursionError: maximum recursion depth exceededレシピ1の深すぎるネストsys.setrecursionlimit() か iterative に書き換え
TypeError: Object of type datetime is not JSON serializableorjson 以外で datetime を encodeorjson/ msgspec に切り替え or default 引数実装
ValueError: Unexpected characterpyjq でクォート漏れquery 文字列内の ' / " をエスケープ
ModuleNotFoundError: libjq.sopyjq の依存ライブラリ未導入OS パッケージ jq を apt / brew で先に入れる

まとめ:要件別のベストプラクティス

  • 単純パース+速度重視orjson、型チェック込みなら msgspec
  • ネストを宣言的に検索jsonpath-ng(XPath ライク)、変形が必要なら pyjq
  • Big Data & BI 連携 ➔ DataFrame 化して pandas で一括フィルタ。
  • まずは小規模 JSON でベンチマークし、本番ログの実サイズで再検証するのが失敗しないコツです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次