【Python】XMLファイル→JSON変換入門

PythonでXMLファイルをJSON形式に変換する例を紹介します。
今回はJSON形式にしたデータを標準出力することにします。
そのため、正しく整形できているかはsample.jsonなどファイルを作成して貼り付けてご確認いください。
JSONで別ファイルに出力したい場合は、json.dump()を使用する例をお調べいただければと思います。

# PythonでXMLファイル→JSON形式への変換をしてよう！
### 処理対象のサンプルファイル
```xml:sample.xml
    <company>
        <region name="North America">
            <department name="Engineering">
                <employee id="E001">
                    <name>John Doe</name>
                    <age>45</age>
                    <position>Senior Engineer</position>
                    <projects>
                        <project code="P001" role="Lead Developer">AI Platform</project>
                        <project code="P002" role="Reviewer">Mobile App</project>
                    </projects>
                </employee>
                <employee id="E002">
                    <name>Jane Smith</name>
                    <age>41</age>
                    <position>Software Engineer</position>
                    <projects>
                        <project code="P003" role="Contributor">Data Pipeline</project>
                    </projects>
                </employee>
            </department>
            <department name="Marketing">
                <employee id="E003">
                    <name>Emily Davis</name>
                    <age>29</age>
                    <position>Marketing Specialist</position>
                    <projects />
                </employee>
            </department>
        </region>
        <region name="Europe">
            <department name="Research">
                <employee id="E004">
                    <name>Albert Einstein</name>
                    <age>50</age>
                    <position>Research Scientist</position>
                    <projects>
                        <project code="P004" role="Lead">Quantum Computing</project>
                    </projects>
                </employee>
            </department>
        </region>
    </company>

```

### Pythonサンプルコード
```python:sample.py
# 課題1.データの解析：XMLファイルを解析し、以下の構造でデータを整備する
#   地域名、部署名、社員ID、名前、年齢、役職、プロジェクト情報(プロジェクトコード、名前、役割)
# 課題2.特定条件でのフィルタリング：年齢が40歳以上、プロジェクトに1件以上関与している
# 課題3.集計：地域ごとの社員数、部署ごとのプロジェクト総数、役職ごとの社員数

import xml.etree.ElementTree as ET


# ファイル読み込み処理をする関数
def read_file(xml_file_path):
    with open(xml_file_path, 'r', encoding='utf-8') as f:
        data = f.read()
        return data

# XMLデータを解析する関数
def parse_xml(root):
    result = []
    for region in root.findall('region'):
        region_name = region.get('name')
        company_dict = {
            region_name: []
        }
        for department in region.findall('department'):
            for employee in department.findall('employee'):
                employee_dict = {
                    'id': employee.get('id'),
                    'name': employee.find('name').text,
                    'age': int(employee.find('age').text),
                    'position': employee.find('position').text,
                    'department': department.get('name'),
                    'projects': []
                }
                projects = employee.find('projects')
                if projects:
                    for project in projects.findall('project'):
                        projects_dict = {
                            'code': project.get('code'),
                            'role': project.get('role')
                        }
                        employee_dict['projects'].append(projects_dict)
                company_dict[region_name].append(employee_dict)
        result.append(company_dict)
    return result


# 課題2の関数
# keys()について：https://zenn.dev/yuto_mo/articles/ab719028804192
# keys()について２：https://stackoverflow.com/questions/26394748/nltk-python-error-typeerror-dict-keys-object-is-not-subscriptable
# 要約：keys(),values(),items()の戻り値は、インデックスでアクセス不可能なリスト
# フィルタリング：40歳以上、かつ、プロジェクトに1つ以上関わっている
def filter_by_age_and_project(parsed_xml_list, min_age, min_project_num):
    result = []
    for region in parsed_xml_list:
        key = list(region.keys())[0]
        filtered_dict = {
            key: []
        }
        for employees in region.values():
            for employee in employees:
                if employee['age'] >= min_age and len(employee['projects']) >= min_project_num:
                    filtered_dict[key].append(employee)
        result.append(filtered_dict)
    return result


# 課題3の関数
# その１：地域ごとの社員数を数える関数
def num_of_employees_by_region(data):
    result = {}
    for d in data:
        d_key = list(d.keys())[0]
        result[d_key] = len(d[d_key])
    return result

# その２：部署ごとのプロジェクト総数
def total_projects_by_department(data):
    result = {}
    for d in data:
        d_key = list(d.keys())[0]
        for e in d[d_key]:
            department_key = list(e.keys())[4]
            if e[department_key] not in result:
                result[e[department_key]] = 0
            result[e[department_key]] += len(e['projects'])
    return result

# その３：役職ごとの社員数
def num_of_employees_by_job(data):
    result = {}
    for d in data:
        d_key = list(d.keys())[0]
        for e in d[d_key]:
            position_key = list(e.keys())[3]
            if e[position_key] not in result:
                result[e[position_key]] = 0
            result[e[position_key]] += 1 if e['position'] == e[position_key] else 0
    return result

xml_file_path = 'sample.xml'
xml_data = read_file(xml_file_path)
root = ET.fromstring(xml_data)
parsed_data = parse_xml(root)

# 課題1
print(parsed_data)

# 課題2：フィルタリング
filtered_data = filter_by_age_and_project(parsed_data, 40, 1)
print(filtered_data)

# 課題3：地域ごとの社員数、部署ごとのプロジェクト総数、役職ごとの社員数の集計処理
# 地域ごとの社員数
nums_of_employee_by_region = num_of_employees_by_region(parsed_data)
print(nums_of_employee_by_region)

# 部署ごとのプロジェクト総数
total = total_projects_by_department(parsed_data)
print(total)

# 役職ごとの社員数
num_of_job = num_of_employees_by_job(parsed_data)
print(num_of_job)

```

## 課題：
- 課題1.データの解析：XMLファイルを解析し、以下の構造でデータを整備する
    - 地域名、部署名、社員ID、名前、年齢、役職、プロジェクト情報(プロジェクトコード、名前、役割)
- 課題2.特定条件でのフィルタリング：年齢が40歳以上、プロジェクトに1件以上関与している
- 課題3.集計：地域ごとの社員数、部署ごとのプロジェクト総数、役職ごとの社員数

## 処理のポイント：
- 課題1について：
    - やることはシンプルなXML解析処理。
    - XMLタグのネストを真似してforループで処理する。
    - JSONの形を意識してデータを作っていくイメージ。
    - 使用するメソッドは主に以下の3つ：
        - find():最初に見つかったタグ（単一のElementオブジェクト）を返す。存在しない場合は、Noneを返す。
        - findall():該当するすべてのタグをリストとして返す。存在しない場合は、空のリストを返す。
        - get():タグの属性に対応する値を取得する。
    - projectタグのcode属性とrole属性については、attribを使用してアンパッキングしても良いが、可読性が下がるためget()を使用する方が良いかと思う。
- 課題2について：
    - 辞書型のデータの扱い方の練習。
    - items(),keys(),values()の戻り値は、イテレート不可能なリストのため、インデックスでアクセスできない。
    - そのため、インデックスでアクセスするためにlist()を使い、普通のリストにすることで対応する。
- 課題3について：
    - 重複なしでカウントする練習。
    - 辞書に値が含まれるかを確認することで重複をなくしてカウントする。

## 最後に
XMLは、階層構造になっていること、タグ、属性、値を意識すると考えやすいと思います。
やることは単純なので、データ構造とメソッドを使って解析→整形してみましょう！