> 二维数组(行 => 列)
*/
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;
}
}