將 CSV 數據渲染為 HTML 表格是後台導入預覽、報表展示的常見需求。本文在基礎代碼基礎上,強化 UTF-8/BOM 處理、欄位轉義、內存控制、無障礙屬性,給出生產可用的渲染器。
<?php
$fp = fopen('sample.csv', 'r') or die("can't open file");
print "<table>\n";
while ($csv_line = fgetcsv($fp)) {
print '<tr>';
for ($i = 0, $j = count($csv_line); $i < $j; $i++) {
print '<td>' . htmlentities($csv_line[$i]) . '</td>';
}
print "</tr>\n";
}
print '</table>\n';
fclose($fp) or die("can't close file");
<?php
class CsvTableRenderer
{
private int $maxRows = 1000; // 防御性限制
private int $rowCount = 0;
private bool $hasHeader = true;
public function __construct(
private string $filepath,
private string $inputEncoding = 'UTF-8', // CSV 實際編碼
private string $outputEncoding = 'UTF-8'
) {}
public function setMaxRows(int $n): self { $this->maxRows = $n; return $this; }
public function setHasHeader(bool $b): self { $this->hasHeader = $b; return $this; }
public function render(): string
{
$handle = fopen($this->filepath, 'r');
if (!$handle) {
throw new RuntimeException("無法打開文件: {$this->filepath}");
}
// 檢測並跳過 BOM
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
$html = ['<table class="csv-table">'];
$isHeaderRow = $this->hasHeader;
while (($row = fgetcsv($handle)) !== false) {
if ($this->rowCount++ >= $this->maxRows) {
$html[] = '<tr><td colspan="999" class="truncated">數據超過 ' . $this->maxRows . ' 行,已截斷顯示</td></tr>';
break;
}
// 編碼轉換
if ($this->inputEncoding !== $this->outputEncoding) {
$row = array_map(fn($v) => mb_convert_encoding($v, $this->outputEncoding, $this->inputEncoding), $row);
}
$tag = $isHeaderRow ? 'th' : 'td';
$html[] = '<tr>';
foreach ($row as $cell) {
// XSS 防護 + 保留換行
$safe = htmlspecialchars($cell, ENT_QUOTES | ENT_SUBSTITUTE, $this->outputEncoding);
$safe = nl2br($safe); // CSV 內換行轉 <br>
$html[] = "<{$tag}>{$safe}</{$tag}>";
}
$html[] = '</tr>';
$isHeaderRow = false;
}
fclose($handle);
$html[] = '</table>';
return implode("\n", $html);
}
}
// 使用示例
echo (new CsvTableRenderer('data.csv'))
->setMaxRows(500)
->setHasHeader(true)
->render();
<style>
.csv-table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
.csv-table th { background: #f5f5f5; font-weight: 600; text-align: left; }
.csv-table th, .csv-table td { border: 1px solid #ddd; padding: 6px 10px; }
.csv-table tr:nth-child(even) td { background: #fafafa; }
.csv-table .truncated { color: #e74c3c; font-style: italic; text-align: center; }
</style>
<?php
// 1. 後端只輸出 JSON(流式生成,不佔內存)
header('Content-Type: application/json; charset=utf-8');
$handle = fopen('big.csv', 'r');
echo '{"headers":[';
$first = true;
if (($row = fgetcsv($handle)) !== false) {
foreach ($row as $cell) {
echo ($first ? '' : ',') . json_encode($cell, JSON_UNESCAPED_UNICODE);
$first = false;
}
}
echo '],"rows":[';
$firstRow = true;
while (($row = fgetcsv($handle)) !== false) {
echo ($firstRow ? '' : ',') . json_encode($row, JSON_UNESCAPED_UNICODE);
$firstRow = false;
}
echo ']}';
fclose($handle);
前端用 DataTables、AG Grid 或輕量級 vanilla JS 實現虛擬滾動分頁,支撐百萬行數據流暢渲染。
| 問題 | 原因 | 解決 |
|---|---|---|
| 中文亂碼 | CSV 為 GBK/GB2312,頁面 UTF-8 | mb_convert_encoding 或 fopen 時指定編碼(PHP 8.1+ 支援) |
| HTML 注入/XSS | 直接輸出 cell 內容 | htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE) |
| 換行丟失 | CSV 單元格含 \n | nl2br() 或 CSS white-space: pre-wrap |
| 大文件 OOM | 一次性讀全文件 | 流式 fgetcsv + 限制行數 / 前端虛擬滾動 |
| BOM 干擾 | UTF-8 BOM 被作為第一個字段前綴 | fread 3 字節檢測並 rewind |
規範的 CSV 表格渲染,不僅是「讀取→輸出」,更是編碼、安全、性能、體驗的綜合工程。