Python のネストされた JSON オブジェクトから値を抽出するためのベストプラクティス

PythonBeginner
オンラインで実践に進む

はじめに

ネストされた Python JSON オブジェクトからデータをナビゲートし、抽出することは、データ処理における一般的なタスクです。API、設定ファイル、またはデータストレージを扱っているかにかかわらず、複雑な JSON 構造から値を効率的に抽出する方法を理解することは、Python 開発者にとって不可欠です。

この実験(Lab)では、Python を使用してネストされた JSON オブジェクトから値を抽出するための実践的なテクニックを学びます。基本的なインデックス作成から、欠落したキーを適切に処理するより堅牢な方法まで、さまざまなアプローチを探求します。この実験(Lab)の終わりには、Python プロジェクトでネストされた JSON データを処理するためのベストプラクティスに関する実践的な経験が得られます。

ネストされた JSON オブジェクトの作成と理解

JSON (JavaScript Object Notation) は、人間が読みやすく、機械が解析しやすい軽量なデータ交換形式です。Python では、JSON データは通常、辞書とリストとして表現されます。

この実験(Lab)全体で使用するサンプルネスト JSON オブジェクトを作成することから始めましょう。

サンプル JSON ファイルの作成

  1. WebIDE インターフェースを開き、/home/labex/project ディレクトリに sample.json という新しいファイルを作成します。

  2. 次の JSON コンテンツをファイルにコピーします。

{
  "person": {
    "name": "John Doe",
    "age": 35,
    "contact": {
      "email": "john.doe@example.com",
      "phone": "555-123-4567"
    },
    "address": {
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zip": "12345"
    },
    "hobbies": ["reading", "hiking", "photography"],
    "employment": {
      "company": "Tech Solutions Inc.",
      "position": "Software Developer",
      "years": 5,
      "projects": [
        {
          "name": "Project Alpha",
          "status": "completed"
        },
        {
          "name": "Project Beta",
          "status": "in-progress"
        }
      ]
    }
  }
}
  1. ファイルを保存します。

JSON 構造の理解

この JSON オブジェクトは、さまざまな属性を持つ人物を表しています。構造には以下が含まれます。

  • 単純なキーと値のペア (name, age)
  • ネストされたオブジェクト (contact, address, employment)
  • 配列 (hobbies)
  • オブジェクトの配列 (projects)

JSON データの構造を理解することは、そこから値を効果的に抽出するための最初のステップです。この構造を視覚化してみましょう。

person
 ├── name
 ├── age
 ├── contact
 │   ├── email
 │   └── phone
 ├── address
 │   ├── street
 │   ├── city
 │   ├── state
 │   └── zip
 ├── hobbies [array]
 └── employment
     ├── company
     ├── position
     ├── years
     └── projects [array of objects]
         ├── name
         └── status

Python での JSON の読み込み

次に、この JSON ファイルを読み込む Python スクリプトを作成しましょう。同じディレクトリに json_basics.py という新しいファイルを作成します。

import json

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Verify data loaded successfully
print("JSON data loaded successfully!")
print(f"Type of data: {type(data)}")
print(f"Keys at the top level: {list(data.keys())}")
print(f"Keys in the person object: {list(data['person'].keys())}")

次のコマンドでスクリプトを実行します。

python3 json_basics.py

次のような出力が表示されるはずです。

JSON data loaded successfully!
Type of data: <class 'dict'>
Keys at the top level: ['person']
Keys in the person object: ['name', 'age', 'contact', 'address', 'hobbies', 'employment']

これにより、JSON ファイルが Python 辞書として正しく読み込まれたことが確認されます。次のステップでは、このネストされた構造から値を抽出するためのさまざまな方法を探求します。

JSON データへのアクセスに関する基本的方法

JSON データが読み込まれたので、ネストされた JSON オブジェクト内の値にアクセスするための基本的方法を探求しましょう。

角括弧を使用した直接インデックス作成

ネストされた JSON オブジェクト内の値にアクセスする最も簡単な方法は、適切なキーを使用して角括弧を使用することです。direct_access.py という新しいファイルを作成しましょう。

import json

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Access simple values
name = data["person"]["name"]
age = data["person"]["age"]
print(f"Name: {name}, Age: {age}")

