Python json.loads()を使った業務データ処理パターン|実装例と注意点

Python

Python json.loads()を使った業務データ処理パターン|実装例と注意点

Pythonでシステム開発を行うとき、JSONフォーマットのデータ処理は避けて通れません。APIレスポンス、設定ファイル、ログデータなど、あらゆる場面でJSONを扱う必要があります。本記事では、Pythonのjson.loads()関数を使った実務的なパターンを、実際のプロジェクトで使える例を交えて解説します。

json.loads()の簡易的な解説

json.loads()は、JSON形式の文字列をPythonのオブジェクト(辞書やリスト)に変換する関数です。名前の「s」は「string」を意味し、文字列入力を受け取ります。

import json

# JSON文字列をPythonのdictに変換
json_string = '{\"name\": \"田中太郎\", \"age\": 35, \"department\": \"営業部\"}'
data = json.loads(json_string)

print(data['name'])  # 出力: 田中太郎
print(type(data))    # 出力: <class 'dict'>

このシンプルな機能ですが、業務で使う際にはエラーハンドリング、データ検証、エンコーディング処理など様々な工夫が必要になります。

業務でのユースケース

ユースケース1:REST APIからのレスポンス処理

最も一般的な使用例がAPIレスポンスの処理です。外部APIから取得したレスポンスボディはJSON文字列形式で返されることがほとんどです。

ユースケース2:ログファイルの解析

アプリケーションログの多くはJSON形式で保存されます。これらを解析して、特定の条件に合致するエントリを抽出する処理が日常的に発生します。

ユースケース3:設定ファイルの読み込み

マイクロサービスやコンテナ環境では、環境ごとの設定をJSON形式で管理することが増えています。

ユースケース4:ファイルのバッチ処理

大量のJSONファイルをバッチ処理する際に、json.loads()は欠かせません。

実装コード:実務で使えるパターン

基本パターン:APIレスポンスの処理

まず、REST APIからのレスポンスを処理する実装例です。

import json
import requests
from typing import Dict, Any, Optional
from datetime import datetime

class APIClient:
    \"\"\"業務用APIクライアント\"\"\"
    
    def __init__(self, base_url: str):
        self.base_url = base_url
    
    def fetch_user_data(self, user_id: int) -> Optional[Dict[str, Any]]:
        \"\"\"ユーザー情報をAPIから取得してjson.loads()で処理\"\"\"
        try:
            response = requests.get(f'{self.base_url}/users/{user_id}')
            response.raise_for_status()
            
            # APIレスポンスのテキストをjson.loads()で変換
            user_data = json.loads(response.text)
            
            # 取得したデータの妥当性チェック
            if self._validate_user_data(user_data):
                return user_data
            else:
                print(f'警告:不正なユーザーデータ形式 - ID: {user_id}')
                return None
                
        except requests.exceptions.RequestException as e:
            print(f'APIリクエストエラー: {e}')
            return None
        except json.JSONDecodeError as e:
            print(f'JSON解析エラー: {e}')
            return None
    
    def _validate_user_data(self, data: Dict) -> bool:
        \"\"\"ユーザーデータの妥当性確認\"\"\"
        required_fields = ['id', 'name', 'email']
        return all(field in data for field in required_fields)

# 実際の使用例
if __name__ == '__main__':
    client = APIClient('https://api.example.com')
    user = client.fetch_user_data(123)
    
    if user:
        print(f'ユーザー名: {user[\"name\"]}')
        print(f'メール: {user[\"email\"]}')

応用パターン1:複数JSONのバッチ処理

ログファイルなど、複数のJSON文字列が改行で区切られているファイルを処理する場合があります。

import json
from pathlib import Path
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime

@dataclass
class LogEntry:
    \"\"\"ログエントリを表すクラス\"\"\"
    timestamp: str
    level: str
    message: str
    user_id: Optional[int] = None

