# Pythonで JSON を扱う：ファイルの読み書き、CSV、pathlib、UTF-8

URL: https://skillaria.top/ja/guides/python/json-python

Pythonでは、標準ライブラリの json モジュールだけでJSONを扱えます。json.load はファイルから読み込み、json.dump はファイルに書き込みます。json.loads と json.dumps は文字列を扱います。ファイルは with open(path, encoding="utf-8") で開き、パスは pathlib.Path で組み立て、表形式のデータは csv モジュールで読みます。

Pythonでのデータ形式JSONの扱いには、外部ライブラリは要りません。`json` モジュールは標準ライブラリに含まれていて、辞書やリストを文字列へ、また文字列から辞書やリストへ変換できます。JSONと組み合わせて、ほぼ必ず必要になるのが、`with` 文を使う `open` 関数、パスを扱う `pathlib` モジュール、表データを扱う `csv` モジュールの3つです。ここでは、動くコードと、つまずきやすいポイントを順に見ていきます。

## Pythonのopen関数：モードと文字コード

テキストファイルは、組み込みの `open(path, mode, encoding=...)` で開きます。戻り値のファイルオブジェクトには `read`、`readline`、`write` メソッドがあり、for文で1行ずつ回せます。

| モード | 動作 |
| --- | --- |

| `"r"` | 読み込み（既定値）。ファイルがなければ `FileNotFoundError` |

| `"w"` | 書き込み。ファイルを作成するか、**開いた時点で中身を空にする** |

| `"a"` | 末尾に追記。既存の内容は残す |

| `"x"` | 新規作成。すでにあれば `FileExistsError` |

| `"b"` | バイナリとして扱う（`"rb"`、`"wb"`） |

### なぜwithを使うのか

ファイルは必ず閉じる必要があります。閉じないとデータがディスクに書かれないことがあり、また、システム側でファイルのハンドルを握ったままになります。`with` 文を使えば、ブロックの中でエラーが起きても、自動的にファイルが閉じられます。

```
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("1行目\n")
    f.write("2行目\n")

with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))
```

for文でファイルを回すと、1行ずつ読み込まれるため、ファイル全体がメモリに載ることはありません。ギガバイト単位のログでも扱えます。`f.read()` は全体を1つの文字列で返し、`f.readlines()` は行のリストを返します。

### 文字列をファイルに書いても、既存の内容を消さないために

初心者が最もよくやるミスは、1行追加したいだけなのに `"w"` モードで開くことです。`"w"` は、最初の `write` より前、開いた時点で内容を消します。末尾に追加するには `"a"` を使います。すでにあるファイルを誤って上書きしたくないなら、`"x"` が安全です。

```
with open("log.txt", "a", encoding="utf-8") as f:
    f.write("新しい記録\n")
```

`write` は改行を自動で入れないため、`"\n"` を明示的に書く必要があります。`print(..., file=f)` を使えば、改行を自動で付けてくれます。

## UTF-8の文字化けはなぜ起きるのか

Python 3.15 より前では、`open()` に `encoding` を指定しなければ、システムの既定の文字コードが使われました。LinuxとmacOSではUTF-8ですが、日本語のWindowsでは cp932（Shift_JIS系）です。Linuxサーバーで問題なく動いていたスクリプトが、Windowsのパソコンでは文字化けしたり、`UnicodeDecodeError` で止まったりしていました。

Python 3.15 では、PEP 686 によってUTF-8モードがデフォルトになりました。`encoding` を指定しない `open()` は、どのOSでもUTF-8で読み書きします。古い動作に戻すには、環境変数 `PYTHONUTF8=0` を設定するか、`-X utf8=0` を指定します。システムのエンコーディングを明示的に使うなら `encoding="locale"` と書きます。

```
import sys
print(sys.flags.utf8_mode)   # Python 3.15 では 1

open("legacy.txt", encoding="locale")   # システムの既定の文字コード
open("old_excel.csv", encoding="cp932")  # cp932 で保存されたファイル
```

注意点もあります。Excelやスクリプトで cp932 として保存された古いCSVは、UTF-8として読まれ、`UnicodeDecodeError` になります。その場合は、`encoding="cp932"` で読んでからUTF-8で保存し直します。どの版のPythonでも同じように動かすには、`encoding` を明示する習慣をつけてください。

## pathlib：パスをオブジェクトとして扱う

パスを文字列で連結する（`"data" + "/" + name`）のは扱いにくく、OSによって壊れることもあります。`pathlib` モジュールの `Path` クラスでは、`/` 演算子でパスを結合でき、便利なプロパティも使えます。

