Python 處理複雜關聯的 JSON 資料
2024-08-04
筆記如何使用 Python 處理複雜類型的 JSON 資料與作業流程。
說明
在資料處理的時候,有時候會有多個 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
。
在工作責任上包含:
- 處理參數的檢查
- 查詢不同來源的資料
- 回傳關聯組合後的資料
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)