核心需求
- 查詢結果流式寫入,避免大結果集佔用內存
- 支援指定輸出編碼(UTF-8/GBK/CP1252 等)
- 符合 RFC 4180:雙引號包裹含逗號/換行/引號的字段,雙引號轉義為雙雙引號
- HTTP 下載響應頭正確
完整實現
<?php
// 1. 數據庫連接(建議 PDO)
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 2. 查詢
$stmt = $pdo->query("SELECT id, name, email, created_at FROM users");
// 3. 響應頭
header('Content-Type: text/csv; charset=cp1252'); // 目標編碼
header('Content-Disposition: attachment; filename="export.csv"');
// 4. 輸出流
$out = fopen('php://output', 'w');
// 5. 寫入 BOM(Excel 識別 UTF-8 需 BOM,CP1252 不需)
// fputs($out, "\xEF\xBB\xBF"); // 僅 UTF-8 時啟用
// 6. 表頭
$firstRow = $stmt->fetch(PDO::FETCH_ASSOC);
if ($firstRow) {
fputcsv($out, array_keys($firstRow), ',', '"');
// 寫入第一行數據
fputcsv($out, array_values($firstRow), ',', '"');
}
// 7. 循環寫入剩餘行
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($out, array_values($row), ',', '"');
}
fclose($out);
exit;
?>
手動實現 echocsv(不依賴 fputcsv,完全可控)
function echocsv($fields, $delimiter=',', $enclosure='"', $escape_char='\\') {
$output = '';
foreach ($fields as $field) {
// 轉換編碼(如內部 UTF-8 → 輸出 CP1252)
// $field = iconv('UTF-8', 'CP1252//IGNORE', $field);
if (str_contains($field, $delimiter) ||
str_contains($field, $enclosure) ||
str_contains($field, "\n") ||
str_contains($field, "\r")) {
$field = $enclosure . str_replace($enclosure, $enclosure.$enclosure, $field) . $enclosure;
}
$output .= $field . $delimiter;
}
echo rtrim($output, $delimiter) . "\r\n";
}
編碼處理關鍵點
| 場景 | 處理方式 |
| 數據庫 UTF-8 → 輸出 UTF-8 | 無需轉換,建議加 BOM 兼容 Excel |
| 數據庫 UTF-8 → 輸出 GBK/CP1252 | 每字段 iconv('UTF-8', 'GBK//IGNORE', $val) |
| 數據庫 GBK → 輸出 UTF-8 | 每字段 iconv('GBK', 'UTF-8', $val) |
| 不可轉換字符 | //IGNORE 忽略或 //TRANSLIT 音譯 |
大數據集優化
- 游標/分批:
LIMIT 1000 OFFSET ... 或 WHERE id > last_id ORDER BY id LIMIT 1000
- 禁用緩衝:PDO
MYSQL_ATTR_USE_BUFFERED_QUERY => false(需 mysqlnd)
- 壓縮下載:
Content-Encoding: gzip + gzencode 流式壓縮
- 異步生成:後台任務生成文件,前端輪詢下載鏈接
常見坑
fputcsv 默認區隔符 ,、包裹符 "、轉義符 \,Excel 完美兼容
- 字段包含
\r\n 會導致行數錯亂,必須包裹
- PHP < 5.5.4
fputcsv 不支援自定義轉義字符,需手動實現
- 導出前記得
set_time_limit(0)、ini_set('memory_limit', '512M')