class LogAnalyzer:
    \"\"\"JSON形式のログファイルを分析\"\"\"
    
    def parse_jsonl_file(self, filepath: str) -> List[LogEntry]:
        \"\"\"
        JSONL形式(JSON Lines)のログファイルを読み込み
        各行がJSON文字列のファイル形式に対応
        \"\"\"
        entries = []
        error_count = 0
        
        with open(filepath, 'r', encoding='utf-8') as f:
            for line_number, line in enumerate(f, 1):
                line = line.strip()
                
                # 空行はスキップ
                if not line:
                    continue
                
                try:
                    # json.loads()で各行を処理
                    log_dict = json.loads(line)
                    
                    # データの変換と検証
                    entry = self._convert_to_entry(log_dict)
                    if entry:
                        entries.append(entry)
                        
                except json.JSONDecodeError as e:
                    error_count += 1
                    print(f'警告:{line_number}行目のJSON解析失敗 - {e}')
                    continue
        
        if error_count > 0:
            print(f'合計 {error_count} 行の解析に失敗しました')
        
        return entries
    
    def _convert_to_entry(self, log_dict: Dict) -> Optional[LogEntry]:
        \"\"\"JSON辞書をLogEntryに変換\"\"\"
        try:
            return LogEntry(
                timestamp=log_dict.get('timestamp', ''),
                level=log_dict.get('level', 'INFO'),
                message=log_dict.get('message', ''),
                user_id=log_dict.get('user_id')
            )
        except (KeyError, ValueError) as e:
            print(f'データ変換エラー: {e}')
            return None
    
    def filter_by_level(self, entries: List[LogEntry], level: str) -> List[LogEntry]:
        \"\"\"指定したログレベルのエントリをフィルタリング\"\"\"
        return [e for e in entries if e.level == level]
    
    def filter_by_user(self, entries: List[LogEntry], user_id: int) -> List[LogEntry]:
        \"\"\"特定ユーザーのログを抽出\"\"\"
        return [e for e in entries if e.user_id == user_id]

# 実際の使用例
if __name__ == '__main__':
    analyzer = LogAnalyzer()
    entries = analyzer.parse_jsonl_file('/var/log/app.jsonl')
    
    # エラーログのみを抽出
    error_logs = analyzer.filter_by_level(entries, 'ERROR')
    print(f'エラーログ件数: {len(error_logs)}')
    
    # 特定ユーザーのログを抽出
    user_logs = analyzer.filter_by_user(entries, 999)
    for log in user_logs:
        print(f'[{log.timestamp}] {log.level}: {log.message}')

応用パターン2:設定ファイルの読み込みと検証

マイクロサービスでよくある、環境ごとの設定をJSON形式で管理するパターンです。

import json
import os
from typing import Dict, Any, Optional
from enum import Enum

class Environment(Enum):
    \"\"\"実行環境の定義\"\"\"
    DEVELOPMENT = 'development'
    STAGING = 'staging'
    PRODUCTION = 'production'

class ConfigManager:
    \"\"\"JSON設定ファイルを管理するクラス\"\"\"
    
    def __init__(self, config_dir: str = './config'):
        self.config_dir = config_dir
        self._config_cache: Dict[str, Any] = {}
    
    def load_config(self, env: Environment) -> Dict[str, Any]:
        \"\"\"環境ごとの設定ファイルを読み込み\"\"\"
        config_file = os.path.join(self.config_dir, f'{env.value}.json')
        
        if not os.path.exists(config_file):
            raise FileNotFoundError(f'設定ファイルが見つかりません: {config_file}')
        
        try:
            with open(config_file, 'r', encoding='utf-8') as f:
                # ファイルの内容をjson.loads()で変換
                config = json.loads(f.read())
            
            # 必須フィールドの検証
            self._validate_config(config)
            
            # キャッシュに保存
            self._config_cache[env.value] = config
            
            return config
            
        except json.JSONDecodeError as e:
            raise ValueError(f'設定ファイルのJSON形式が不正です: {e}')
    
    def _validate_config(self, config: Dict[str, Any]) -> None:
        \"\"\"設定の必須項目を確認\"\"\"
        required_keys = ['database', 'api_endpoint', 'log_level']
        
        missing_keys = [key for key in required_keys if key not in config]
        
        if missing_keys:
            raise ValueError(f'設定に必須キーがありません: {missing_keys}')
    
    def get_config(self, env: Environment) -> Dict[str, Any]:
        \"\"\"キャッシュから設定を取得。なければ読み込み\"\"\"
        env_key = env.value
        
        if env_key not in self._config_cache:
            self.load_config(env)
        
        return self._config_cache[env_key]

