LoginSignup
24
24

More than 5 years have passed since last update.

PHPでCSVエクスポート時の改行されてしまう問題解消

Posted at

今さらな感じですが、よく忘れてしまうので備忘録程度になります。
phpでエクスポートすると、文字コードやら改行コードを気にしなきゃいけなくて何かとめんどくさい。
ライブラリーを使った方が確実ですが、メモリやCPUに負担がかかるのを避けたいので、
オレオレコードでエクスポートしています。

表題とずれてしまいますが、まずはメモリ問題を抑える方法を書いておきます。

phpの消費メモリを抑える

あくまでもphpのメモリを抑えるためのコードなので、サーバーのバッファに使っているメモリは
ここでは気にしないことにします。

CSVを出力するときはいろんな方法があると思います。foreachで変数等に入れて、最後に出力したり、
ファイルに直接1行ずつ書き込んだり、ストリームに出力したり。
変数等にデータを貯めこむとメモリは使ってしまうし、ファイルへの出力は書き込みが遅いかったり、
出力時にfopenで開くと結局はファイルの容量分だけメモリを消費したりと、
この辺のやり方は使えなさそうですね。

なので、今回はphpのストームを使います。ストリームにも種類がありますが、出力バッファに
直接書き込んでくれるphp://outputを使おうと思います。

簡単に書くとこんな感じですね。ポイントは、ヘッダー情報をfopenより前に行うことですね。

sample-export.php
<?php 
// 区切り文字と囲み文字を指定
define('CSV_DELIMITER', ',');
define('CSV_ENCLOSURE', '"');

// 念のために出力をキレイにする
ob_clean();

// ヘッダー送信
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=sample-". strtotime("now") .".csv");

// ストリームを開く
$fp = fopen('php://output','w');

// $dataにDBから取得した情報が入っているとして
foreach( $data as $row )
{
    // ストリームにCSVになったものを書き込んでいく
    fputcsv($fp, $row, CSV_DELIMITER, CSV_ENCLOSURE);
}

// ストリームを閉めて、出力を終える(必須ではないが、念のため)
fclose($fp); exit;

シンプルですね。テストはしていないので、間違っていたらすみません…
これでメモリ問題は最善を尽くした感じにはなると思います。

しかし、デメリットが一つあります。ヘッダーを早い段階で送信しているので、
ファイルサイズが不明です。なので、ブラウザで『残りX秒』的な表示やプログレスバーが
表示されない形になってしまいます。重要ではないが、気にする人は気にするので…

文字コードを修正する

DBの文字コードはUTF-8が主流なので、このままだとExcel等で文字化けが起こってしまいます。
文字コードの修正方法も色々ありますね。$rowを更にforeachで回してmb_convert_encoding等で
変換したり、一旦溜めたものをテキストにして変換したりなど。
しかし、どれもメモリを食うので、出力中になんどかならないかと考えてみる。

ストリームにもフィルター機能があるので、それをうまく活用しましょう。

sample-export.php
<?php 
// 区切り文字と囲み文字を指定
define('CSV_DELIMITER', ',');
define('CSV_ENCLOSURE', '"');

// 念のために出力をキレイにする
ob_clean();

// ヘッダー送信
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=sample-". strtotime("now") .".csv");

// ストリームを開く
$fp = fopen('php://output','w');

// UTF-8 を Shift_JIS に変換
stream_filter_append($fp, 'convert.iconv.UTF-8/CP932//TRANSLIT', STREAM_FILTER_WRITE);

// $dataにDBから取得した情報が入っているとして
foreach( $data as $row )
{
    // ストリームにCSVになったものを書き込んでいく
    fputcsv($fp, $row, CSV_DELIMITER, CSV_ENCLOSURE);
}

// ストリームを閉めて、出力を終える
fclose($fp); exit;

stream_filter_appendを活用して、UTF-8からShift_JISに変換しています。
CP932はShift_JISのことらしいです。また、iconv関数が使えない環境にあると
使えないコードになってしまうのでご注意ください。

ここでポイントはTRANSLITという記述です。変換ができない文字が含まれていた場合に
文字化けが起こってしまうので、これで強制的に変換になります。恐らく『?』に置き換わるけど
化けるよりはマシという判断ですね。完璧にしたい方は、CP932を調整すると良いかもしれません。

これで完璧だ!と思っていた時期がありましたが、MySQLのエスケープ問題が残ってましたね。

データベースのエスケープに対応する

例えば、書き込みデータの中に下記のような文字が入ってたとします。

テストテスト\"テスト\"テスト\r\nテスト\r\nテスト\r\nテスト

これはデータベースに入れる前にエスケープ処理を加えたことで
クオートの前にバックスラッシュがついたものですね。
どうなるんでしょうか?MacのExcelで開くと下記のように何故か改行されてしまう。

スクリーンショット 2015-12-17 11.25.23.jpg

なので、エスケープされたものをキレイに表示したいと思います。

sample-export.php
<?php 
// 区切り文字と囲み文字を指定
define('CSV_DELIMITER', ',');
define('CSV_ENCLOSURE', '"');


class php_fix_escaped_quotes_filter extends php_user_filter {

    function filter( $in, $out, &$consumed, $closing )
    {
        while($bucket = stream_bucket_make_writeable($in))
        {
            $bucket->data = str_replace('\\' . CSV_ENCLOSURE, CSV_ENCLOSURE . CSV_ENCLOSURE, $bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }

        return PSFS_PASS_ON;
    }

}

// 念のために出力をキレイにする
ob_clean();

// ヘッダー送信
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=sample-". strtotime("now") .".csv");

// ストリームを開く
$fp = fopen('php://output','w');

// エスケープされたものを変換
stream_filter_register("fix_escaped_quotes", "php_fix_escaped_quotes_filter");
stream_filter_append($fp, 'fix_escaped_quotes', STREAM_FILTER_WRITE);

// UTF-8 を Shift_JIS に変換
stream_filter_append($fp, 'convert.iconv.UTF-8/CP932//TRANSLIT', STREAM_FILTER_WRITE);

// $dataにDBから取得した情報が入っているとして
foreach( $data as $row )
{
    // ストリームにCSVになったものを書き込んでいく
    fputcsv($fp, $row, CSV_DELIMITER, CSV_ENCLOSURE);
}

// ストリームを閉めて、出力を終える
fclose($fp); exit;

これでエスケープされたものもキレイに表示されるはずです。
記載されているコードは未テストなので、細かいミスがあったらご指摘ください。

他にも方法が沢山あると思いますが、今回はメモリとCPUの消費量を抑えるというのが目的です。
CPUに関しては、ループ中にsleepさせるとさらに軽減できると思います。
処理がタイムアウトするようでしたら、set_time_limit(0) 入れると良いかもしれません。

いまさらながら良く忘れてしまうので、とりあえず載せておきます。

24
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
24