,rows:array>} */ public function parse(string $path): array { $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return match ($ext) { 'xlsx' => $this->parseXlsx($path), 'csv' => $this->parseCsv($path), default => throw new \InvalidArgumentException('仅支持 .xlsx 或 .csv 文件'), }; } private function parseCsv(string $path): array { $handle = fopen($path, 'r'); if ($handle === false) { throw new \InvalidArgumentException('无法打开 CSV 文件'); } // 去掉 UTF-8 BOM $first = fgets($handle); if ($first === false) { fclose($handle); return ['header' => [], 'rows' => []]; } if (str_starts_with($first, "\xEF\xBB\xBF")) { $first = substr($first, 3); } $header = str_getcsv($first); $rows = []; while (($line = fgetcsv($handle)) !== false) { // 跳过空行 if (count($line) === 1 && trim($line[0]) === '') { continue; } $rows[] = $line; } fclose($handle); return ['header' => $header, 'rows' => $rows]; } private function parseXlsx(string $path): array { $zip = new \ZipArchive; if ($zip->open($path) !== true) { throw new \InvalidArgumentException('无法打开 .xlsx 文件'); } // sharedStrings $shared = []; $ss = $zip->getFromName('xl/sharedStrings.xml'); if ($ss !== false) { $xml = new \SimpleXMLElement($ss); foreach ($xml->si as $si) { $text = ''; foreach ($si->t ?? [] as $t) { $text .= (string) $t; } $shared[] = $text; } } // 第一个工作表 $sheet = $zip->getFromName('xl/worksheets/sheet1.xml'); if ($sheet === false) { $zip->close(); throw new \InvalidArgumentException('未找到工作表'); } $xml = new \SimpleXMLElement($sheet); $rows = []; $header = []; foreach ($xml->sheetData->row as $rowEl) { $cells = []; foreach ($rowEl->c as $c) { $ref = (string) $c['r']; // 如 "A1", "B2" $col = $this->colIndex($ref); $type = (string) $c['t']; if ($type === 's') { // 共享字符串:v 是 sharedStrings 索引 $v = (string) ($c->v ?? ''); $cells[$col] = $shared[(int) $v] ?? ''; } elseif ($type === 'inlineStr') { // 内联字符串(现代 Excel/WPS 常用):值在 里,富文本可能有多个 $text = ''; foreach ($c->is->t ?? [] as $t) { $text .= (string) $t; } $cells[$col] = $text; } else { // 数字/布尔/公式结果等 $cells[$col] = (string) ($c->v ?? ''); } } $line = []; $max = empty($cells) ? 0 : max(array_keys($cells)); for ($i = 0; $i <= $max; $i++) { $line[$i] = $cells[$i] ?? ''; } $rows[] = $line; } $zip->close(); if (empty($rows)) { return ['header' => [], 'rows' => []]; } $header = array_shift($rows); // 规范化表头:去空白 $header = array_map(fn ($h) => trim((string) $h), $header); return ['header' => $header, 'rows' => $rows]; } /** "A"->0, "B"->1, "AA"->26 ... */ private function colIndex(string $ref): int { $letters = preg_replace('/\d/', '', $ref); $idx = 0; foreach (str_split($letters) as $ch) { $idx = $idx * 26 + (ord($ch) - ord('A') + 1); } return $idx - 1; } // ---- 导出(生成真正的 .xlsx,不依赖 PhpSpreadsheet) ---- /** * 生成 .xlsx 文件,返回临时文件路径。 * * @param array $header 表头 * @param array> $rows 数据行 */ public function exportXlsx(string $sheetName, array $header, array $rows): string { $sheetName = mb_substr(preg_replace('/[\\/?*\[\]:]/', ' ', $sheetName) ?: 'Sheet1', 0, 31) ?: 'Sheet1'; $rowsXml = ''; $rowNum = 1; $rowsXml .= $this->buildRow($rowNum++, $header); foreach ($rows as $row) { $cells = []; foreach ($row as $i => $v) { $cells[$i] = is_string($v) ? $v : (string) $v; } $rowsXml .= $this->buildRow($rowNum++, $cells); } $zip = new \ZipArchive; $tmp = tempnam(sys_get_temp_dir(), 'bomexp'); @unlink($tmp); $path = $tmp.'.xlsx'; if ($zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \RuntimeException('无法创建导出文件'); } $zip->addFromString('[Content_Types].xml', '' .'' .'' .'' .'' .'' .''); $zip->addFromString('_rels/.rels', '' .'' .'' .''); $zip->addFromString('xl/workbook.xml', '' .'' .'' .''); $zip->addFromString('xl/_rels/workbook.xml.rels', '' .'' .'' .''); $zip->addFromString('xl/worksheets/sheet1.xml', '' .'' .$rowsXml .''); $zip->close(); return $path; } /** * 生成 .csv 文件,返回临时文件路径(带 UTF-8 BOM,Excel 打开不乱码)。 */ public function exportCsv(array $header, array $rows): string { $tmp = tempnam(sys_get_temp_dir(), 'bomexp'); $path = $tmp.'.csv'; $fh = fopen($path, 'w'); if ($fh === false) { throw new \RuntimeException('无法创建导出文件'); } fwrite($fh, "\xEF\xBB\xBF"); fputcsv($fh, $header); foreach ($rows as $row) { fputcsv($fh, array_map(fn ($v) => (string) $v, $row)); } fclose($fh); return $path; } private function buildRow(int $rowNum, array $cells): string { $xml = ''; $col = 0; foreach ($cells as $v) { $ref = $this->colLetter($col).$rowNum; $xml .= ''.$this->xml((string) $v).''; $col++; } $xml .= ''; return $xml; } private function colLetter(int $idx): string { $s = ''; $idx++; while ($idx > 0) { $idx--; $s = chr(ord('A') + ($idx % 26)).$s; $idx = intdiv($idx, 26); } return $s; } private function xml(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_XML1, 'UTF-8'); } }