12.

PHP CSV 読み込み完全ガイド (fgetcsv/SplFileObject)

編集
この記事の要点
  • 定番: fopen() + fgetcsv() ループで 1 行ずつ配列で取得
  • SplFileObject + READ_CSV フラグでイテレータ的に扱える(推奨)
  • 文字列からのパースは str_getcsv()
  • SJIS-WIN → UTF-8 変換: mb_convert_encoding($line, 'UTF-8', 'SJIS-win')、BOM 除去は先頭 3 バイト判定
  • 大ファイルは generator で逐次処理、Laravel は League/Csv パッケージが便利

基本: fopen + fgetcsv

PHP 標準の最もシンプルな CSV 読み込み方法です。1 行ずつ配列として取得できます。

$handle = fopen('users.csv', 'r');
if ($handle === false) {
    throw new RuntimeException('CSV を開けません');
}

while (($row = fgetcsv($handle)) !== false) {
    // $row = ['1', '田中', 'tanaka@example.com'] のような数値添字配列
    [$id, $name, $email] = $row;
    echo "$id: $name <$email>\n";
}

fclose($handle);

ヘッダー行のスキップ + 連想配列化

$handle = fopen('users.csv', 'r');

// 1 行目をヘッダーとして取得
$header = fgetcsv($handle);
// ['id', 'name', 'email']

while (($row = fgetcsv($handle)) !== false) {
    // ヘッダーと組み合わせて連想配列に
    $data = array_combine($header, $row);
    // ['id' => '1', 'name' => '田中', 'email' => 'tanaka@example.com']

    echo $data['name'] . "\n";
}

fclose($handle);

SplFileObject (推奨)

SplFileObject は CSV をイテレータとして扱え、より OOP らしく書けます。

$file = new SplFileObject('users.csv', 'r');
$file->setFlags(
    SplFileObject::READ_CSV         // CSV としてパース
    | SplFileObject::READ_AHEAD     // 先読み(末尾空行回避)
    | SplFileObject::SKIP_EMPTY     // 空行スキップ
    | SplFileObject::DROP_NEW_LINE  // 改行除去
);

// 区切り文字 / 囲い文字を変更したい場合
$file->setCsvControl(',', '"', '\\');

foreach ($file as $row) {
    if ($row === [null]) continue;  // 空行
    print_r($row);
}

str_getcsv (文字列から)

すでにメモリ上にある CSV 文字列をパースするには str_getcsv() を使います:

$line = '1,"田中, 太郎","tanaka@example.com"';
$row = str_getcsv($line);
// ['1', '田中, 太郎', 'tanaka@example.com']

// CSV 全体を行ごとにパース
$csv = "id,name\n1,田中\n2,佐藤";
$rows = array_map('str_getcsv', explode("\n", $csv));

Shift-JIS (SJIS-WIN) CSV を読む

Excel が日本語環境で保存した CSV はSJIS-WIN (CP932) エンコーディングが多く、PHP のデフォルト UTF-8 と齟齬します。

// 方法1: 読み込み時にストリームフィルタを噛ます
$handle = fopen('sjis.csv', 'r');
stream_filter_append($handle, 'convert.iconv.SJIS-WIN/UTF-8');

while (($row = fgetcsv($handle)) !== false) {
    print_r($row);   // UTF-8 で取得できる
}

fclose($handle);

// 方法2: 1 行ずつ手動変換
$handle = fopen('sjis.csv', 'r');
while (($row = fgetcsv($handle)) !== false) {
    $row = array_map(
        fn($v) => mb_convert_encoding($v, 'UTF-8', 'SJIS-win'),
        $row
    );
    print_r($row);
}
fclose($handle);

// 方法3: setlocale 経由(fgetcsv が SJIS を理解できるよう)
setlocale(LC_ALL, 'ja_JP.SJIS');

BOM 除去

Excel の「CSV UTF-8」保存は先頭に BOM (EF BB BF, 3 バイト) が付きます。最初のカラム名が "\xEF\xBB\xBFid" のようになり、ヘッダー連想配列が壊れます。

