Python 處理複雜關聯的 JSON 資料

2024-08-04

筆記如何使用 Python 處理複雜類型的 JSON 資料與作業流程。

logo

說明

在資料處理的時候,有時候會有多個 JSON 檔案,而這些檔案之間有關聯,筆記常見的資料處理方式。

第零步是先整理好要使用的 JSON 檔案以及 KEY, VALUE 的對照,可以直接儲存在專案的 README.md 檔案中。

KEY 說明
uid 編號
Name 物品名稱
Description 描述
English 英文描述

資料讀取與物件轉換

第一步是讀取 JSON 檔案並且轉換為物件,使用內建的 json 模組即可。

getData.py

import json
from types import SimpleNamespace

file_path = r'D:\datasource\data.json'

with open(file_path, 'r', encoding='utf-8-sig') as json_file:
    item_data = json.load(json_file, object_hook=lambda d: SimpleNamespace(**d)).CharacterPoolData

首先透過 open 搭配指定 encoding 的方式來讀取 JSON 檔案,接著使用 json.load 來將檔案內容轉換成 Python dict。

而這邊的處理上搭配使用 SimpleNamespace 來將 JSON 轉換成 SimpleNamespace 物件,這樣可以使用 . 來存取 JSON 的內容。

優點是非常簡便的處理資料,不需要額外定義類別與 namedtuple,缺點是這樣的方式沒有辦法利用 IDE 的 intellisense 功能,

資料索引

第二步是建立資料的主鍵索引,因為有時候 JSON 檔案室 List 集合,Primary Key 是其中一項 Property,要進行資料查詢的時候並不方便。

[
  {
    "uid": "1001",
    "Name": "Bronze Sword",
    "Description": "一把普通的銅劍",
    "English": "a normal bronze sword",
  },
  ...
]

getData.py

item_dict = {item.uid: item for item in item_data}

item_dict.get('1001', None)

替代方式使用 pandas 套件,透過 DataFrame 處理資料或是透過 SQLAlchemy 模擬成資料庫的方式來處理。

pandas

import pandas as pd

df = pd.DataFrame(item_data)
df.set_index('uid', inplace=True)

df.loc['1001']

SQLAlchemy

import SQLALchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///:memory:', echo=True)
Session = sessionmaker(bind=engine)
session = Session()

class Item(Base):
    __tablename__ = 'item'
    uid = Column(Integer, primary_key=True)
    Name = Column(String)
    Description = Column(String)
    English = Column(String)

Base.metadata.create_all(engine)

# insert data from json
for data in item_dict:
    session.add(Item(**data))

session.commit()

# get data by uid
item = session.query(Item).filter(Item.uid == 1001).first()

資料關聯

第三步是處理資料之間的關聯。

關聯的處理定義在 utilities 當中,函式命名可以使用 get_xxx_by_yyy 的方式來命名,例如 get_weapon_by_uid

在工作責任上包含:

  1. 處理參數的檢查
  2. 查詢不同來源的資料
  3. 回傳關聯組合後的資料

utilities.py

from viewmodel import *

def get_weapon_by_uid(uid):
    # check uid
    if not uid:
        return None

    # get weapon data
    weapon = item_dict.get(uid, None)
    weapon_description = item_description_dict.get(uid, None)

    weapon_abilities = []
    # check diff prefix and uid
    weapon_abilities.add(item_abilities_dict.get('w_' + uid, None))
    weapon_abilities.add(item_abilities_dict.get('weapon_' + uid, None))
    weapon_abilities.add(item_abilities_dict.get('@_' + uid, None))

    weapon_abilities = [ability for ability in weapon_abilities if ability is not None]

    # using view model to combine data
    return WeaponViewModel(weapon, weapon_abilities, weapon_description)

回傳資料的處理,可以透過定義 namedtuple 或是 dataclass 來處理,用以享受 IDE 的 intellisense 功能。

viewmodel.py

from collections import namedtuple

WeaponViewModel = namedtuple('WeaponViewModel', ['weapon', 'abilities', 'description'])

資料輸出

第四步是處理資料的輸出,端看 Python 要處理的範圍可能的形式有多種。

可以是輸出成 sqlite 或者是寫入 SQL Server 當中,再接棒給其他網頁框架來處理資料。

又或者是輸出成 Markdown 檔案,搭配 MKDocs 或者是 docsify 來建立文件。

本次筆記說明輸出成 Markdown 檔案的方式,同時為了優化編輯體驗,使用 jinjia2 模板引擎來處理 teamplate。

template.md

## 物品名稱 - {{ item.name }}

{{ description }}

{% for ability in abilities %}
- {{ ability.Name.replace('@', '') }}: {{ ability.Value }}
{% endfor %}

app.py

from jinja2 import Template
import getData
from utilities import *

template = Template(open('./template.md', 'r', encoding='utf-8').read())

for item in getData.item_data:
    with open(f'./docs/{item.uid}.md', 'w', encoding='utf-8') as file:
        content = template.render(
          item=item,
          description=get_weapon_by_uid(item.uid).description,
          abilities=get_weapon_by_uid(item.uid).abilities)

        file.write(content)