> 二维数组(行 => 列) */ public static function read(string $file): array { if (!is_file($file)) { throw new RuntimeException('文件不存在:' . $file); } $entries = self::readZip($file); $shared = []; if (isset($entries['xl/sharedStrings.xml'])) { $shared = self::parseSharedStrings($entries['xl/sharedStrings.xml']); } $sheet = self::firstSheetContent($entries); if ($sheet === null) { throw new RuntimeException('未找到工作表内容'); } return self::parseSheet($sheet, $shared); } // ---------------- ZIP ---------------- private static function readZip(string $file): array { if (class_exists('ZipArchive')) { $out = []; $za = new ZipArchive(); if ($za->open($file) === true) { for ($i = 0; $i < $za->numFiles; $i++) { $name = $za->getNameIndex($i); if ($name === false) { continue; } $content = $za->getFromIndex($i); $out[$name] = $content === false ? '' : $content; } $za->close(); return $out; } } return self::parseZipRaw($file); } private static function parseZipRaw(string $file): array { $data = (string)@file_get_contents($file); $len = strlen($data); if ($len < 22) { throw new RuntimeException('不是有效的 xlsx 文件'); } $eocd = -1; for ($i = $len - 22; $i >= 0; $i--) { if (substr($data, $i, 4) === "\x50\x4b\x05\x06") { $eocd = $i; break; } } if ($eocd < 0) { throw new RuntimeException('不是有效的 xlsx/zip 文件(未找到目录结束标记)'); } $count = self::u16($data, $eocd + 10); $offset = self::u32($data, $eocd + 16); $out = []; $p = $offset; for ($i = 0; $i < $count; $i++) { if (substr($data, $p, 4) !== "\x50\x4b\x01\x02") { break; } $method = self::u16($data, $p + 10); $compSize = self::u32($data, $p + 20); $nameLen = self::u16($data, $p + 28); $extraLen = self::u16($data, $p + 30); $commentLen = self::u16($data, $p + 32); $localOff = self::u32($data, $p + 42); $name = substr($data, $p + 46, $nameLen); $lNameLen = self::u16($data, $localOff + 26); $lExtraLen = self::u16($data, $localOff + 28); $start = $localOff + 30 + $lNameLen + $lExtraLen; $raw = substr($data, $start, $compSize); if ($method === 0) { $content = $raw; } else { $content = @gzinflate($raw); if ($content === false) { $content = @gzinflate(substr($raw, 2)); } } $out[str_replace('\\', '/', $name)] = $content === false ? '' : $content; $p += 46 + $nameLen + $extraLen + $commentLen; } return $out; } private static function u16(string $d, int $off): int { $s = substr($d, $off, 2); return $s === '' || strlen($s) < 2 ? 0 : unpack('v', $s)[1]; } private static function u32(string $d, int $off): int { $s = substr($d, $off, 4); return $s === '' || strlen($s) < 4 ? 0 : unpack('V', $s)[1]; } // ---------------- XML ---------------- private static function firstSheetContent(array $entries): ?string { if (isset($entries['xl/worksheets/sheet1.xml'])) { return $entries['xl/worksheets/sheet1.xml']; } $found = []; foreach (array_keys($entries) as $k) { if (preg_match('#^xl/worksheets/sheet\d+\.xml$#', $k)) { $found[] = $k; } } if ($found === []) { return null; } sort($found, SORT_NATURAL); return $entries[$found[0]]; } private static function parseSharedStrings(string $xml): array { $out = []; if (preg_match_all('#(.*?)#s', $xml, $m) > 0) { foreach ($m[1] as $si) { if (preg_match_all('#]*>(.*?)#s', $si, $tm) > 0) { $out[] = implode('', array_map( static fn($x) => html_entity_decode($x, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $tm[1] )); } else { $out[] = ''; } } } return $out; } private static function parseSheet(string $xml, array $shared): array { $rows = []; if (preg_match_all('#]*>(.*?)#s', $xml, $rm) === 0) { return $rows; } foreach ($rm[1] as $rowXml) { $cells = []; if (preg_match_all('#]*?))?(?:/>|>(.*?))#s', $rowXml, $cm, PREG_SET_ORDER) > 0) { $autoIdx = 0; foreach ($cm as $c) { $attrs = $c[1] ?? ''; $inner = $c[2] ?? ''; $idx = $autoIdx; if (preg_match('#\br="([A-Z]+)\d*"#', $attrs, $r)) { $idx = self::colToIndex($r[1]); } $autoIdx = $idx + 1; $type = preg_match('#\bt="([^"]+)"#', $attrs, $tm) ? $tm[1] : ''; if ($type === 'inlineStr') { $val = ''; if (preg_match_all('#]*>(.*?)#s', $inner, $im) > 0) { $val = implode('', array_map( static fn($x) => html_entity_decode($x, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $im[1] )); } } else { $val = preg_match('#(.*?)#s', $inner, $vm) ? html_entity_decode($vm[1], ENT_QUOTES | ENT_HTML5, 'UTF-8') : ''; if ($type === 's') { $i = (int)$val; $val = $shared[$i] ?? ''; } elseif ($type === 'b') { $val = ((int)$val) === 1 ? 'TRUE' : 'FALSE'; } } $cells[$idx] = $val; } } if ($cells === []) { continue; } $max = max(array_keys($cells)); $line = []; for ($i = 0; $i <= $max; $i++) { $line[] = $cells[$i] ?? ''; } $rows[] = $line; } return $rows; } private static function colToIndex(string $letters): int { $n = 0; $len = strlen($letters); for ($i = 0; $i < $len; $i++) { $n = $n * 26 + (ord($letters[$i]) - 64); } return $n - 1; } }