## Access nested values
email = data["person"]["contact"]["email"]
city = data["person"]["address"]["city"]
print(f"Email: {email}, City: {city}")

## Access array values
first_hobby = data["person"]["hobbies"][0]
all_hobbies = data["person"]["hobbies"]
print(f"First hobby: {first_hobby}")
print(f"All hobbies: {all_hobbies}")

## Access values in arrays of objects
first_project_name = data["person"]["employment"]["projects"][0]["name"]
first_project_status = data["person"]["employment"]["projects"][0]["status"]
print(f"First project: {first_project_name}, Status: {first_project_status}")

スクリプトを実行します。

python3 direct_access.py

次のような出力が表示されるはずです。

Name: John Doe, Age: 35
Email: john.doe@example.com, City: Anytown
First hobby: reading
All hobbies: ['reading', 'hiking', 'photography']
First project: Project Alpha, Status: completed

直接インデックス作成の問題点

直接インデックス作成は、JSON データの正確な構造を把握しており、すべてのキーが存在することが確実な場合にうまく機能します。ただし、キーが欠落している場合は、KeyError 例外が発生します。

この問題を実証するために、error_demo.py というファイルを作成しましょう。

import json

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

try:
    ## Try to access a key that doesn't exist
    occupation = data["person"]["occupation"]
    print(f"Occupation: {occupation}")
except KeyError as e:
    print(f"Error occurred: KeyError - {e}")

## Now let's try a nested key that doesn't exist
try:
    salary = data["person"]["employment"]["salary"]
    print(f"Salary: {salary}")
except KeyError as e:
    print(f"Error occurred: KeyError - {e}")

スクリプトを実行します。

python3 error_demo.py

次のような出力が表示されるはずです。

Error occurred: KeyError - 'occupation'
Error occurred: KeyError - 'salary'

ご覧のように、直接インデックス作成は、キーが存在しない場合に例外を発生させます。実際のアプリケーション、特に外部 API やユーザーが生成したデータを扱う場合、JSON オブジェクトの構造は異なる可能性があります。次のステップでは、欠落したキーを適切に処理する、ネストされた JSON データにアクセスするためのより安全な方法を探求します。

ネストされた JSON データへの安全なアクセス方法

実際のアプリケーションでは、キーが欠落している場合や、JSON の構造が異なる場合を処理する必要がよくあります。ネストされた JSON データにアクセスするための、より安全な方法を探求しましょう。

get() メソッドの使用

辞書の get() メソッドを使用すると、キーが見つからない場合にデフォルト値を指定できます。これにより、KeyError 例外を回避できます。safe_access.py というファイルを作成しましょう。

import json

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Using get() for safer access
name = data.get("person", {}).get("name", "Unknown")
## If the "occupation" key doesn't exist, "Not specified" will be returned
occupation = data.get("person", {}).get("occupation", "Not specified")

print(f"Name: {name}")
print(f"Occupation: {occupation}")

## Accessing deeply nested values with get()
company = data.get("person", {}).get("employment", {}).get("company", "Unknown")
salary = data.get("person", {}).get("employment", {}).get("salary", "Not specified")

print(f"Company: {company}")
print(f"Salary: {salary}")

## Providing default values for arrays
hobbies = data.get("person", {}).get("hobbies", [])
skills = data.get("person", {}).get("skills", ["No skills listed"])

print(f"Hobbies: {', '.join(hobbies)}")
print(f"Skills: {', '.join(skills)}")

スクリプトを実行します。

python3 safe_access.py

次のような出力が表示されるはずです。

Name: John Doe
Occupation: Not specified
Company: Tech Solutions Inc.
Salary: Not specified
Hobbies: reading, hiking, photography
Skills: No skills listed

"occupation" や "skills" のようなキーが JSON に存在しないにもかかわらず、エラーが発生しなかったことに注目してください。代わりに、指定したデフォルト値が得られました。

深くネストされたデータに対する get() の連鎖

深くネストされた JSON 構造を扱う場合、複数の get() 呼び出しを連鎖させると冗長になり、読みづらくなる可能性があります。より読みやすいバージョンを、変数を使用して作成しましょう。chained_get.py というファイルを作成します。

