根本原因是MySQL默认utf8(utf8mb3)不支持4字节emoji,须统一使用utf8mb4字符集并确保连接层、表结构、配置文件均生效;过滤emoji应优先用专业库而非简单正则或iconv//IGNORE。

PHP 导入 Excel 时表情符号报错 SQLSTATE[HY000]: General error: 1366 Incorrect string value
这是最典型的症状:Excel 里有 ?、?、?? 这类 emoji,用 fgetcsv 或 PhpSpreadsheet 读取后直接插入 MySQL,就卡在字符集不兼容上。根本原因不是 PHP 读不了 emoji,而是 MySQL 默认的 utf8(实为 utf8mb3)只支持最多 3 字节字符,而大部分 emoji 是 4 字节 UTF-8 编码。
解决前提是数据库和表必须用 utf8mb4 字符集,且连接层也要显式指定:
- 建表语句加
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - PDO 连接时在 DSN 后追加
;charset=utf8mb4,或执行$pdo->exec("SET NAMES utf8mb4") - 确保 MySQL 配置文件中
collation-server = utf8mb4_unicode_ci和character-set-server = utf8mb4已生效
用 preg_replace 过滤 emoji 的实际效果很有限
网上常见写法是 preg_replace('/[\x{1F600}-\x{1F6FF}]/u', '', $str),但问题在于:emoji 范围远不止这个区间,Unicode 15.1 已定义超 3700 个 emoji,还包含组合序列(如 ?? = ? + ZWJ + ?)、变体修饰符(肤色)、国旗(?? 是两个区域指示符字符)——正则很难全覆盖,且容易误杀合法生僻汉字(比如某些 CJK 扩展区字符也在类似码位)。
更务实的做法是「保留可安全入库的字符」而非「穷举过滤」:
立即学习“PHP免费学习笔记(深入)”;
- 用
mb_convert_encoding($str, 'UTF-8', 'UTF-8')先做编码归一化 - 用
mb_strlen($str, 'UTF-8') === mb_strlen(utf8_encode(utf8_decode($str)), 'UTF-8')粗筛含非法字节的字符串(此法对部分边缘 case 不严谨,但够用) - 真正要过滤时,推荐用现成库如
stevegrunwell/emojione的Emoji::remove(),它基于 Unicode 官方 emoji 数据库,维护及时
iconv('UTF-8', 'UTF-8//IGNORE', $str) 会静默丢弃 emoji,慎用
这个技巧看似简单——//IGNORE 模式会让 iconv 跳过无法转换的字节,结果确实是把 emoji 去掉了。但它的问题是:不仅删 emoji,也会删掉所有其他无法映射的 UTF-8 序列(比如损坏的编码、未授权私有区字符),而且不报任何 warning,排查时完全无迹可循。
如果坚持用转换方式处理,更可控的是:
- 先用
mb_check_encoding($str, 'UTF-8')确保输入是合法 UTF-8 - 再用
mb_substr($str, 0, 255, 'UTF-8')截断(避免超字段长度),而不是依赖iconv的暴力截断 - 若业务允许替换而非删除,可用
mb_ereg_replace将 emoji 替换为占位符,如[EMOJI]
PhpSpreadsheet 读取含 emoji 的单元格需设置 setReadDataOnly(true)
默认情况下,PhpSpreadsheet 会解析公式、样式、富文本等元数据,这些额外信息可能引入不可见控制字符或编码异常,加剧 emoji 处理失败。开启 setReadDataOnly(true) 能跳过渲染逻辑,只提取纯文本值,大幅降低出错概率。
示例关键代码:
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($filePath);
$sheet = $spreadsheet->getActiveSheet();
foreach ($sheet->getRowIterator() as $row) {
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
foreach ($cellIterator as $cell) {
$value = $cell->getValue();
// 此时 $value 是干净字符串,可直接 utf8mb4 入库或按需过滤
}
}
真正麻烦的从来不是“怎么删表情”,而是“删完之后字段长度、空格、换行、零宽字符是否还一致”。通信录里一个隐藏的 \u200B(零宽空格)可能让姓名查重失效,比 emoji 本身更难察觉。