# 実際の使用例
if __name__ == '__main__':
    manager = ConfigManager()
    
    # 環境変数で環境を指定(デフォルトはdevelopment)
    env_name = os.getenv('APP_ENV', 'development')
    env = Environment(env_name)
    
    config = manager.get_config(env)
    
    print(f'データベース接続先: {config[\"database\"][\"host\"]}')
    print(f'APIエンドポイント: {config[\"api_endpoint\"]}')

設定ファイルの例(config/production.json):

{
  \"database\": {
    \"host\": \"db.production.internal\",
    \"port\": 5432,
    \"name\": \"prod_db\",
    \"pool_size\": 20
  },
  \"api_endpoint\": \"https://api.example.com\",
  \"log_level\": \"INFO\",
  \"cache\": {
    \"enabled\": true,
    \"ttl\": 3600
  }
}

応用パターン3:ネストされたJSON構造の抽出

複雑にネストされたJSONから特定のデータを安全に抽出するパターンです。

import json
from typing import Any, Optional, List
from functools import reduce

class DeepJsonExtractor:
    \"\"\"深くネストされたJSON構造から値を安全に抽出\"\"\"
    
    @staticmethod
    def get_nested_value(
        data: Dict[str, Any],
        path: str,
        default: Any = None
    ) -> Any:
        \"\"\"
        ドット記法でネストされた値にアクセス
        例: 'user.profile.address.city'
        \"\"\"
        try:
            keys = path.split('.')
            value = reduce(
                lambda obj, key: obj[key] if isinstance(obj, dict) else None,
                keys,
                data
            )
            return value if value is not None else default
        except (KeyError, TypeError):
            return default
    
    @staticmethod
    def extract_multiple(
        json_string: str,
        paths: List[str]
    ) -> Dict[str, Any]:
        \"\"\"複数のパスから値を一度に抽出\"\"\"
        try:
            data = json.loads(json_string)
            result = {}
            
            for path in paths:
                result[path] = DeepJsonExtractor.get_nested_value(data, path)
            
            return result
            
        except json.JSONDecodeError as e:
            print(f'JSON解析エラー: {e}')
            return {}

# 実際の使用例
if __name__ == '__main__':
    # ネストされたJSON構造の例
    api_response = '''
    {
        \"status\": \"success\",
        \"data\": {
            \"user\": {
                \"id\": 12345,
                \"profile\": {
                    \"name\": \"佐藤花子\",
                    \"contact\": {
                        \"email\": \"hanako@example.com\",
                        \"phone\": \"090-1234-5678\",
                        \"address\": {
                            \"prefecture\": \"東京都\",
                            \"city\": \"渋谷区\",
                            \"postal_code\": \"150-0001\"
                        }
                    }
                }
            },
            \"metadata\": {
                \"request_id\": \"req-001\",
                \"timestamp\": \"2024-01-15T10:30:00Z\"
            }
        }
    }
    '''
    
    extractor = DeepJsonExtractor()
    
    # 個別に値を取得
    city = extractor.get_nested_value(
        json.loads(api_response),
        'data.user.profile.contact.address.city'
    )
    print(f'取得した市区町村: {city}')
    
    # 複数の値を一度に取得
    paths_to_extract = [
        'data.user.id',
        'data.user.profile.name',
        'data.user.profile.contact.email',
        'data.metadata.timestamp'
    ]
    
    results = extractor.extract_multiple(api_response, paths_to_extract)
    for path, value in results.items():
        print(f'{path}: {value}')

応用パターン4:エラーハンドリングと回復戦略

実務では、部分的に壊れたJSONや予期しない形式のデータに対応する必要があります。

import json
import logging
from typing import Dict, Any, Optional, Union
from datetime import datetime, timedelta

