プログラミング / 言語 / Python

Excel を JSON に変換する
— openpyxl / pandas × json モジュール

1. json.dumps の基本

Python オブジェクトを JSON 文字列に変換するには json.dumps() を使います。主要なオプションを押さえておきましょう。

python
import json

data = {
    "name": "田中太郎",
    "age": 30,
    "active": True,
    "score": None,
    "tags": ["Python", "Excel"]
}

# ── 基本変換 ─────────────────────────────────────────────
s = json.dumps(data)
print(s)
# {"name": "田中太郎", "age": 30, ...}

# ── 日本語を ASCII エスケープしない ───────────────────────
s = json.dumps(data, ensure_ascii=False)
print(s)
# {"name": "田中太郎", "age": 30, "active": true, "score": null, ...}

# ── 整形して出力(インデントあり) ───────────────────────
s = json.dumps(data, ensure_ascii=False, indent=2)
print(s)
# {
#   "name": "田中太郎",
#   "age": 30,
#   ...
# }

# ── キーをソートして出力 ─────────────────────────────────
s = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)

# ── 区切り文字のカスタマイズ(compact 出力) ─────────────
s = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
# {"name":"田中太郎","age":30,...}
オプションデフォルト説明
ensure_asciiTrueFalse で日本語をそのまま出力
indentNone整数でインデントスペース数を指定
sort_keysFalseTrue でキーをアルファベット順にソート
separators(", ", ": ")(",", ":") でスペースなしのコンパクト出力
defaultNone非対応型の変換関数を指定

2. Excel → dict(openpyxl)

openpyxl でシートを読み取り、ヘッダー行をキーとした dict のリストに変換します。

python
import openpyxl

def sheet_to_dicts(ws, header_row: int = 1) -> list[dict]:
    """
    シートをヘッダー付き dict のリストに変換
    - header_row: ヘッダーが何行目か(デフォルト 1 行目)
    """
    rows = ws.iter_rows(values_only=True)

    # ヘッダー行まで読み飛ばす
    headers = None
    for _ in range(header_row - 1):
        next(rows, None)
    headers = [str(h) if h is not None else f"col_{i}"
               for i, h in enumerate(next(rows, []))]

    # データ行を dict に変換
    records = []
    for row in rows:
        if all(v is None for v in row):
            continue                    # 空行はスキップ
        record = dict(zip(headers, row))
        records.append(record)
    return records


# ── 使用例 ───────────────────────────────────────────────
wb = openpyxl.load_workbook("sales.xlsx", data_only=True)
ws = wb.active

records = sheet_to_dicts(ws)
print(f"読み取り: {len(records)} 件")
print(records[0])
# {'ID': 1, '名前': '田中太郎', '部署': '営業', '売上': 520000, '登録日': datetime(2026,...)}

wb.close()
datetime の扱い: openpyxl は日付セルを datetime オブジェクトとして返します。json.dumps() は datetime を直接シリアライズできないため、後述のカスタムエンコーダが必要です。

3. Excel → JSON 文字列(openpyxl)

openpyxl で読み取った dict リストを json.dumps() で JSON 文字列に変換します。

python
import json
import openpyxl
from datetime import datetime, date

# ── カスタムエンコーダ(datetime / date 対応) ───────────
class ExcelEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.strftime("%Y-%m-%dT%H:%M:%S")   # ISO 8601
        if isinstance(obj, date):
            return obj.strftime("%Y-%m-%d")
        return super().default(obj)

# ── Excel を読み込む ─────────────────────────────────────
wb = openpyxl.load_workbook("sales.xlsx", data_only=True)
ws = wb.active

# ヘッダーを取得
headers = [cell.value for cell in ws[1]]

# データ行を dict のリストに変換
records = []
for row in ws.iter_rows(min_row=2, values_only=True):
    if all(v is None for v in row):
        continue
    records.append(dict(zip(headers, row)))

wb.close()

# ── JSON 文字列に変換 ────────────────────────────────────
json_str = json.dumps(records, ensure_ascii=False, indent=2, cls=ExcelEncoder)
print(json_str[:300])   # 先頭 300 文字を確認
出力例
[ { "ID": 1, "名前": "田中太郎", "部署": "営業", "売上": 520000, "登録日": "2026-01-15" }, { "ID": 2, "名前": "鈴木花子", ... } ]

4. Excel → JSON(pandas)

pandas を使う方法はコードが簡潔です。DataFrame.to_json() が JSON 変換を一手に引き受けます。

python
import pandas as pd
import json

