在不依賴第三方庫(如 PhpSpreadsheet)的情況下,PHP 原生導出 Excel 主要有兩種經典方案:Tab 分隔文本流輸出(偽裝 .xls) 和 HTML 表格輸出(偽裝 .xls)。兩者各有優劣,本文將完整呈現兩種方案的核心代碼、關鍵點解析及選型建議。
設置 HTTP Header 為 application/vnd.ms-excel,直接向 php://output 流寫入 Tab(\t)分隔、換行(\n)分行的純文本內容。Excel 打開時會自動識別為欄位和行。
<?php
// 配置
$filename = '導出數據_' . date('YmdHis');
$title = '數據報表';
// 執行導出邏輯(通常由表單提交觸發)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 大數據優化設置
set_time_limit(0); // 移除執行時間限制
ini_set('memory_limit', '512M'); // 適當提高內存限制
// 關鍵 Header:強制下載、指定編碼、文件名
header('Content-Type: application/vnd.ms-excel; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '.xls"');
header('Cache-Control: max-age=0');
header('Pragma: public'); // 兼容舊版 IE
// 1. 打開輸出流
$fp = fopen('php://output', 'w');
if (!$fp) {
die('無法打開輸出流');
}
// 2. 輸出 BOM 頭(解決 Excel 直接雙擊打開 UTF-8 亂碼問題)
fwrite($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
// 3. 輸出表頭
$headers = ['ID', '姓名', '郵箱', '註冊時間'];
fwrite($fp, implode("\t", $headers) . "\n");
// 4. 分批查詢並流式輸出(核心:邊讀邊寫,不佔內存)
$batchSize = 1000;
$offset = 0;
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
while (true) {
$stmt = $pdo->prepare("SELECT id, name, email, created_at FROM users LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) break;
foreach ($rows as $row) {
// 數據清洗:移除換行符、Tab 等破壞結構的字符
$cleanRow = array_map(function($val) {
$val = str_replace(["\r\n", "\n", "\r", "\t"], ' ', $val);
return $val;
}, $row);
fwrite($fp, implode("\t", $cleanRow) . "\n");
}
$offset += $batchSize;
// 定期刷新緩衝區,實時傳輸給瀏覽器
if ($offset % (10 * $batchSize) === 0) {
ob_flush();
flush();
}
}
fclose($fp);
exit;
}
?>
<!DOCTYPE html>
<html><body>
<form method="post"><button type="submit">導出 Excel (Tab 方案)</button></form>
</body></html>
LIMIT/OFFSET 或游標分批讀取,配合 fwrite 直接寫入流,**不將全部數據載入內存**,支撐百萬級導出。charset=utf-8,配合 BOM,雙擊打開正常。輸出包含 Excel 特定 XML 命名空間的 HTML <table>,瀏覽器按 .xls 下載,Excel 打開時解析 HTML 表格結構並識別樣式。
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filename = '樣式報表_' . date('YmdHis');
header('Content-Type: application/vnd.ms-excel; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '.xls"');
header('Cache-Control: max-age=0');
header('Pragma: public');
// 輸出 Excel 識別的 HTML 頭部(含 Office 命名空間)
echo '<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="https://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!--[if gte mso 9]>
<xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>報表數據</x:Name>
<x:WorksheetOptions>
<x:DisplayGridlines/>
<x:PrintGridlines/>
</x:WorksheetOptions>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml>
<![endif]-->
<style>
/* 可在此定義 CSS 樣式,Excel 會部分識別 */
.header { background-color: #4472C4; color: white; font-weight: bold; }
.text-col { mso-number-format: "\@"; } /* 強制文本格式,防止長數字科學計數法 */
</style>
</head>
<body>
<table border="1">';
// 表頭
echo '<tr>';
foreach (['ID', '姓名', '身份證號', '備註'] as $col) {
echo '<th class="header">' . htmlspecialchars($col) . '</th>';
}
echo '</tr>';
// 數據行(此處為演示,實際應分批查詢)
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'user', 'pass');
$stmt = $pdo->query("SELECT id, name, id_card, remark FROM users LIMIT 5000");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo '<tr>';
echo '<td>' . htmlspecialchars($row['id']) . '</td>';
echo '<td>' . htmlspecialchars($row['name']) . '</td>';
// 長數字/身份證號強制文本格式
echo '<td class="text-col">' . htmlspecialchars($row['id_card']) . '</td>';
echo '<td>' . htmlspecialchars($row['remark']) . '</td>';
echo '</tr>';
}
echo '</table></body></html>';
exit;
}
?>
<!DOCTYPE html>
<html><body>
<form method="post"><button type="submit">導出 Excel (HTML 方案)</button></form>
</body></html>
xmlns:x="urn:schemas-microsoft-com:office:excel" 及 <x:ExcelWorkbook> 區塊是 Excel 識別此 HTML 為試算表的關鍵。mso-number-format: "\@" 強制文本格式**,解決長數字(身份證、銀行卡)自動變科學計數法的痛點。< > & 破壞 HTML 結構。echo 拼接巨大 HTML 字符串,極易爆內存。**實際大數據場景建議分批輸出配合 ob_flush/flush,或改用方案一。**| 維度 | 方案一:Tab 分隔流 | 方案二:HTML 表格 |
|---|---|---|
| 內存佔用 | 極低(流式寫入) | 高(需構建完整 HTML 或大量 echo) |
| 數據量適應 | 百萬行+ 無壓力 | 建議 < 5萬行,否則極易 OOM |
| 樣式支持 | 無(純文本) | 豐富(顏色、字體、邊框、數字格式) |
| 合併儲存格 | 不支援 | 支援 (rowspan/colspan) |
| 中文亂碼風險 | 需加 BOM 解決 | UTF-8 Header 通常正常 |
| 實現複雜度 | 低 | 中(需處理 HTML/Office XML) |
| 適用場景 | 大數據導出、後台報表、定時任務 | 精美報表、需合併儲存格、強制文本格式場景 |
對於生產環境,**強烈建議使用成熟庫**:
原生導出無依賴、部署簡單。**方案一(Tab 流)是大數據、高性能場景的「瑞士軍刀」**,正確加 BOM、清洗字符、分批流式輸出即可穩健運行;**方案二(HTML 表格)適合中小數據、需樣式/合併儲存格/強制文本格式的報表**,但必須警惕內存陷阱。根據業務數據量、樣式需求、服務器資源選型,或直接升級成熟庫,才是長久之計。
PHP 原生導出 Excel 兩大方案完整解析:Tab 分隔流式輸出(大數據首選)與 HTML 表格偽裝法(樣式豐富),附完整代碼、關鍵點、對比表及現代庫推薦。