function removeBom(string $s): string {
    return preg_replace('/^\xEF\xBB\xBF/', '', $s);
}

// ヘッダー読み込み時に適用
$header = fgetcsv($handle);
$header[0] = removeBom($header[0]);

大ファイル向け: Generator パターン

100MB を超えるような CSV では全行をメモリに乗せず、generator で逐次処理します:

function readCsv(string $path): Generator
{
    $handle = fopen($path, 'r');
    try {
        $header = fgetcsv($handle);
        $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);

        while (($row = fgetcsv($handle)) !== false) {
            if ($row === [null]) continue;
            yield array_combine($header, $row);
        }
    } finally {
        fclose($handle);
    }
}

// 使用: 100 万行でもメモリ数 MB
foreach (readCsv('big.csv') as $i => $user) {
    User::create($user);
    if ($i % 1000 === 0) {
        echo "$i 件処理\n";
    }
}

Laravel: League/Csv パッケージ

Laravel エコシステムで最も使われている CSV ライブラリ:

composer require league/csv
use League\Csv\Reader;

$csv = Reader::createFromPath(storage_path('app/users.csv'), 'r');
$csv->setHeaderOffset(0);                    // 1 行目をヘッダーに

// Shift-JIS なら
$csv->addStreamFilter('convert.iconv.SJIS-WIN/UTF-8');

foreach ($csv->getRecords() as $record) {
    // ['id' => '1', 'name' => '田中', ...]
    User::create($record);
}

// 統計
echo count($csv) . " 行\n";

// クエリビルダ風フィルタ
use League\Csv\Statement;
$stmt = (new Statement())->where(fn($r) => (int)$r['age'] >= 20)
                          ->limit(100);
foreach ($stmt->process($csv) as $r) {
    // ...
}

よくあるトラブル

症状原因対処
日本語が文字化けSJIS-WIN を UTF-8 で読んでいるストリームフィルタ or mb_convert_encoding
1 列目のヘッダー名がおかしいBOM が付いているpreg_replace('/^\xEF\xBB\xBF/', '', ...)
改行 (CRLF) で列がずれるセル内の \r\n を区切り扱いini_set('auto_detect_line_endings', '1') (PHP 8.1 まで) or SplFileObject 利用
カンマを含むセルで列数が増えるクォート無しExcel 経由で再保存 or 元データ修正
1 行目だけ読めないBOM を含むファイルを file() で読んでいるBOM 除去 or fgetcsv 利用

FAQ

Q: TSV (タブ区切り) を読みたい
A: fgetcsv($handle, 0, "\t") のように区切り文字を指定。SplFileObject なら setCsvControl("\t")

Q: メモリエラーが出る
A: file()file_get_contents() + explode はファイル全体をメモリに載せます。fgetcsv ループや generator に切り替えてください。

Q: PHP 8.4 で fgetcsv() の挙動が変わったと聞いた
A: PHP 8.4 で escape 引数のデフォルト変更があります。RFC 4180 準拠を期待するなら明示的に fgetcsv($h, 0, ",", '"', "") と空文字を渡すのが安全。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストール方法
  2. 文法
  3. Composerのインストール
  4. 内部関数
  5. フレームワーク
  6. エラー一覧
  7. 改行出力
  8. printとechoの違い
  9. シングルクォートとダブルクォートの違い
  10. returnとyieldの違い
  11. var_dumpをログ出力
  12. CSV読み込み
  13. 待機・処理の遅延
  14. ログファイルにエラーを出力する方法
  15. エラーログ出力関数
  16. URLパラメータの配列化
  17. empty, is_null. issetの判定比較表
  18. httpステータスコードの付与
  19. バージョンの確認
  20. php.ini
  21. APIを呼び出す方法
  22. 外部ファイルを呼び出す方法
  23. カンマ区切りの文字列を配列に変換
  24. 配列からランダムに値を取り出す方法
  25. Webスクレイピング

最近更新/作成されたページ