そもそもShift_JISだと表せる文字がUTF-8より減ってしまい,範囲外の文字は?
に化けてしまうので,以下のようにUnicodeを取り扱えるBOMつきのCSVを用いるべきです.
【追記】
CSVにSJISで表現できない文字はありません。
ごめんなさい見てませんでした…
- UTF-8なら
0xEF
0xBB
0xBF
- UTF-16のLE (Little Endian)なら
0xFE
0xFF
(Mac版のExcelはこれしか受け付けない)
- UTF-16のBE (Big Endian)なら
0xFF
0xFE
Macも意識するなら真ん中のやつ1択です.
- [[PHP] Mac版Excelと互換性のあるCSVファイルを出来るだけ効率よく作成する
](http://qiita.com/mpyw/items/2795bef3ed561f4cf4e9)
こいつをもとに今回の要件に合わせて回答を書くと
php
1<?php
2
3// バッファリングをオフにして巨大なCSVファイルも取り扱えるようにする
4// readfileやfpassthruを使うならこれをやっておかないと勿体無い
5while (ob_get_level()) ob_end_clean();
6
7// BOM無しUTF-8のCSVファイルをオープン
8if (!$fp = @fopen('data.csv', 'rb')) {
9 // 失敗時
10 header('Content-Type: text/plain; charset=UTF-8');
11 $error = error_get_last()['message'];
12 if (strpos($error, 'No such file or directory') !== false) {
13 http_response_code(404);
14 } elseif (strpos($error, 'Permission denied') !== false) {
15 http_response_code(403);
16 } else {
17 http_response_code(500);
18 }
19 exit($error);
20}
21
22// 読み出し時にUTF-8からUTF-16LEに変換するようにストリームフィルタを付加
23stream_filter_prepend($fp, 'convert.iconv.utf-8/utf-16le');
24
25// ダウンロード用のHTTPヘッダを出力
26// MIMEタイプも正しく出しておいたほうが親切
27header('Content-Type: text/csv; charset=UTF-16');
28header('Content-Disposition: attachment; filename="data.csv"');
29
30// UTF-16(LE)のBOMを出力
31echo "\xFE\xFF";
32
33// 続けてCSVファイルの内容をUTF-16LEで出力
34// バッファリングをオフにしていれば最も効率のいい区切りでPHPが適当に分割出力してくれる
35fpassthru($fp);
となります.ストリームフィルタを使っているので,メモリに載りきらないような巨大なCSVファイルも取り扱える上に非常に処理が高速です.