import json

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Step-by-step approach for deeply nested values
person = data.get("person", {})
employment = person.get("employment", {})
projects = employment.get("projects", [])

## Get the first project or an empty dict if projects list is empty
first_project = projects[0] if projects else {}
project_name = first_project.get("name", "No project")
project_status = first_project.get("status", "Unknown")

print(f"Project: {project_name}, Status: {project_status}")

## Let's try a non-existent path
person = data.get("person", {})
education = person.get("education", {})
degree = education.get("degree", "Not specified")

print(f"Degree: {degree}")

スクリプトを実行します。

python3 chained_get.py

次のような出力が表示されるはずです。

Project: Project Alpha, Status: completed
Degree: Not specified

このアプローチは、ネストのレベルを個別の変数に分割するため、より読みやすくなっています。また、各レベルでデフォルト値を指定しているため、安全でもあります。

Default Dict の使用

もう一つのアプローチは、Python の collections モジュールから defaultdict を使用することです。これは、欠落しているキーに対して自動的にデフォルト値を提供します。default_dict.py というファイルを作成します。

import json
from collections import defaultdict

## Function to create a nested defaultdict
def nested_defaultdict():
    return defaultdict(nested_defaultdict)

## Load JSON from file
with open('sample.json', 'r') as file:
    regular_data = json.load(file)

## Convert to defaultdict
def dict_to_defaultdict(d):
    if not isinstance(d, dict):
        return d
    result = defaultdict(nested_defaultdict)
    for k, v in d.items():
        if isinstance(v, dict):
            result[k] = dict_to_defaultdict(v)
        elif isinstance(v, list):
            result[k] = [dict_to_defaultdict(item) if isinstance(item, dict) else item for item in v]
        else:
            result[k] = v
    return result

## Convert our data to defaultdict
data = dict_to_defaultdict(regular_data)

## Now we can access keys that don't exist without errors
print(f"Name: {data['person']['name']}")
print(f"Occupation: {data['person']['occupation']}")  ## This key doesn't exist!
print(f"Education: {data['person']['education']['degree']}")  ## Deeply nested non-existent path

## defaultdict returns another defaultdict for missing keys, which might not be what you want
print(f"Type of data['person']['occupation']: {type(data['person']['occupation'])}")

## To get a specific default value, you would use get() even with defaultdict
occupation = data['person'].get('occupation', 'Not specified')
print(f"Occupation with default: {occupation}")

スクリプトを実行します。

python3 default_dict.py

次のような出力が表示されるはずです。

Name: John Doe
Occupation: defaultdict(<function nested_defaultdict at 0x...>, {})
Education: defaultdict(<function nested_defaultdict at 0x...>, {})
Type of data['person']['occupation']: <class 'collections.defaultdict'>
Occupation with default: Not specified

defaultdictKeyError 例外を回避しますが、欠落しているキーに対して別の defaultdict を返します。これは、必要なデフォルト値ではない可能性があります。これが、get() メソッドが特定のデフォルト値を提供する上で、より実用的である理由です。

次のステップでは、安全性と柔軟性の両方を備えた、深くネストされた JSON から値を抽出するためのユーティリティ関数を作成する方法を探求します。

JSON 値抽出のためのユーティリティ関数の作成

ネストされた JSON データへのさまざまなアクセス方法を探求したので、複雑なネスト構造から値を抽出するのを容易にするユーティリティ関数を作成しましょう。この関数は、get() メソッドの安全性と、さまざまな種類のデータを処理する柔軟性を組み合わせます。

パスベースの抽出関数

json_extractor.py という新しいファイルを作成します。

import json
from typing import Any, List, Dict, Union, Optional

def extract_value(data: Dict, path: List[str], default: Any = None) -> Any:
    """
    Safely extract a value from a nested dictionary using a path list.

    Args:
        data: The dictionary to extract value from
        path: A list of keys representing the path to the value
        default: The default value to return if the path doesn't exist

    Returns:
        The value at the specified path or the default value if not found
    """
    current = data
    for key in path:
        if isinstance(current, dict) and key in current:
            current = current[key]
        else:
            return default
    return current

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Basic usage examples
name = extract_value(data, ["person", "name"], "Unknown")
age = extract_value(data, ["person", "age"], 0)
print(f"Name: {name}, Age: {age}")