```
from pathlib import Path

data = Path("data")
csv_path = data / "expenses.csv"

print(csv_path.name)    # expenses.csv
print(csv_path.stem)    # expenses
print(csv_path.suffix)  # .csv
print(csv_path.parent)  # data
print(csv_path.exists())

data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("こんにちは", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))

for p in data.glob("*.csv"):
    print(p)
```

`read_text` と `write_text` は、小さなファイルなら `open` と `with` の組み合わせの代わりになります。`Path` オブジェクトは、通常の `open` にも、`json` や `csv` にも渡せます。スクリプトの置かれた場所を基準にしたパスは、`Path(__file__).parent / "data"` で作れます。

Python 3.14では、`Path` に `copy()` と `move()` メソッドが加わりました。単純なコピーであれば、もう `shutil` は必要ありません。同じバージョンでは、Zstandard圧縮のための標準モジュール `compression.zstd` も追加されました（PEP 784）。

## Pythonのjson：load、dump、loads、dumps

JSONはテキストのデータ交換形式です。APIの応答や、設定ファイル、レポートの保存に使われます。`json` モジュールは、2つの操作を提供します。シリアライズ（Pythonオブジェクトからテキストへ）と、デシリアライズ（テキストからオブジェクトへ）です。関数は4つあり、混同しやすいので表にまとめます。

| 関数 | 動作 |
| --- | --- |

| `json.dumps(obj)` | オブジェクトをJSON文字列にする |

| `json.loads(s)` | JSON文字列をオブジェクトにする |

| `json.dump(obj, f)` | オブジェクトを開いたファイルに書く |

| `json.load(f)` | 開いたファイルからJSONを読む |

覚え方は簡単です。末尾の `s` は string（文字列）の意味で、文字列を扱うことを表します。`s` がなければ、ファイルを扱います。

### 型の対応

| Python | JSON |
| --- | --- |

| `dict` | オブジェクト `{}` |

| `list`、`tuple` | 配列 `[]` |

| `str` | 文字列 |

| `int`、`float` | 数値 |

| `True` / `False` | `true` / `false` |

| `None` | `null` |

タプルは読み戻すとリストになります。また、集合、日付、`Decimal` はjsonモジュールが直接扱えず、`TypeError: Object of type ... is not JSON serializable` になります。

### JSONをファイルに保存する

```
import json
from pathlib import Path

report = {
    "month": "2026-10",
    "total": 15230.5,
    "categories": {"食費": 8200, "交通費": 2100},
    "tags": ["家", "仕事"],
}

path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)
```

`ensure_ascii=False` を指定すると、日本語がそのまま保存されます。指定しないと、「食費」は `"食費"` のような形で書かれます。これは正しいJSONですが、人間には読みにくくなります。`indent=2` は、2つのスペースによる字下げで整形します。

### JSONをファイルから読み込む

```
with open("data/report.json", encoding="utf-8") as f:
    data = json.load(f)

print(data["total"])               # 15230.5
print(data["categories"]["食費"])   # 8200
```

結果は普通の辞書やリストなので、いつもどおり扱えます。ファイルがJSONとして正しくないか、途中で切れている場合は、`json.JSONDecodeError` が、行番号と位置の情報とともに発生します。外部から届くデータを扱うときは、このエラーを捕まえるようにしてください。

### JSON文字列の解析と見やすい出力

```
import json

raw = '{"name": "花子", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0])   # python

print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
```

`sort_keys=True` は、出力時にキーを並べ替えます。gitでファイルの差分を比べるときに便利です。ターミナルからJSONファイルを整形するには、標準の方法があります。`python -m json.tool data/report.json` です。

HTTP APIの応答も、JSON文字列です。`requests` ライブラリの `response.json()` は、内部で `json.loads` を使っています。

### 注意：JSONのキーは常に文字列

```
d = {1: "1月", 2: "2月"}
back = json.loads(json.dumps(d))
print(back == d)   # False
print(back)        # {'1': '1月', '2': '2月'}
# back[1] -> KeyError。back["1"] と書く必要がある

fixed = {int(k): v for k, v in back.items()}
```

JSONのオブジェクトのキーは文字列だけです。そのため、数値のキーは変換の往復で文字列になります。読み込んだ後で、元の型に戻してください。

### 独自の型：日付とDecimal

```
import json
from datetime import date

def default(obj):
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"{type(obj).__name__} はシリアライズできません")

print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
```

