今さらな感じですが、よく忘れてしまうので備忘録程度になります。
phpでエクスポートすると、文字コードやら改行コードを気にしなきゃいけなくて何かとめんどくさい。
ライブラリーを使った方が確実ですが、メモリやCPUに負担がかかるのを避けたいので、
オレオレコードでエクスポートしています。
表題とずれてしまいますが、まずはメモリ問題を抑える方法を書いておきます。
phpの消費メモリを抑える
あくまでもphpのメモリを抑えるためのコードなので、サーバーのバッファに使っているメモリは
ここでは気にしないことにします。
CSVを出力するときはいろんな方法があると思います。foreachで変数等に入れて、最後に出力したり、
ファイルに直接1行ずつ書き込んだり、ストリームに出力したり。
変数等にデータを貯めこむとメモリは使ってしまうし、ファイルへの出力は書き込みが遅いかったり、
出力時にfopenで開くと結局はファイルの容量分だけメモリを消費したりと、
この辺のやり方は使えなさそうですね。
なので、今回はphpのストームを使います。ストリームにも種類がありますが、出力バッファに
直接書き込んでくれるphp://outputを使おうと思います。
簡単に書くとこんな感じですね。ポイントは、ヘッダー情報をfopenより前に行うことですね。
<?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等で
変換したり、一旦溜めたものをテキストにして変換したりなど。
しかし、どれもメモリを食うので、出力中になんどかならないかと考えてみる。
ストリームにもフィルター機能があるので、それをうまく活用しましょう。
<?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で開くと下記のように何故か改行されてしまう。
なので、エスケープされたものをキレイに表示したいと思います。
<?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) 入れると良いかもしれません。
いまさらながら良く忘れてしまうので、とりあえず載せておきます。