## Extracting values that don't exist
occupation = extract_value(data, ["person", "occupation"], "Not specified")
print(f"Occupation: {occupation}")

## Extracting deeply nested values
email = extract_value(data, ["person", "contact", "email"], "No email")
phone = extract_value(data, ["person", "contact", "phone"], "No phone")
print(f"Email: {email}, Phone: {phone}")

## Extracting from arrays
if isinstance(extract_value(data, ["person", "hobbies"], []), list):
    first_hobby = extract_value(data, ["person", "hobbies"], [])[0] if extract_value(data, ["person", "hobbies"], []) else "No hobbies"
else:
    first_hobby = "No hobbies"
print(f"First hobby: {first_hobby}")

## Extracting from arrays of objects
projects = extract_value(data, ["person", "employment", "projects"], [])
if projects and len(projects) > 0:
    first_project_name = extract_value(projects[0], ["name"], "Unknown project")
    first_project_status = extract_value(projects[0], ["status"], "Unknown status")
    print(f"First project: {first_project_name}, Status: {first_project_status}")
else:
    print("No projects found")

スクリプトを実行します。

python3 json_extractor.py

次のような出力が表示されるはずです。

Name: John Doe, Age: 35
Occupation: Not specified
Email: john.doe@example.com, Phone: 555-123-4567
First hobby: reading
First project: Project Alpha, Status: completed

パス文字列を使用した拡張 JSON エクストラクタ

エクストラクタを拡張して、パスにドット表記をサポートし、より直感的に使用できるようにしましょう。enhanced_extractor.py というファイルを作成します。

import json
from typing import Any, Dict, List, Union

def get_nested_value(data: Dict, path_string: str, default: Any = None) -> Any:
    """
    Safely extract a value from a nested dictionary using a dot-separated path string.

    Args:
        data: The dictionary to extract value from
        path_string: A dot-separated string representing the path to the value
        default: The default value to return if the path doesn't exist

    Returns:
        The value at the specified path or the default value if not found
    """
    ## Convert the path string to a list of keys
    path = path_string.split(".")

    ## Start with the full dictionary
    current = data

    ## Follow the path
    for key in path:
        ## Handle list indexing with [n] notation
        if key.endswith("]") and "[" in key:
            list_key, index_str = key.split("[")
            index = int(index_str[:-1])  ## Remove the closing bracket and convert to int

            ## Get the list
            if list_key:  ## If there's a key before the bracket
                if not isinstance(current, dict) or list_key not in current:
                    return default
                current = current[list_key]

            ## Get the item at the specified index
            if not isinstance(current, list) or index >= len(current):
                return default
            current = current[index]
        else:
            ## Regular dictionary key
            if not isinstance(current, dict) or key not in current:
                return default
            current = current[key]

    return current

## Load JSON from file
with open('sample.json', 'r') as file:
    data = json.load(file)

## Test the enhanced extractor
print("Basic access:")
print(f"Name: {get_nested_value(data, 'person.name', 'Unknown')}")
print(f"Age: {get_nested_value(data, 'person.age', 0)}")
print(f"Occupation: {get_nested_value(data, 'person.occupation', 'Not specified')}")

print("\nNested access:")
print(f"Email: {get_nested_value(data, 'person.contact.email', 'No email')}")
print(f"City: {get_nested_value(data, 'person.address.city', 'Unknown city')}")

print("\nArray access:")
print(f"First hobby: {get_nested_value(data, 'person.hobbies[0]', 'No hobbies')}")
print(f"Second hobby: {get_nested_value(data, 'person.hobbies[1]', 'No second hobby')}")
print(f"Non-existent hobby: {get_nested_value(data, 'person.hobbies[10]', 'No such hobby')}")

print("\nComplex access:")
print(f"Company: {get_nested_value(data, 'person.employment.company', 'Unknown company')}")
print(f"First project name: {get_nested_value(data, 'person.employment.projects[0].name', 'No project')}")
print(f"Second project status: {get_nested_value(data, 'person.employment.projects[1].status', 'Unknown status')}")
print(f"Non-existent project: {get_nested_value(data, 'person.employment.projects[2].name', 'No such project')}")
print(f"Education: {get_nested_value(data, 'person.education.degree', 'No education info')}")