`default` 引数に渡した関数は、扱えないオブジェクトごとに呼ばれます。より複雑な場合は、`json.JSONEncoder` を継承して `default` メソッドを上書きできます。

### JSON配列に要素を追加する

JSONは、テキストのログのように末尾へ追記できません。ファイルを読み込み、変更して、全体を書き直す必要があります。

```
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "新規"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
```

レコードが多く、継続的に追加される場合は、JSON Lines という形式が向いています。1行に1つのJSONオブジェクトを書き、ファイルを `"a"` モードで開きます。

## Pythonのcsvモジュール

CSVは、テキストファイルの中の表です。行がレコードで、値はカンマで区切られます。`csv` モジュールは、引用符、値の中のカンマ、改行を正しく扱います。手作業の `split(",")` では、これができません。

```
import csv

with open("data/expenses.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    total = 0.0
    for row in reader:
        total += float(row["amount"])
        print(row["date"], row["category"], row["amount"])
print("合計:", total)
```

`csv.DictReader` は、ファイルの見出し行をキーにして、各行を辞書に変換します。値はすべて文字列です。数値や日付は自分で変換してください（`float`、`int`、`date.fromisoformat`）。区切り文字がセミコロン「;」のファイル（日本語版Excelが保存する形式のもの）は、`delimiter=";"` を指定して読みます。

```
rows = [
    {"date": "2026-10-01", "category": "食費", "amount": 450},
    {"date": "2026-10-02", "category": "交通費", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
    writer.writeheader()
    writer.writerows(rows)
```

書き込みのときは `newline=""` が必要です。これがないと、Windowsでは行の間に空行が入ります。ファイルをExcelで開く予定があるなら、`encoding="utf-8-sig"` を使ってください。先頭のBOMにより、ExcelがUTF-8として認識しやすくなります。

## CSVからJSONへ：完全な例

```
import csv
import json
from collections import defaultdict
from pathlib import Path

src = Path("data/expenses.csv")
dst = Path("data/report.json")

by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        by_category[row["category"]] += float(row["amount"])

report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
```

これは、レポートを作るスクリプトの基本形です。CSVを入力として読み、辞書で集計し、JSONとして出力します。

## よくあるミス

- `FileNotFoundError` ― 相対パスは、スクリプトの場所ではなく、起動したフォルダを基準に解釈されます。`Path(__file__).parent` を使いましょう。
- ファイルの中身が消えた ― `"a"` ではなく `"w"` で開いてしまった。
- `UnicodeDecodeError` ― 別の文字コードのファイルです。`cp932` や `utf-8-sig` を明示してください。
- `TypeError: Object of type set is not JSON serializable` ― リストに変換するか、`default` 引数を渡します。
- JSONを読み込んだ後の `KeyError` ― 数値のキーが文字列になっています。
- 自作のファイルを `json.py` や `csv.py` と名付けた ― 標準モジュールより優先されてしまい、`import json` で自分のファイルが読み込まれます。

## まとめ

JSONをPythonで扱うのに、標準ライブラリで十分です。ファイルには `json.load` と `json.dump`、文字列には `loads` と `dumps` を使います。読みやすい出力には `ensure_ascii=False` と `indent` を指定します。ファイルは `with` で、文字コードを明示して開き、パスは `pathlib` で組み立て、表は `csv.DictReader` で読みます。講座では、このテーマを実践で扱います。家計簿ツールがCSVから支出を読み込み、レポートをJSONに保存します。

## FAQ

**json.load と json.loads の違いは何ですか？**

json.load は開いたファイルオブジェクトからJSONを読み込み、json.loads は文字列を解析します。同様に json.dump はファイルへ書き込み、json.dumps は文字列を返します。

**日本語をJSONに \u で始まるエスケープなしで保存するには？**

json.dump または json.dumps に ensure_ascii=False を渡し、ファイルを encoding="utf-8" で開きます。

**JSONを扱うために、ライブラリをインストールする必要はありますか？**

いいえ。json モジュールは標準ライブラリに含まれているので、import json と書くだけで使えます。

**JSONに日付や集合を保存できますか？**

直接はできません。日付は isoformat で文字列にし、集合はリストに変換するか、default 引数に関数を渡して処理します。

**メモリに入らないほど大きなJSONはどう読めばよいですか？**

標準の json.load はファイル全体を読みます。大量のデータは JSON Lines 形式で保存し、1行ずつ json.loads で解析するのがよい方法です。


---
Source: Skillaria.Top (https://skillaria.top)