# ロギング設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class RobustJsonParser:
    \"\"\"耐障害性を備えたJSON解析クラス\"\"\"
    
    def __init__(self, max_retries: int = 3):
        self.max_retries = max_retries
        self.parse_errors: List[Dict[str, Any]] = []
    
    def parse_with_fallback(
        self,
        json_string: str,
        fallback_value: Optional[Dict] = None
    ) -> Union[Dict[str, Any], None]:
        \"\"\"
        JSONパースに失敗した場合、フォールバック値を返す
        エラーログに記録して追跡可能にする
        \"\"\"
        
        # 最初に標準的なパースを試みる
        try:
            return json.loads(json_string)
        except json.JSONDecodeError as e:
            logger.warning(f'通常のパース失敗: {e}')
        
        # 修復を試みる
        repaired_json = self._try_repair_json(json_string)
        if repaired_json:
            try:
                return json.loads(repaired_json)
            except json.JSONDecodeError:
                pass
        
        # フォールバック値を使用
        error_record = {
            'original': json_string[:200],  # 最初の200文字
            'timestamp': datetime.now().isoformat(),
            'fallback_used': fallback_value is not None
        }
        self.parse_errors.append(error_record)
        logger.error(f'JSON解析完全失敗。フォールバック使用: {error_record}')
        
        return fallback_value
    
    def _try_repair_json(self, json_string: str) -> Optional[str]:
        \"\"\"簡単なJSON修復を試みる\"\"\"
        # よくある問題を修復
        
        # シングルクォートをダブルクォートに変換
        if \"'\" in json_string:
            try:
                repaired = json_string.replace(\"'\", '\"')
                json.loads(repaired)  # 有効性確認
                return repaired
            except:
                pass
        
        # 末尾のカンマを削除
        if json_string.rstrip().endswith(','):
            try:
                repaired = json_string.rstrip()[:-1]
                json.loads(repaired)
                return repaired
            except:
                pass
        
        return None
    
    def parse_with_partial_recovery(
        self,
        json_string: str
    ) -> Dict[str, Any]:
        \"\"\"
        部分的に壊れたJSONから抽出可能な部分を回復
        \"\"\"
        
        # 開き括弧を数える
        open_braces = json_string.count('{')
        close_braces = json_string.count('}')
        
        if open_braces > close_braces:
            # 閉じる括弧が不足している
            repaired = json_string + '}' * (open_braces - close_braces)
            try:
                return json.loads(repaired)
            except:
                pass
        
        # 複数のJSON対象を探す
        candidates = self._find_json_candidates(json_string)
        for candidate in candidates:
            try:
                return json.loads(candidate)
            except:
                continue
        
        return {}
    
    def _find_json_candidates(self, text: str) -> List[str]:
        \"\"\"テキストから有効そうなJSON候補を抽出\"\"\"
        candidates = []
        
        # 最初の{ から最後の} までを抽出
        first_brace = text.find('{')
        last_brace = text.rfind('}')
        
        if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
            candidates.append(text[first_brace:last_brace+1])
        
        # 配列の場合
        first_bracket = text.find('[')
        last_bracket = text.rfind(']')
        
        if first_bracket != -1 and last_bracket != -1 and first_bracket < last_bracket:
            candidates.append(text[first_bracket:last_bracket+1])
        
        return candidates
    
    def get_error_report(self) -> Dict[str, Any]:
        \"\"\"パースエラーの統計を返す\"\"\"
        return {
            'total_errors': len(self.parse_errors),
            'recent_errors': self.parse_errors[-5:] if self.parse_errors else []
        }

# 実際の使用例
if __name__ == '__main__':
    parser = RobustJsonParser()
    
    # ケース1:不正なJSON(シングルクォート)
    malformed_json = \"{'name': 'Taro', 'age': 30}\"
    result = parser.parse_with_fallback(
        malformed_json,
        fallback_value={'name': 'Unknown', 'age': 0}
    )
    print(f'結果1: {result}')
    
    # ケース2:末尾に余分なカンマ
    malformed_json2 = '{\"items\": [1, 2, 3,]}'
    result2 = parser.parse_with_fallback(malformed_json2)
    print(f'結果2: {result2}')
    
    # ケース3:部分的に壊れたJSON
    partial_json = '{\"user\": {\"name\": \"Hanako\", \"status\": \"active\"'
    result3 = parser.parse_with_partial_recovery(partial_json)
    print(f'結果3: {result3}')
    
    # エラーレポート
    report = parser.get_error_report()
    print(f'エラー統計: {report}')

よくある応用パターン

パターン1:JSON Schemaによる検証

JSONデータが期待する形式であるか検証することは重要です。

import json
from jsonschema import validate, ValidationError, Draft7Validator

# スキーマ定義の例
USER_SCHEMA = {
    \"type\": \"object\",
    \"properties\": {
        \"id\": {\"type\": \"integer\"},
        \"name\": {\"type\": \"string\"},
        \"email\": {\"type\": \"string\", \"format\": \"email\"},
        \"age\": {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 150},
        \"roles\": {
            \"type\": \"array\",
            \"items\": {\"type\": \"string\"},
            \"minItems\": 1
        }
    },
    \"required\": [\"id\", \"name\", \"email\"],
    \"additionalProperties\": False
}