スクリプトを実行します。

python3 enhanced_extractor.py

次のような出力が表示されるはずです。

Basic access:
Name: John Doe
Age: 35
Occupation: Not specified

Nested access:
Email: john.doe@example.com
City: Anytown

Array access:
First hobby: reading
Second hobby: hiking
Non-existent hobby: No such hobby

Complex access:
Company: Tech Solutions Inc.
First project name: Project Alpha
Second project status: in-progress
Non-existent project: No such project
Education: No education info

実用的なアプリケーション

次に、拡張された JSON エクストラクタを、より複雑な現実世界のシナリオに適用してみましょう。practical_example.py というファイルを作成します。

import json
import os
from typing import Any, Dict, List

## Import our enhanced extractor function
from enhanced_extractor import get_nested_value

## Create a more complex JSON structure for reporting
report_data = {
    "company": "Global Analytics Ltd.",
    "report_date": "2023-11-01",
    "departments": [
        {
            "name": "Engineering",
            "manager": "Alice Johnson",
            "employee_count": 45,
            "projects": [
                {"id": "E001", "name": "API Gateway", "status": "completed", "budget": 125000},
                {"id": "E002", "name": "Mobile App", "status": "in-progress", "budget": 200000}
            ]
        },
        {
            "name": "Marketing",
            "manager": "Bob Smith",
            "employee_count": 28,
            "projects": [
                {"id": "M001", "name": "Q4 Campaign", "status": "planning", "budget": 75000}
            ]
        },
        {
            "name": "Customer Support",
            "manager": "Carol Williams",
            "employee_count": 32,
            "projects": []
        }
    ],
    "financial": {
        "current_quarter": {
            "revenue": 2500000,
            "expenses": 1800000,
            "profit_margin": 0.28
        },
        "previous_quarter": {
            "revenue": 2300000,
            "expenses": 1750000,
            "profit_margin": 0.24
        }
    }
}

## Save this data to a JSON file for demonstration
with open('report.json', 'w') as file:
    json.dump(report_data, file, indent=2)

print("Report data saved to report.json")

## Now let's extract useful information from this report
def generate_summary(data: Dict) -> str:
    """Generate a summary of the company report"""

    company = get_nested_value(data, "company", "Unknown Company")
    report_date = get_nested_value(data, "report_date", "Unknown Date")

    ## Financial summary
    current_revenue = get_nested_value(data, "financial.current_quarter.revenue", 0)
    previous_revenue = get_nested_value(data, "financial.previous_quarter.revenue", 0)
    revenue_change = current_revenue - previous_revenue
    revenue_change_percent = (revenue_change / previous_revenue * 100) if previous_revenue > 0 else 0

    ## Department summary
    departments = get_nested_value(data, "departments", [])
    total_employees = sum(get_nested_value(dept, "employee_count", 0) for dept in departments)

    ## Project counts
    total_projects = sum(len(get_nested_value(dept, "projects", [])) for dept in departments)
    completed_projects = sum(
        1 for dept in departments
        for proj in get_nested_value(dept, "projects", [])
        if get_nested_value(proj, "status", "") == "completed"
    )

    ## Generate summary text
    summary = f"Company Report Summary for {company} as of {report_date}\n"
    summary += "=" * 50 + "\n\n"

    summary += "Financial Overview:\n"
    summary += f"- Current Quarter Revenue: ${current_revenue:,}\n"
    summary += f"- Revenue Change: ${revenue_change:,} ({revenue_change_percent:.1f}%)\n\n"

    summary += "Operational Overview:\n"
    summary += f"- Total Departments: {len(departments)}\n"
    summary += f"- Total Employees: {total_employees}\n"
    summary += f"- Total Projects: {total_projects}\n"
    summary += f"- Completed Projects: {completed_projects}\n\n"

    summary += "Department Details:\n"
    for i, dept in enumerate(departments):
        dept_name = get_nested_value(dept, "name", f"Department {i+1}")
        manager = get_nested_value(dept, "manager", "No manager")
        employees = get_nested_value(dept, "employee_count", 0)
        projects = get_nested_value(dept, "projects", [])

        summary += f"- {dept_name} (Manager: {manager})\n"
        summary += f"  * Employees: {employees}\n"
        summary += f"  * Projects: {len(projects)}\n"

        if projects:
            for proj in projects:
                proj_name = get_nested_value(proj, "name", "Unnamed Project")
                proj_status = get_nested_value(proj, "status", "unknown")
                proj_budget = get_nested_value(proj, "budget", 0)

                summary += f"    - {proj_name} (Status: {proj_status}, Budget: ${proj_budget:,})\n"
        else:
            summary += "    - No active projects\n"

        summary += "\n"

    return summary