# ── Excel を DataFrame に読み込む ────────────────────────
df = pd.read_excel("sales.xlsx", engine="openpyxl")
print(df.head())

# ── to_json の主要な orient オプション ───────────────────
# orient='records' — [{列:値, ...}, ...] 形式(最もよく使う)
json_str = df.to_json(orient="records", force_ascii=False, indent=2)
print(json_str[:200])

# orient='index'   — {行インデックス: {列:値, ...}, ...}
# orient='columns' — {列名: {行インデックス: 値, ...}, ...}
# orient='values'  — [[値, 値, ...], ...] 配列形式
# orient='split'   — {columns, index, data} を分離した形式
# orient='table'   — JSON Table Schema に準拠

# ── 日付の扱い ───────────────────────────────────────────
# date_format='iso' で ISO 8601 文字列に変換
json_str = df.to_json(
    orient="records",
    force_ascii=False,
    indent=2,
    date_format="iso",           # "iso" または "epoch"(Unix 時間)
    default_handler=str          # 未対応型は str に変換
)

# ── None / NaN の扱い ────────────────────────────────────
# pandas の NaN は JSON では null になる
# NaN を "N/A" に置換してから変換する場合
df_clean = df.fillna("N/A")
json_str = df_clean.to_json(orient="records", force_ascii=False, indent=2)

# ── JSON を dict/list として取得 ─────────────────────────
records = json.loads(json_str)
print(type(records))   # 
print(len(records))    # 行数

5. カスタムエンコーダ(datetime 対応)

json.JSONEncoder を継承してカスタムエンコーダを作ると、datetime・Decimal・独自クラスなど標準では変換できない型に対応できます。

python
import json
from datetime import datetime, date, time
from decimal import Decimal
import openpyxl

# ── 汎用カスタムエンコーダ ───────────────────────────────
class UniversalEncoder(json.JSONEncoder):
    """openpyxl / Excel で遭遇する型をすべてカバーするエンコーダ"""

    def default(self, obj):
        # datetime(日時)→ ISO 8601 文字列
        if isinstance(obj, datetime):
            return obj.isoformat()                  # "2026-05-26T14:30:00"

        # date(日付のみ)→ YYYY-MM-DD
        if isinstance(obj, date):
            return obj.strftime("%Y-%m-%d")         # "2026-05-26"

        # time(時刻のみ)→ HH:MM:SS
        if isinstance(obj, time):
            return obj.strftime("%H:%M:%S")         # "14:30:00"

        # Decimal → float(精度が必要なら str にする)
        if isinstance(obj, Decimal):
            return float(obj)

        # bytes → base64 文字列
        if isinstance(obj, (bytes, bytearray)):
            import base64
            return base64.b64encode(obj).decode("utf-8")

        # その他は str に変換(フォールバック)
        return str(obj)


# ── 使用例 ───────────────────────────────────────────────
wb = openpyxl.load_workbook("sample.xlsx", data_only=True)
ws = wb.active
headers = [c.value for c in ws[1]]
records = [dict(zip(headers, row)) for row in ws.iter_rows(min_row=2, values_only=True)
           if any(v is not None for v in row)]
wb.close()

# cls=UniversalEncoder を渡すだけで OK
json_str = json.dumps(records, ensure_ascii=False, indent=2, cls=UniversalEncoder)
print(json_str)

