核心问题是CSV/Excel编码与PHP解析不匹配:Windows下Excel默认UTF-8 with BOM,fgetcsv不自动剥离BOM导致首列乱码;PhpSpreadsheet需显式设setReadDataOnly(true)和setLoadSheetsOnly()防OOM,.xls文件须用Excel5 reader。

PHP 导入 Excel 文件时中文乱码怎么解决
核心问题不是 PHP 本身,而是 fgetcsv、str_getcsv 或第三方库(如 PhpSpreadsheet)读取时没正确识别 BOM 和编码。Windows 下 Excel 默认保存为 UTF-8 with BOM,但 PHP 的 fopen + fgetcsv 不会自动剥离 BOM,导致第一列字段名或首行数据开头多出不可见字符,后续 mb_convert_encoding 处理失效。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
file_get_contents先读文件头 3 字节,判断是否为\xEF\xBB\xBF(UTF-8 BOM),是则用substr($content, 3)截掉再写回临时文件或传给str_getcsv - PhpSpreadsheet 中必须显式设置读取编码:
$reader->setReadDataOnly(true); $reader->setLoadAllSheets();,并确保IOFactory::load()前不手动mb_internal_encoding('GBK')—— 它内部已按 Excel 自带编码解析 - 若上传的是 .xls(非 .xlsx),别用
XmlReader方式加载,改用Excel5reader,否则中文字段名直接变空
用 PhpSpreadsheet 导出万级数据内存爆掉怎么办
默认方式是「全量加载 → 写入内存 → 一次性输出」,1 万行 × 20 列的 Excel 就可能吃掉 200MB+ 内存。关键不是换库,而是切换到「流式写入」模式。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 不用
Spreadsheet实例,改用Writer\Xlsx\Writer+Worksheet的流式接口:$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Writer($spreadsheet);不行,得用\PhpOffice\PhpSpreadsheet\Writer\Xlsx并配合setPreCalculateFormulas(false) - 逐行写入:用
$sheet->fromArray($row, NULL, "A{$currentRow}")替代$sheet->setCellValue()单元格赋值,减少对象创建开销 - 导出前调用
$spreadsheet->garbageCollect(),尤其在循环中每写 1000 行后执行一次 - 避免在循环里调用
$spreadsheet->getActiveSheet()->getStyle(...)—— 样式应最后统一设置,或用setStyleArray批量应用
CSV 导入时数值被自动转成科学计数法(如 123456789012 → 1.23E+11)
这不是 PHP 的错,是 Excel/Numbers 打开 CSV 时自动格式化导致的显示问题。原始 CSV 文件里存的仍是纯数字字符串,但用户双击打开就「被转换」了。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 导出 CSV 时对数字字段加英文双引号 + 前缀单引号:
"'123456789012",Excel 解析时会保留为文本 - 更稳妥的做法是统一字段类型:导出前把所有可能超长的字段(手机号、订单号、身份证号)强制转为字符串:
(string)$value,再用fputcsv($fp, $row, ',', '"') - 如果必须支持 Excel 直接打开且保持数值可计算,改用 XLSX 格式导出,并为对应列设置单元格格式:
$sheet->getStyle("A2:A1000")->getNumberFormat()->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT);
导入过程中用户刷新页面导致任务中断,如何支持断点续传
PHP 默认没有跨请求状态保持机制,靠 session 或数据库记录进度即可实现,难点在于「如何定义断点粒度」和「避免重复写入」。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 不要以「行号」为断点,而以「唯一业务键」(如订单号、手机号)为准 —— 因为 CSV 行序可能变动,但业务主键不变
- 导入前先查数据库已有主键集合:
$exists = array_column($pdo->query("SELECT mobile FROM user WHERE mobile IN (".str_repeat('?,', count($batch)-1).'?)')->fetchAll(), 'mobile');,再过滤掉已存在记录 - 每次处理完一个批次(如 500 行),把当前最大主键或时间戳写入 Redis:
$redis->set("import:task_{$taskId}:last_key", $lastMobile);,下次从该位置 SELECT ... WHERE mobile > ? 开始 - 别依赖
ignore_user_abort(true)—— 它只防客户端断开,不防 PHP 超时;务必配合set_time_limit(0)和 Nginx 的fastcgi_read_timeout
setReadDataOnly(true) 必须配 setLoadSheetsOnly(['Sheet1']),否则照样 OOM。



















