CSV を読み書きする
テクノロジ系 / file-io (read / write / CSV / paths / upload概念)
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
$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
$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 $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
$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) で列を変数に取り出せる
✅ 書き込みは 一時ファイル で完結させ、読み返して検算する流れができる
→ ドリルへ
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ドリル 01 — CSV を読んで整形出力
問題
__DIR__ . '/tests/data/users.csv' を読み、各データ行を "id: name (age歳)" の形式で出力してください。
データファイル (tests/data/users.csv):
id,name,age 1,太郎,20 2,花子,25 3,次郎,30
期待される出力:
1: 太郎 (20歳) 2: 花子 (25歳) 3: 次郎 (30歳)
(1 行目のヘッダ id,name,age は出力に含めない)
採点
php scripts/grade.php topics/file-io/ch04-csv/drill/01-read-csv/
ヒント
$fp = fopen(__DIR__ . '/tests/data/users.csv', 'r');
$header = fgetcsv($fp, 0, ',', '"', ''); // 1 行目を読み捨てる
while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
[$id, $name, $age] = $row;
echo "{$id}: {$name} ({$age}歳)\n";
}
fclose($fp);fgetcsvは 1 行を配列で返す (数値添字)。[0]=id, [1]=name, [2]=age- ヘッダ行は ループに入る前に 1 回だけ読み捨てる
- 配列の分配代入
[$a, $b, $c] = $row;で列を変数に取り出せる (PHP 7.1+) - 第 2〜5 引数 (
length,separator,enclosure,escape) を明示 — PHP 8.4 以降 で警告が出るのを防ぐ
つまづいたら
- 出力の 1 行目が
"id: name (age歳)"になる
→ ヘッダ行を 読み捨てていない。ループに入る前に fgetcsv($fp) を 1 回呼ぶ
ageが"20歳"ではなく"20.0歳"になる
→ (float) キャストや number_format を使っている。fgetcsv の値は 文字列のまま で OK
ドリル 02 — 配列を CSV に書いて読み返す
問題
次の配列を 一時 CSV ファイル に書き出し、その CSV を 読み返して 各行を " | " で結合して 1 行ずつ出力してください。最後に一時ファイルを unlink で消します。
書き出す配列:
$rows = [
['id', 'name', 'score'],
[1, '太郎', 80],
[2, '花子', 95],
[3, '次郎', 60],
];期待される出力 (書き出した CSV を読み返した結果):
id | name | score 1 | 太郎 | 80 2 | 花子 | 95 3 | 次郎 | 60
採点
php scripts/grade.php topics/file-io/ch04-csv/drill/02-write-csv/
ヒント
$tmp = tempnam(sys_get_temp_dir(), 'dojo_csv_');
// 書く
$fp = fopen($tmp, 'w');
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
fclose($fp);
// 読み返す
$fp = fopen($tmp, 'r');
while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
echo implode(' | ', $row) . "\n";
}
fclose($fp);
unlink($tmp);fputcsv($fp, $row, ',', '"', '')で 配列を 1 行の CSV にして書く- 読み返しは
fgetcsvで 行ごとの配列に戻る →implode(' | ', $row)で表示用に結合 - 書き込みは
sys_get_temp_dir()配下、最後にunlinkで消す
つまづいたら
- 出力が
id | name | scoreではなくid|name|score(空白なし) になる
→ implode(' | ', $row) のセパレータが '|' になっている。前後に空白付きの " | " にする
- Deprecated 警告が出る
→ fgetcsv / fputcsv の引数を省略している。第 2〜5 引数を明示 (fgetcsv($fp, 0, ',', '"', '') / fputcsv($fp, $row, ',', '"', ''))
- 1 行多い・少ない
→ 「書いた配列」と「読み返した結果」は本来一致する。fputcsv を呼び忘れた / fclose を忘れて flush されていない