# ch04 — CSV を読み書きする

Source: https://php-school.pages.dev/topics/file-io/ch04-csv/
Site: AIと学ぶPHPの学校 (https://php-school.pages.dev)
Lesson: ファイル入出力
Level: L4

Reference (official): https://www.php.net/manual/ja/book.filesystem.php

## 本文

# ch04 — CSV を読み書きする

## 概要

**CSV (Comma-Separated Values)** は「カンマで区切ったテキスト形式」のシンプルな表形式データ。エクセルや分析ツールなどデータ交換のデファクト。だが「カンマで `explode` すれば OK」と自前実装すると、値にカンマが含まれる (`"山田, 太郎"`) ケースや、ダブルクォートで囲まれた値で必ず破綻する。エスケープ仕様が地味に複雑、というのが落とし穴。

そこで PHP の **`fgetcsv()` / `fputcsv()`** を使う。ダブルクォート囲み・カンマエスケープを **きちんと処理** してくれる正解関数。`fgetcsv($fp)` は 1 行読んで **数値添字の配列** (`[0] => id, [1] => name, ...`) を返す。連想配列ではないので、列の順番を頭で覚えておく必要がある。書き込みは `fputcsv($fp, ['1', '太郎', '20'])` のように配列を渡すだけ。

**PHP 8.4 以降**、`fgetcsv` / `fputcsv` の `$escape` 引数の挙動が変わり、明示的に `''` を指定しないと **deprecation 警告** が出る。歴史的なバックスラッシュ・エスケープが標準 CSV 仕様と違っていた混乱を正す変更で、現代の PHP では `fgetcsv($fp, 0, ',', '"', '')` と **第 5 引数を空文字で明示** するのが推奨形。本コースの解答例もこの形。

## 学習目標

- `fgetcsv()` で CSV を **行 × 列の配列** として読める
- `fputcsv()` で 配列を **CSV の 1 行** として書ける
- CSV のヘッダ行 (`id,name,age`) を切り離して、データ行だけを処理できる
- 書き込みは **一時ファイル** で完結させ、読み返して検算できる

## 所要時間

スライド = 5 分 / ドリル = 約 25 分

## ドリル

| # | 内容 |
|---|---|
| `01-read-csv/` | `tests/data/users.csv` を読んで `"id: name (age歳)"` 形式で出力 |
| `02-write-csv/` | 配列を `fputcsv` で 一時 CSV に書き、読み返して `fgetcsv` で出力 |

## 採点

```bash
php scripts/grade.php topics/file-io/ch04-csv/drill/01-read-csv/
php scripts/grade.php topics/file-io/ch04-csv/drill/02-write-csv/
```

## 確認のコツ

- `fgetcsv` は **数値添字の配列** を返す (連想配列ではない)。`[0] = id, [1] = name, [2] = age` のように **列の順番** で取り出す
- ヘッダ行は **最初の 1 回だけ別扱い** にして、本体ループに混ぜない
- 書き込みは `fputcsv($fp, ['1', '太郎', '20'])` のように **配列を 1 行ずつ** 渡す

## スライド

---
marp: true
theme: dojo
paginate: true
size: 16:9
---

<!--
LLM_CONTEXT:
  Lesson 14 / Chapter 4
  目的: CSV を fgetcsv / fputcsv で読み書きする。ヘッダ行の扱いと配列の添字に慣れる
  扱わない: パス分解 (ch05) / アップロード (ch06)
  読み上げ時間目安: 5 分
-->

# CSV を読み書きする

Lesson 14 / Chapter 4

---

## CSV って結局なに？

```
id,name,age
1,太郎,20
2,花子,25
3,次郎,30
```

- **Comma-Separated Values** = カンマで区切ったテキスト
- 表 (Excel / DB) を **1 行 = 1 レコード / カンマで列を分ける** 形式で並べたもの
- PHP では `fgetcsv` / `fputcsv` で **行 = 配列** として扱う

→ Excel と PHP の橋渡し / 簡易データ交換に頻出。

---

## 読み込み: `fgetcsv` の基本

```php
<?php
$fp = fopen(__DIR__ . '/tests/data/users.csv', 'r');

// 第 2〜5 引数: length=0 (制限なし) / separator="," / enclosure="\"" / escape=""
while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
    // $row は数値添字の配列: [0] = id, [1] = name, [2] = age
    print_r($row);
}

fclose($fp);

// 出力 (1 行目はヘッダ):
// Array ( [0] => id    [1] => name  [2] => age )
// Array ( [0] => 1     [1] => 太郎  [2] => 20  )
// Array ( [0] => 2     [1] => 花子  [2] => 25  )
```

- `fgetcsv` は **1 行を配列で返す** (列を自動分割)
- 値は **常に文字列** (age も `"20"`)。数値が必要なら `(int)` でキャスト
- EOF で `false` を返す
- **PHP 8.4 以降** は 第 5 引数 `$escape` を明示しないと Deprecated 警告が出る。空文字 `''` を渡すのが安全 (RFC 4180 準拠)

---

## ヘッダ行を分離する常套手段

```php
<?php
$fp = fopen(__DIR__ . '/tests/data/users.csv', 'r');

// 1 行目 (ヘッダ) を先に読み捨てる
$header = fgetcsv($fp, 0, ',', '"', '');   // ['id', 'name', 'age']

// 2 行目以降をループで処理
while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
    [$id, $name, $age] = $row;   // 配列の分配代入
    echo "{$id}: {$name} ({$age}歳)\n";
}

fclose($fp);
```

- ヘッダを **ループ前に 1 回だけ読む** = データ行と混ざらない
- `[$id, $name, $age] = $row;` は **配列の分配代入** (PHP 7.1+)。インデックス順に変数に入る

→ 「ヘッダは別扱い、本体はループ」が CSV 処理の定形。

---

## 書き込み: `fputcsv` の基本

```php
<?php
$tmp = tempnam(sys_get_temp_dir(), 'dojo_csv_');
$fp = fopen($tmp, 'w');

// ヘッダ。第 3〜5 引数 (separator/enclosure/escape) を明示 (PHP 8.4+ で警告回避)
fputcsv($fp, ['id', 'name', 'age'], ',', '"', '');
// データ行
fputcsv($fp, [1, '太郎', 20], ',', '"', '');
fputcsv($fp, [2, '花子', 25], ',', '"', '');

fclose($fp);

echo file_get_contents($tmp);
// id,name,age
// 1,太郎,20
// 2,花子,25

unlink($tmp);
```

- `fputcsv($fp, $row)` は **配列を 1 行の CSV** にして書く
- 区切り (`,`) / 囲み (`"`) / エスケープは **自動で処理** (空白や `"` を含む値は `"..."` で囲ってくれる)
- 末尾の改行も自動で付く

---

## CSV の罠 (本コース外だけ把握だけしておく)

| 罠 | 何が起きる |
|---|---|
| Shift_JIS の CSV (Excel 由来) | UTF-8 前提の `fgetcsv` で文字化け |
| BOM (バイトオーダーマーク) 付き UTF-8 | 1 列目の先頭に `\xEF\xBB\xBF` が混ざる |
| 値の中にカンマや改行 | RFC 4180 では `"..."` で囲む。`fgetcsv` / `fputcsv` は自動対応 |
| 値の中に `"` | `""` に二重化して `"..."` で囲む。同上 |
| 巨大ファイル | 全行配列に積むとメモリが死ぬ。`fgetcsv` で **1 行ずつ** 流す |

→ 本 lesson では UTF-8 / 単純なデータのみ扱う。文字化けに遭遇したら `mb_convert_encoding` / `iconv` を検討。

---

## `fputcsv` で書いた後は読み返して検算

```php
<?php
$tmp = tempnam(sys_get_temp_dir(), 'dojo_csv_');

// 書く
$fp = fopen($tmp, 'w');
fputcsv($fp, ['id', 'name'], ',', '"', '');
fputcsv($fp, [1, '太郎'], ',', '"', '');
fclose($fp);

// 読み返す (= 書いた内容の検算)
$fp = fopen($tmp, 'r');
while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
    echo implode(' | ', $row) . "\n";
}
fclose($fp);

unlink($tmp);

// 出力:
// id | name
// 1 | 太郎
```

書き込み系のドリルでは必ず **「書く → 読み返す → 出力する」** を 1 セットでやる。

---

## このチャプターでできるようになること

✅ `fgetcsv` で CSV を **行配列** として読める
✅ ヘッダ行を **分離して** データ行をループ処理できる
✅ `fputcsv` で 配列を **CSV の 1 行** として書ける
✅ 配列の **分配代入** (`[$a, $b] = $row`) で列を変数に取り出せる
✅ 書き込みは **一時ファイル** で完結させ、読み返して検算する流れができる

→ ドリルへ