# ── default 関数として渡す方法(クラス不要) ─────────────
def excel_default(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError(f"型 {type(obj)} はシリアライズできません")

json_str2 = json.dumps(records, ensure_ascii=False, indent=2, default=excel_default)

6. ネスト JSON を生成する

フラットな Excel データから、階層構造を持つネスト JSON を生成するパターンです。グループ集約・親子関係の構築に役立ちます。

python
import json
import openpyxl
from collections import defaultdict

# ── Excel: 部署・社員の売上データ ────────────────────────
# | 部署コード | 部署名 | 社員ID | 社員名 | 売上   |
# |-----------|--------|--------|--------|--------|
# | S01       | 営業   | 1      | 田中   | 520000 |
# | S01       | 営業   | 2      | 山田   | 390000 |
# | D01       | 開発   | 3      | 鈴木   | 480000 |

wb = openpyxl.load_workbook("dept_sales.xlsx", data_only=True)
ws = wb.active
headers = [c.value for c in ws[1]]
rows = [dict(zip(headers, r)) for r in ws.iter_rows(min_row=2, values_only=True)
        if any(v is not None for v in r)]
wb.close()

# ── 部署コードでグループ化してネスト JSON を生成 ──────────
dept_map = defaultdict(lambda: {"部署名": "", "社員数": 0, "合計売上": 0, "社員": []})

for row in rows:
    code = row["部署コード"]
    dept_map[code]["部署名"]  = row["部署名"]
    dept_map[code]["合計売上"] += row["売上"] or 0
    dept_map[code]["社員数"]  += 1
    dept_map[code]["社員"].append({
        "社員ID": row["社員ID"],
        "社員名": row["社員名"],
        "売上":   row["売上"]
    })

# dict → list に変換してトップレベルに部署コードを追加
result = []
for code, dept in dept_map.items():
    result.append({"部署コード": code, **dept})

json_str = json.dumps(result, ensure_ascii=False, indent=2)
print(json_str)
出力例
[ { "部署コード": "S01", "部署名": "営業", "社員数": 2, "合計売上": 910000, "社員": [ {"社員ID": 1, "社員名": "田中", "売上": 520000}, {"社員ID": 2, "社員名": "山田", "売上": 390000} ] }, { "部署コード": "D01", "部署名": "開発", "社員数": 1, "合計売上": 480000, "社員": [ {"社員ID": 3, "社員名": "鈴木", "売上": 480000} ] } ]

7. JSON スキーマ設計のパターン

Excel → JSON の変換時によく使われる出力形式を比較します。用途に合わせて選びましょう。

形式構造pandas orient向いている用途
records [{k:v}, ...] "records" API レスポンス・汎用データ交換(最もよく使う)
フラット配列 [[v1,v2,...], ...] "values" 軽量な転送・列順が確定している場合
ネスト(グループ化) [{key, children:[...]}] 手動で生成 親子関係・階層データ・集計レポート
キー付きオブジェクト {id: {k:v}, ...} "index" ID で素早くルックアップしたい場合
JSON Lines 1 行 1 JSON オブジェクト lines=True 大容量ログ・ストリーム処理・BigQuery 取込
python
import pandas as pd

df = pd.read_excel("sales.xlsx")

# records — [{列:値}, ...]
df.to_json("out_records.json",  orient="records", force_ascii=False, indent=2)

# index — {0: {列:値}, 1: {...}, ...}
df.to_json("out_index.json",    orient="index",   force_ascii=False, indent=2)

# values — [[値, 値, ...], ...]
df.to_json("out_values.json",   orient="values")

# JSON Lines — 1 行 1 JSON(大容量向け)
df.to_json("out_lines.jsonl",   orient="records", lines=True, force_ascii=False)

# 特定の ID 列をキーにした辞書形式(手動)
import json
records = json.loads(df.to_json(orient="records", force_ascii=False))
keyed = {r["ID"]: r for r in records}
with open("out_keyed.json", "w", encoding="utf-8") as f:
    json.dump(keyed, f, ensure_ascii=False, indent=2)

8. JSON ファイルへの保存

json.dump() でオブジェクトをファイルに直接書き出します。ファイル出力のベストプラクティスをまとめます。

python
import json
from pathlib import Path
from datetime import datetime

# ── 基本的なファイル保存 ─────────────────────────────────
data = [{"id": 1, "name": "田中", "created": datetime(2026, 5, 26)}]

# cls=UniversalEncoder を使って datetime を変換
class UniversalEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

output_path = Path("output.json")
with output_path.open("w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2, cls=UniversalEncoder)

print(f"保存完了: {output_path}")

# ── タイムスタンプ付きファイル名 ─────────────────────────
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
timestamped_path = Path(f"output_{ts}.json")

# ── 出力ディレクトリを自動作成 ───────────────────────────
out_dir = Path("json_output")
out_dir.mkdir(parents=True, exist_ok=True)
with (out_dir / "result.json").open("w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2, cls=UniversalEncoder)

# ── 既存ファイルに追記(JSON Lines 形式) ───────────────
new_record = {"id": 99, "name": "新規データ", "created": datetime.now()}
with open("output.jsonl", "a", encoding="utf-8") as f:
    f.write(json.dumps(new_record, ensure_ascii=False, cls=UniversalEncoder) + "\n")

# ── 大容量データを JSON Lines で保存(メモリ節約) ──────
def save_as_jsonlines(records, filepath: str, encoder=None):
    """レコードを 1 行 1 JSON の jsonlines 形式で保存"""
    with open(filepath, "w", encoding="utf-8") as f:
        for record in records:
            line = json.dumps(record, ensure_ascii=False, cls=encoder)
            f.write(line + "\n")
    print(f"{len(records)} 件を {filepath} に保存しました")
JSON Lines(.jsonl)形式: 1 行に 1 つの JSON オブジェクトを書く形式で、大容量データのストリーム処理や BigQuery・Elasticsearch へのデータ取込に適しています。pandas の to_json(lines=True) でも出力できます。

9. 実践スクリプト

Excel の複数シートを読み込み、フラット JSON・ネスト JSON・JSON Lines の 3 形式でまとめて出力するスクリプトです。

python
"""
Excel → JSON 実践スクリプト
- 複数シートを読み込んでフラット / ネスト / JSON Lines の 3 形式で出力
"""

import json
import openpyxl
import pandas as pd
from pathlib import Path
from datetime import datetime, date
from collections import defaultdict


# ── カスタムエンコーダ ────────────────────────────────────
class ExcelEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        return str(obj)


def excel_to_records(filepath: str, sheet_name: str = None) -> list[dict]:
    """Excel シートを dict のリストに変換(openpyxl)"""
    wb = openpyxl.load_workbook(filepath, data_only=True)
    ws = wb[sheet_name] if sheet_name else wb.active
    headers = [str(c.value or f"col_{i}") for i, c in enumerate(ws[1])]
    records = []
    for row in ws.iter_rows(min_row=2, values_only=True):
        if all(v is None for v in row):
            continue
        records.append(dict(zip(headers, row)))
    wb.close()
    return records


def group_by_key(records: list[dict], group_key: str, child_keys: list[str]) -> list[dict]:
    """指定キーでグループ化してネスト JSON を生成"""
    groups: dict = defaultdict(lambda: {group_key: None, "items": []})
    for rec in records:
        key_val = rec.get(group_key)
        groups[key_val][group_key] = key_val
        child = {k: rec[k] for k in child_keys if k in rec}
        groups[key_val]["items"].append(child)
    return list(groups.values())


def save_json(data, path: str, indent: int = 2):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=indent, cls=ExcelEncoder)
    print(f"  保存: {path} ({len(data)} 件)")


def save_jsonlines(records: list[dict], path: str):
    with open(path, "w", encoding="utf-8") as f:
        for rec in records:
            f.write(json.dumps(rec, ensure_ascii=False, cls=ExcelEncoder) + "\n")
    print(f"  保存: {path} ({len(records)} 件, JSON Lines)")


def main():
    input_file = "sales_report.xlsx"
    out_dir = Path("json_export")
    out_dir.mkdir(exist_ok=True)

    # 全シートを処理
    wb = openpyxl.load_workbook(input_file)
    print(f"シート一覧: {wb.sheetnames}")
    wb.close()

    all_records = []
    for sheet in ["1月", "2月", "3月"]:
        try:
            records = excel_to_records(input_file, sheet)
            for rec in records:
                rec["シート"] = sheet
            all_records.extend(records)
            print(f"  {sheet}: {len(records)} 件")
        except KeyError:
            print(f"  {sheet}: シートが見つかりません(スキップ)")

    print(f"\n合計 {len(all_records)} 件を読み込みました")

    # ① フラット JSON(records 形式)
    save_json(all_records, out_dir / "flat.json")

    # ② ネスト JSON(部署でグループ化)
    if all_records and "部署" in all_records[0]:
        non_group_keys = [k for k in all_records[0] if k != "部署"]
        nested = group_by_key(all_records, "部署", non_group_keys)
        save_json(nested, out_dir / "nested_by_dept.json")

    # ③ JSON Lines(大容量向け)
    save_jsonlines(all_records, out_dir / "all_records.jsonl")

    # ④ pandas でも出力(orient='records' + 日付 ISO 形式)
    df = pd.concat([
        pd.read_excel(input_file, sheet_name=s, engine="openpyxl")
        for s in ["1月", "2月", "3月"]
        if s in openpyxl.load_workbook(input_file).sheetnames
    ], ignore_index=True)
    df.to_json(out_dir / "pandas_output.json",
               orient="records", force_ascii=False,
               indent=2, date_format="iso")
    print(f"  保存: {out_dir}/pandas_output.json ({len(df)} 件, pandas)")


if __name__ == "__main__":
    main()
実行例
シート一覧: ['1月', '2月', '3月', '集計'] 1月: 31 件 2月: 28 件 3月: 31 件 合計 90 件を読み込みました 保存: json_export/flat.json (90 件) 保存: json_export/nested_by_dept.json (3 件) 保存: json_export/all_records.jsonl (90 件, JSON Lines) 保存: json_export/pandas_output.json (90 件, pandas)