BulkTableIo.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. <?php
  2. namespace app\common\util;
  3. /**
  4. * CSV / XLSX 批量导入导出(无第三方依赖,xlsx 依赖 ZipArchive)
  5. */
  6. class BulkTableIo
  7. {
  8. const MAX_IMPORT_ROWS = 2000;
  9. const MAX_EXPORT_ROWS = 10000;
  10. public static function colName($index)
  11. {
  12. $n = (int)$index;
  13. $s = '';
  14. while ($n >= 0) {
  15. $s = chr(65 + ($n % 26)) . $s;
  16. $n = intdiv($n, 26) - 1;
  17. }
  18. return $s;
  19. }
  20. public static function xmlEsc($str)
  21. {
  22. return htmlspecialchars((string)$str, ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  23. }
  24. public static function filterRowKeys(array $row, array $allowedKeys)
  25. {
  26. $allowed = array_flip($allowedKeys);
  27. $out = [];
  28. foreach ($row as $k => $v) {
  29. if (isset($allowed[$k])) {
  30. $out[$k] = $v;
  31. }
  32. }
  33. return $out;
  34. }
  35. public static function prepareGenericForSave(array $data, $prefix)
  36. {
  37. $idKey = $prefix . '_id';
  38. if (isset($data[$idKey])) {
  39. $id = (int)$data[$idKey];
  40. $data[$idKey] = $id > 0 ? $id : null;
  41. if ($id <= 0) unset($data[$idKey]);
  42. }
  43. if (isset($data['type_id'])) {
  44. $data['type_id'] = (int)$data['type_id'];
  45. }
  46. $data['uptime'] = isset($data['uptime']) ? (int)$data['uptime'] : 0;
  47. $data['uptag'] = isset($data['uptag']) ? (int)$data['uptag'] : 0;
  48. $multiFields = ($prefix === 'vod') ? ['vod_play_from','vod_play_server','vod_play_note','vod_play_url','vod_down_from','vod_down_server','vod_down_note','vod_down_url']
  49. : [$prefix.'_content', $prefix.'_title', $prefix.'_note'];
  50. foreach ($multiFields as $f) {
  51. if (!isset($data[$f]) || is_array($data[$f])) continue;
  52. $v = $data[$f];
  53. if ($v === '' || $v === null) { unset($data[$f]); continue; }
  54. $data[$f] = (strpos($v, '$$$') !== false) ? explode('$$$', $v) : [$v];
  55. }
  56. return $data;
  57. }
  58. public static function parseFile($path, $ext)
  59. {
  60. $ext = strtolower($ext);
  61. if ($ext === 'csv' || $ext === 'txt') {
  62. return self::parseCsv($path);
  63. }
  64. if (in_array($ext, ['xlsx', 'xlsm'], true)) {
  65. return self::parseXlsx($path);
  66. }
  67. throw new \InvalidArgumentException('unsupported format');
  68. }
  69. public static function parseCsv($path)
  70. {
  71. $handle = fopen($path, 'rb');
  72. if (!$handle) {
  73. throw new \RuntimeException('read fail');
  74. }
  75. $bom = fread($handle, 3);
  76. if ($bom !== "\xEF\xBB\xBF") {
  77. rewind($handle);
  78. }
  79. $headers = fgetcsv($handle);
  80. if ($headers === false || empty($headers)) {
  81. fclose($handle);
  82. return ['headers' => [], 'rows' => []];
  83. }
  84. $headers = array_map(function ($h) {
  85. return trim((string)$h);
  86. }, $headers);
  87. $rows = [];
  88. while (($line = fgetcsv($handle)) !== false) {
  89. if ($line === [null] || $line === false) {
  90. continue;
  91. }
  92. $allEmpty = true;
  93. foreach ($line as $cell) {
  94. if ($cell !== '' && $cell !== null) {
  95. $allEmpty = false;
  96. break;
  97. }
  98. }
  99. if ($allEmpty) {
  100. continue;
  101. }
  102. $assoc = [];
  103. foreach ($headers as $i => $h) {
  104. if ($h === '') {
  105. continue;
  106. }
  107. $assoc[$h] = isset($line[$i]) ? $line[$i] : '';
  108. }
  109. $rows[] = $assoc;
  110. }
  111. fclose($handle);
  112. return ['headers' => $headers, 'rows' => $rows];
  113. }
  114. public static function parseCellRef($ref)
  115. {
  116. if (!preg_match('/^([A-Z]+)(\d+)$/i', (string)$ref, $m)) {
  117. return [0, 0];
  118. }
  119. $letters = strtoupper($m[1]);
  120. $col = 0;
  121. $len = strlen($letters);
  122. for ($i = 0; $i < $len; $i++) {
  123. $col = $col * 26 + (ord($letters[$i]) - 64);
  124. }
  125. return [$col - 1, (int)$m[2] - 1];
  126. }
  127. public static function parseXlsx($path)
  128. {
  129. if (!class_exists('ZipArchive')) {
  130. throw new \RuntimeException('zip');
  131. }
  132. $zip = new \ZipArchive();
  133. if ($zip->open($path) !== true) {
  134. throw new \RuntimeException('zip open');
  135. }
  136. $sheetPath = 'xl/worksheets/sheet1.xml';
  137. if ($zip->locateName($sheetPath) === false) {
  138. $wb = $zip->getFromName('xl/workbook.xml');
  139. $rel = $zip->getFromName('xl/_rels/workbook.xml.rels');
  140. if ($wb && $rel) {
  141. if (preg_match('/<sheet[^>]+r:id="([^"]+)"/', $wb, $sm)) {
  142. $rid = $sm[1];
  143. if (preg_match('/Relationship[^>]+Id="' . preg_quote($rid, '/') . '"[^>]+Target="([^"]+)"/', $rel, $tm)) {
  144. $target = str_replace('\\', '/', $tm[1]);
  145. if (strpos($target, '/') === false) {
  146. $sheetPath = 'xl/' . $target;
  147. } else {
  148. $sheetPath = 'xl/' . ltrim($target, '/');
  149. }
  150. }
  151. }
  152. }
  153. }
  154. $shared = [];
  155. $ss = $zip->getFromName('xl/sharedStrings.xml');
  156. if ($ss !== false) {
  157. $sx = @simplexml_load_string($ss);
  158. $ns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
  159. if ($sx) {
  160. $sx->registerXPathNamespace('m', $ns);
  161. $sis = $sx->xpath('//m:si') ?: [];
  162. foreach ($sis as $si) {
  163. $ts = $si->xpath('.//m:t') ?: [];
  164. $buf = '';
  165. foreach ($ts as $t) {
  166. $buf .= (string)$t;
  167. }
  168. $shared[] = $buf;
  169. }
  170. }
  171. }
  172. $sheetXml = $zip->getFromName($sheetPath);
  173. $zip->close();
  174. if ($sheetXml === false) {
  175. throw new \RuntimeException('sheet');
  176. }
  177. $sx = @simplexml_load_string($sheetXml);
  178. if (!$sx) {
  179. return ['headers' => [], 'rows' => []];
  180. }
  181. $sx->registerXPathNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
  182. $cells = $sx->xpath('//m:sheetData//m:c');
  183. if (empty($cells)) {
  184. return ['headers' => [], 'rows' => []];
  185. }
  186. $grid = [];
  187. foreach ($cells as $c) {
  188. $r = (string)$c['r'];
  189. if ($r === '') {
  190. continue;
  191. }
  192. list($col, $row) = self::parseCellRef($r);
  193. $t = (string)$c['t'];
  194. $val = '';
  195. $children = $c->children('http://schemas.openxmlformats.org/spreadsheetml/2006/main');
  196. if ($t === 'inlineStr' && isset($children->is)) {
  197. $ns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
  198. foreach ($children->is->children($ns) as $ch) {
  199. if ($ch->getName() === 't') {
  200. $val .= (string)$ch;
  201. }
  202. }
  203. if ($val === '' && isset($children->is->t)) {
  204. $val = (string)$children->is->t;
  205. }
  206. } elseif (isset($children->v)) {
  207. $v = (string)$children->v;
  208. if ($t === 's') {
  209. $val = isset($shared[(int)$v]) ? $shared[(int)$v] : '';
  210. } else {
  211. $val = $v;
  212. }
  213. }
  214. if (!isset($grid[$row])) {
  215. $grid[$row] = [];
  216. }
  217. $grid[$row][$col] = $val;
  218. }
  219. if (empty($grid)) {
  220. return ['headers' => [], 'rows' => []];
  221. }
  222. ksort($grid);
  223. $maxRow = max(array_keys($grid));
  224. $maxCol = 0;
  225. foreach ($grid as $cols) {
  226. if (!empty($cols)) {
  227. $maxCol = max($maxCol, max(array_keys($cols)));
  228. }
  229. }
  230. $headers = [];
  231. for ($c = 0; $c <= $maxCol; $c++) {
  232. $headers[$c] = isset($grid[0][$c]) ? trim((string)$grid[0][$c]) : '';
  233. }
  234. $rows = [];
  235. for ($r = 1; $r <= $maxRow; $r++) {
  236. if (!isset($grid[$r])) {
  237. continue;
  238. }
  239. $assoc = [];
  240. for ($c = 0; $c <= $maxCol; $c++) {
  241. $h = $headers[$c];
  242. if ($h === '') {
  243. continue;
  244. }
  245. $assoc[$h] = isset($grid[$r][$c]) ? $grid[$r][$c] : '';
  246. }
  247. $allEmpty = true;
  248. foreach ($assoc as $v) {
  249. if ($v !== '' && $v !== null) {
  250. $allEmpty = false;
  251. break;
  252. }
  253. }
  254. if (!$allEmpty) {
  255. $rows[] = $assoc;
  256. }
  257. }
  258. return ['headers' => array_values($headers), 'rows' => $rows];
  259. }
  260. public static function exportCsvDownload($basename, array $headers, array $list)
  261. {
  262. $filename = preg_replace('/[^a-zA-Z0-9_\-\x{4e00}-\x{9fa5}]/u', '_', $basename) . '.csv';
  263. header('Content-Type: text/csv; charset=UTF-8');
  264. header('Content-Disposition: attachment; filename="' . $filename . '"');
  265. echo "\xEF\xBB\xBF";
  266. $out = fopen('php://output', 'w');
  267. fputcsv($out, $headers);
  268. foreach ($list as $row) {
  269. $line = [];
  270. foreach ($headers as $h) {
  271. $line[] = isset($row[$h]) ? $row[$h] : '';
  272. }
  273. fputcsv($out, $line);
  274. }
  275. fclose($out);
  276. }
  277. public static function exportXlsxDownload($basename, array $headers, array $list)
  278. {
  279. if (!class_exists('ZipArchive')) {
  280. throw new \RuntimeException('zip');
  281. }
  282. $filename = preg_replace('/[^a-zA-Z0-9_\-\x{4e00}-\x{9fa5}]/u', '_', $basename) . '.xlsx';
  283. $tmp = tempnam(sys_get_temp_dir(), 'macxlsx');
  284. if ($tmp === false) {
  285. throw new \RuntimeException('temp');
  286. }
  287. $zip = new \ZipArchive();
  288. if ($zip->open($tmp, \ZipArchive::OVERWRITE | \ZipArchive::CREATE) !== true) {
  289. @unlink($tmp);
  290. throw new \RuntimeException('zip');
  291. }
  292. $zip->addFromString('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  293. . '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
  294. . '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
  295. . '<Default Extension="xml" ContentType="application/xml"/>'
  296. . '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
  297. . '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
  298. . '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
  299. . '</Types>');
  300. $zip->addFromString('_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  301. . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
  302. . '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
  303. . '</Relationships>');
  304. $zip->addFromString('xl/workbook.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  305. . '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
  306. . 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
  307. . '<sheets><sheet name="data" sheetId="1" r:id="rId1"/></sheets></workbook>');
  308. $zip->addFromString('xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  309. . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
  310. . '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
  311. . '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
  312. . '</Relationships>');
  313. $zip->addFromString('xl/styles.xml', '<?xml version="1.0" encoding="UTF-8"?>'
  314. . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
  315. . '<fonts count="1"><font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/></font></fonts>'
  316. . '<fills count="1"><fill><patternFill patternType="none"/></fill></fills>'
  317. . '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
  318. . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
  319. . '<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>'
  320. . '</styleSheet>');
  321. $sheetBody = '<sheetData>';
  322. $rowNum = 1;
  323. $sheetBody .= '<row r="' . $rowNum . '">';
  324. foreach ($headers as $ci => $h) {
  325. $cn = self::colName($ci);
  326. $sheetBody .= '<c r="' . $cn . $rowNum . '" t="inlineStr"><is><t xml:space="preserve">' . self::xmlEsc($h) . '</t></is></c>';
  327. }
  328. $sheetBody .= '</row>';
  329. foreach ($list as $row) {
  330. $rowNum++;
  331. $sheetBody .= '<row r="' . $rowNum . '">';
  332. foreach ($headers as $ci => $h) {
  333. $cn = self::colName($ci);
  334. $v = isset($row[$h]) ? $row[$h] : '';
  335. $sheetBody .= '<c r="' . $cn . $rowNum . '" t="inlineStr"><is><t xml:space="preserve">' . self::xmlEsc($v) . '</t></is></c>';
  336. }
  337. $sheetBody .= '</row>';
  338. }
  339. $sheetBody .= '</sheetData>';
  340. $sheetXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  341. . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
  342. . 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
  343. . $sheetBody . '</worksheet>';
  344. $zip->addFromString('xl/worksheets/sheet1.xml', $sheetXml);
  345. $zip->close();
  346. header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  347. header('Content-Disposition: attachment; filename="' . $filename . '"');
  348. header('Content-Length: ' . filesize($tmp));
  349. readfile($tmp);
  350. @unlink($tmp);
  351. }
  352. }