make is.dev make it simple. development.
2024年9月13日

PHPでCSVファイル出力

SplFileObjectを使ってCSVファイルを出力してみる。

使用環境

・ Amazon Linux 2
・ PHP 8.2.9 ※default_charset = UTF-8 環境

※以下はUTF-8環境を前提に書いているが、そうでない場合は話が変わるので注意。

基本サンプル

<?php

// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];


// インスタンス生成
$fp = new SplFileObject( './sample.csv', 'w' );

// 出力する(例として100行同じ内容を出力)
for ( $i = 0; $i < 100; ++$i ) {
    $fp->fputcsv( $vs, escape:'' );    // 行末をCRLFにしたい場合は、eol:"\r\n"も指定
}
PHP

SplFileObjectを使う場合、最低限であればこれだけで終わる。

出力される文字コードはUTF-8(BOMなし)。

UTF-8(BOMあり)で出力する

BOMありで出力する場合、先頭にBOMの情報を付加して出力すればOK。

<?php

// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];

// インスタンス生成
$fp = new SplFileObject( './sample.csv', 'w' );

// 出力する(例として100行同じ内容を出力)
for ( $i = 0; $i < 100; ++$i ) {

    // BOMを付加する
    if ( $i === 0 ) {
        $vs[0] = "\xEF\xBB\xBF" . $vs[0];
    }

    $fp->fputcsv( $vs, escape: '' );    // 行末をCRLFにしたい場合は、eol : "\r\n"も指定
}
PHP

SJISで出力する

SJISのファイルを出力したい場合は、出力する配列の内容をSJISにすれば良い。

<?php

// 文字コードをSJISに変換する
function convSjis( $v ) {
    return mb_convert_encoding( $v, 'SJIS-win', 'UTF-8' );
}


// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];
$vs = array_map( 'convSjis', $vs );


// インスタンス生成
$fp = new SplFileObject( './sample.csv', 'w' );

// 出力する(例として100行同じ内容を出力)
for ( $i = 0; $i < 100; ++$i ) {
    $fp->fputcsv( $vs, escape: '' );    // 行末をCRLFにしたい場合は、eol : "\r\n"も指定
}
PHP

fputcsv()の注意点

いくつかのルールが固定されるので、これに沿わない場合は fputcsv() は使えない。

  • 囲み文字は必要な時に必ず付く(常につけたり、なしには出来ない)
  • 囲み文字(デフォルトはダブルクォーテーション)のエスケープは二重出力

これは、SplFileObjectを使わずに fopen() + fputcsv() で出力しても同様。

このあたりを調整するには、以下のように自前で配列を文字列に変換して出力する必要がある。

囲み文字やエスケープを自前で調整

常に囲み文字を付ける(ブランクの場合も囲み文字あり)のサンプル。

<?php

// 値に囲み文字を付ける&囲み文字をエスケープ
function convVs( $v ) {
    return '"' . preg_replace( '/"/u', '""', $v ) . '"';
}


// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    null,
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];


// インスタンス生成
$fp = new SplFileObject( './sample.csv', 'w' );

// 出力する(例として100行同じ内容を出力)
for ( $i = 0; $i < 100; ++$i ) {
    $str = implode( ',', array_map( 'convVs', $vs ) ) . "\n";
    $fp->fwrite( $str );
}
PHP

BOMありやSJISで出力する場合も前述の内容でそのまま使える。

SplFileObjectを使わない場合

SplFileObjectを使わない場合、fopen() + fputcsv() (又は fwrite() )で同じように出力出来る。

<?php

// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];


// ファイルを開く
$fp = fopen( './sample.csv', 'w' );

// 出力する(例として100行同じ内容を出力)
for ( $i = 0; $i < 100; ++$i ) {
    fputcsv( $fp, $vs, escape: '' );
}

// ファイルを閉じる
fclose( $fp );
PHP

SplFileObject::fputcsv() と fputcsv()、SplFileObject::fwrite() と fwrite() は同じように使えるので、どちらを使っても似た実装になる。

多量に出力する場合

1行ずつ出力するとその分File I/Oに時間がかかるので、多量に出力する場合は複数行をまとめて出力した方が速い。

<?php

// 値に囲み文字を付ける&囲み文字をエスケープ
function convVs( $v ) {
    return '"' . preg_replace( '/"/u', '""', $v ) . '"';
}


// 出力する内容を配列で用意する
$vs = [
    'あいうえお',
    'カンマ「,」',
    null,
    'ダブルクォーテーション「"」',
    "改行「\n」",
    "バックスラッシュ「\\」"
];


// インスタンス生成
$fp = new SplFileObject( './sample.csv', 'w' );

// 出力内容のストック初期化
$stock = '';

// 出力する(例として500,000行同じ内容を出力)
for ( $i = 0; $i < 500000; ++$i ) {

    // 行毎に出力せず一度ストックに格納する
    $stock .= implode( ',', array_map( 'convVs', $vs ) ) . "\n";

    if ( $i > 0 && $i % 1000 === 0 ) {
        $fp->fwrite( $stock );
        $stock = '';
    }
}

// 出力出来ていない分を出力する
if ( $stock !== '' ) {
    $fp->fwrite( $stock );
}
PHP

複数行まとめて出力する場合は fputcsv() は使えないので、囲み文字やエスケープは自前で調整する必要がある。

また、ある程度出力内容をためるので、その分メモリを使う。
何件ずつ出力するかは、使えるメモリ量を踏まえて適宜判断が必要。