## Generate and display the summary
summary = generate_summary(report_data)
print("\nGenerated Report Summary:")
print(summary)

## Save the summary to a file
with open('report_summary.txt', 'w') as file:
    file.write(summary)

print("Summary saved to report_summary.txt")

スクリプトを実行します。

python3 practical_example.py

レポートデータが保存されたことを確認するメッセージが表示され、その後に会社のレポートの詳細な概要が表示されます。

出力ファイルを確認します。

cat report_summary.txt

この実用的な例は、JSON エクストラクタユーティリティを使用して、欠落しているデータを適切に処理する堅牢なレポートツールを構築する方法を示しています。get_nested_value 関数を使用すると、KeyError や NoneType 例外を気にすることなく、複雑なネスト構造から安全に値を抽出できます。

ベストプラクティスの概要

このラボで探求したテクニックに基づいて、ネストされた JSON オブジェクトから値を抽出するためのベストプラクティスを以下に示します。

  1. get() メソッドを使用する 直接インデックス作成の代わりに、欠落しているキーのデフォルト値を指定します。
  2. ユーティリティ関数を作成する 繰り返しコードを避けるために、一般的な JSON 抽出パターンに対して。
  3. 欠落しているパスを適切に処理する 適切なデフォルト値を指定することによって。
  4. 値を型チェックする エラーを回避するために、処理する前に(たとえば、インデックスにアクセスする前に値がリストかどうかを確認する)。
  5. 複雑なパスを分割する より読みやすくするために、個別の変数に。
  6. ドット表記のパス文字列を使用する ネストされた値へのより直感的なアクセスを実現するために。
  7. 抽出コードをドキュメント化する JSON 構造で何を探しているのかを明確にするために。

これらのベストプラクティスに従うことで、Python でネストされた JSON オブジェクトを操作するための、より堅牢で保守性の高いコードを作成できます。

まとめ

この実験では、Python でネストされた JSON オブジェクトから値を抽出するための実用的なテクニックを学びました。ネストされた JSON の構造と、Python で JSON データをロードする方法を理解することから始めました。次に、基本的なインデックス作成から、より堅牢なアプローチまで、ネストされたデータにアクセスするためのさまざまな方法を探求しました。

この実験からの主なポイントは次のとおりです。

  1. JSON 構造の理解: ネストされた JSON オブジェクトの階層的な性質を認識することは、その値を効果的にアクセスするための基本です。

  2. 基本的なアクセス方法: 角括弧を使用した直接インデックス作成は、JSON データの構造が確実な場合に機能します。

  3. 安全なアクセス技術: get() メソッドを使用すると、欠落しているキーのデフォルト値が提供され、不確実なデータ構造を扱う場合にコードがより堅牢になります。

  4. ユーティリティ関数: JSON 抽出用の特殊な関数を作成すると、コードが大幅に簡素化され、保守性が向上します。

  5. パスベースのアクセス: ドット表記のパス文字列を使用すると、深くネストされた値にアクセスするための直感的な方法が提供されます。

  6. 実際のアプリケーション: これらのテクニックを実際のシナリオに適用すると、堅牢なデータ処理ツールを構築するのに役立ちます。

これらのベストプラクティスに従うことで、構造が不規則であったり、欠落している値が含まれていたりする場合でも、ネストされた JSON データの複雑さを適切に処理する、より回復力のあるコードを作成できます。