CSV 導出是後台管理系統、報表系統的高頻功能。看似簡單的 fputcsv(),實際落地時常遇到中文亂碼、換行符不一致、內存溢出、Excel 打開欄位錯位等問題。本文通過重構示例代碼,給出生產級導出方案。
<?php
// 原代碼存在的問題:
// 1. 硬編碼文件名、行數限制 1838 行
// 2. 直接操作 $item[0]..$item[3] 假設固定列數,極易越界
// 3. 無 BOM 頭,Excel 直接打開中文亂碼
// 4. 未處理字段內含逗號、雙引號、換行符的轉義
// 5. 全量讀入內存,大文件導致 OOM
<?php
class CsvExporter
{
private $handle;
private string $filename;
private int $bufferSize = 1000; // 緩衝行數
private array $buffer = [];
private int $count = 0;
public function __construct(string $filename, array $headers = [])
{
$this->filename = $filename;
// 以追加模式打開,自動創建
$this->handle = fopen($filename, 'w');
if (!$this->handle) {
throw new RuntimeException("無法創建文件: $filename");
}
// 寫入 UTF-8 BOM,修復 Excel 亂碼
fwrite($this->handle, "\xEF\xBB\xBF");
if ($headers) {
$this->writeRow($headers);
}
}
public function writeRow(array $row): void
{
$this->buffer[] = $row;
if (count($this->buffer) >= $this->bufferSize) {
$this->flush();
}
}
public function flush(): void
{
foreach ($this->buffer as $row) {
fputcsv($this->handle, $row);
}
$this->buffer = [];
}
public function __destruct()
{
$this->flush();
if (is_resource($this->handle)) {
fclose($this->handle);
}
}
public function getRowCount(): int
{
return $this->count;
}
}
<?php
// 模擬從數據庫分批讀取(生成器/PDOStatement fetchAll 分頁)
$exporter = new CsvExporter('export_orders_' . date('YmdHis') . '.csv', [
'訂單號', '用戶ID', '商品名', '金額', '下單時間'
]);
// 假設 $pdo 為 PDO 實例
$stmt = $pdo->query("SELECT order_no, user_id, goods_name, amount, created_at FROM orders WHERE status=1");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$exporter->writeRow([
$row['order_no'],
$row['user_id'],
$row['goods_name'],
$row['amount'],
$row['created_at']
]);
}
// 析構函數自動 flush & close
echo "導出完成,共 {$exporter->getRowCount()} 行。";
| 問題 | 原因 | 解決方案 |
|---|---|---|
| Excel 打開中文亂碼 | 缺少 UTF-8 BOM | fwrite($fp, "\xEF\xBB\xBF") 在寫入首行前 |
| 字段含逗號/換行/雙引號導致列錯位 | CSV 格式要求包含分隔符的字段必須雙引號包裹且內部雙引號轉義 | fputcsv() 內部已自動處理,勿自行拼接字符串 |
| 大數據導出內存溢出 | 一次性 fetchAll 或併發寫入 | 分批游標讀取 + 緩衝寫入(如上類) |
| 數字科學計數法/前導零丟失 | Excel 自動識別數值格式 | 字段前加 \t 或 ='value' 強制文本格式 |
| 換行符不一致(Windows CRLF vs Linux LF) | 跨平台查看差異 | fopen 模式用 'w' 由 PHP 自動轉換為系統默認;或指定 'c' 模式手動控制 |
fopen('php://output', 'w') 配合 header('Content-Type: text/csv') 邊生成邊輸出,不落盤。PhpSpreadsheet 導出真正的 .xlsx。掌握 fputcsv 的正確姿勢,加上 BOM、緩衝、流式輸出三板斧,CSV 導出功能即可穩定支撐百萬級數據業務。