def validate_user_json(json_string: str) -> tuple[bool, Optional[str]]:
    \"\"\"JSON文字列がスキーマに適合しているか検証\"\"\"
    try:
        data = json.loads(json_string)
    except json.JSONDecodeError as e:
        return False, f'JSON形式エラー: {e}'
    
    try:
        validate(instance=data, schema=USER_SCHEMA)
        return True, None
    except ValidationError as e:
        return False, f'スキーマ検証失敗: {e.message}'

# 使用例
valid_json = '{\"id\": 1, \"name\": \"田中\", \"email\": \"tanaka@example.com\", \"age\": 30, \"roles\": [\"admin\"]}'
is_valid, error_msg = validate_user_json(valid_json)

if is_valid:
    print('検証成功')
else:
    print(f'検証失敗: {error_msg}')

パターン2:ストリーミング処理

大容量のJSONLファイルをメモリ効率的に処理する方法:

import json
from typing import Iterator, Dict, Any

def stream_jsonl_file(filepath: str) -> Iterator[Dict[str, Any]]:
    \"\"\"
    大容量JSONLファイルを行ごとにジェネレータで返す
    メモリ効率的な処理が可能
    \"\"\"
    with open(filepath, 'r', encoding='utf-8') as f:
        for line_number, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            
            try:
                yield json.loads(line)
            except json.JSONDecodeError as e:
                print(f'警告:{line_number}行目パース失敗 - {e}')
                continue

# 使用例
for record in stream_jsonl_file('/var/log/data.jsonl'):
    # 1行ずつ処理。全体をメモリに読まない
    if record.get('status') == 'error':
        print(f'エラー検出: {record}')

json.loads()使用時の注意点

注意1:文字エンコーディング

JSONファイルを読む際は、正しいエンコーディングを指定することが重要です。

import json

# ファイルから読み込むときはencoding引数で明示的に指定
with open('data.json', 'r', encoding='utf-8') as f:
    data = json.loads(f.read())

# requestsライブラリを使う場合は自動判定されるが確認推奨
import requests
response = requests.get('https://api.example.com/data')
# response.textはencoding属性で判定されたテキスト
data = json.loads(response.text)

注意2:float精度の問題

JSONの数値はFloatで解析されるため、精度が失われることがあります。金銭データはDecimalを使うべきです。

import json
from decimal import Decimal

# 通常:floatで解析される
json_string = '{\"price\": 19.99}'
data = json.loads(json_string)
print(type(data['price']))  # <class 'float'>

# 金銭情報の場合:parse_floatオプションを使用
data = json.loads(json_string, parse_float=Decimal)
print(type(data['price']))  # <class 'decimal.Decimal'>

注意3:セキュリティ:JSONインジェクション

ユーザー入力をJSONに含める場合、必ずエスケープが必要です。

import json

# 危険:ユーザー入力を直接含める
user_input = '\" \"} \"x\": \"y'
# これは自分でJSONを作るとインジェクション可能

# 安全:json.dumps()で適切にエスケープ
safe_json = json.dumps({
    'user_input': user_input,
    'timestamp': '2024-01-15'
})
print(safe_json)

# パースするときは安全
data = json.loads(safe_json)
print(data['user_input'])

注意4:メモリ使用量

非常に大きなJSONファイルを一度に読み込むと、メモリ不足になる可能性があります。

import json
from ijson import items

# 非常に大きなJSONファイルの場合はijsonライブラリを使用
# $ pip install ijson

# 全体をメモリに読まずストリーミング処理
with open('huge_file.json', 'r') as f:
    for record in items(f, 'item'):
        # recordは1つずつ取得される
        process(record)

注意5:重複キーの処理

JSONに重複キーがある場合、json.loadsは後ろの値で上書きします。

import json

# 同じキーが複数存在する場合
json_string = '{\"name\": \"Taro\", \"name\": \"Hanako\"}'
data = json.loads(json_string)
print(data['name']) # 'Hanako' (後ろの値)

# 重複キーを検出したい場合
def detect_duplicate_keys(json_string: str) -> List[str]:
\"\"\"重複したキーをすべて検出\"\"\"
import re

# キーを抽出する簡易的な方法
keys = re.findall(r'\"([^\"]+)\

タイトルとURLをコピーしました