mb_detect_encoding() 基於字節特徵猜測,對短文本、純 ASCII、GBK/UTF-8 混雜極易誤判。原文利用 「編碼轉換往返一致性」 原理構建檢測器,是工程上可行的啟發式方案。
若字符串 $str 的真實編碼為 $enc,則:
mb_convert_encoding($str, $enc, $enc) === $str // 恒成立 mb_convert_encoding($str, 'UTF-8', $enc) // 可正確轉為 UTF-8
遍歷候選編碼列表,找到滿足往返一致性的編碼即為真實編碼。
<?php
/**
* 文件編碼檢測與安全讀取
* 支持:UTF-8, GBK/GB2312/GB18030, UTF-16LE/BE, ISO-8859-1, Big5 等
*/
class EncodingDetector
{
/** @var array 候選編碼優先級(常用在前) */
private array $candidates = [
'UTF-8', 'GBK', 'GB2312', 'GB18030',
'UTF-16LE', 'UTF-16BE', 'UTF-32LE', 'UTF-32BE',
'Big5', 'ISO-8859-1', 'ASCII'
];
/** @var int 讀取樣本字節數(大文件無需全讀) */
private int $sampleSize = 8192;
public function __construct(array $customCandidates = [], int $sampleSize = 8192)
{
if ($customCandidates) $this->candidates = $customCandidates;
$this->sampleSize = $sampleSize;
}
/**
* 檢測文件編碼
* @return string|null 編碼名稱或 null(無法確定)
*/
public function detectFile(string $filePath): ?string
{
if (!is_readable($filePath)) return null;
$fp = fopen($filePath, 'rb');
$sample = fread($fp, $this->sampleSize);
fclose($fp);
if ($sample === '') return 'ASCII'; // 空文件
// BOM 優先檢測(最可靠)
$bom = $this->detectBOM($sample);
if ($bom) return $bom;
return $this->detectByConsistency($sample);
}
/**
* 自動讀取並轉為目標編碼(默認 UTF-8)
*/
public function readAs(string $filePath, string $targetEncoding = 'UTF-8'): string
{
$encoding = $this->detectFile($filePath);
if (!$encoding) {
// 兜底:嘗試作為 UTF-8 讀取,替換無效字節
$content = file_get_contents($filePath);
return mb_convert_encoding($content, $targetEncoding, 'UTF-8');
}
$content = file_get_contents($filePath);
return mb_convert_encoding($content, $targetEncoding, $encoding);
}
/** BOM 檢測 */
private function detectBOM(string $sample): ?string
{
$boms = [
"\xEF\xBB\xBF" => 'UTF-8',
"\xFF\xFE\x00\x00" => 'UTF-32LE',
"\x00\x00\xFE\xFF" => 'UTF-32BE',
"\xFF\xFE" => 'UTF-16LE',
"\xFE\xFF" => 'UTF-16BE',
];
foreach ($boms as $bytes => $enc) {
if (str_starts_with($sample, $bytes)) return $enc;
}
return null;
}
/** 一致性檢測核心算法 */
private function detectByConsistency(string $sample): ?string
{
// 先嘗試 UTF-8(最常見)
if ($this->isValidUtf8($sample)) return 'UTF-8';
foreach ($this->candidates as $enc) {
if ($enc === 'UTF-8') continue; // 已檢測
if ($this->checkRoundtrip($sample, $enc)) {
return $enc;
}
}
return null;
}
/** 嚴格 UTF-8 驗證 */
private function isValidUtf8(string $str): bool
{
// 使用 mb_check_encoding (PHP 7.1+) 或正則
return mb_check_encoding($str, 'UTF-8');
}
/** 往返一致性檢查 */
private function checkRoundtrip(string $str, string $enc): bool
{
try {
$converted = mb_convert_encoding($str, $enc, $enc);
// 使用二進制安全比較
return $converted === $str;
} catch (ValueError $e) {
return false; // 不支持的編碼或非法字節序列
}
}
}
// ===== 使用示例 =====
$detector = new EncodingDetector();
// 1. 檢測單文件
$file = 'data.csv';
$enc = $detector->detectFile($file);
echo "檢測編碼: " . ($enc ?? '未知') . "\n";
// 2. 直接讀取為 UTF-8
$content = $detector->readAs($file, 'UTF-8');
echo "前 200 字符: " . mb_substr($content, 0, 200) . "\n";
// 3. 批量處理目錄
$dir = new RecursiveDirectoryIterator('/path/to/files');
foreach (new RecursiveIteratorIterator($dir) as $file) {
if ($file->isFile() && $file->getExtension() === 'csv') {
$content = $detector->readAs($file->getPathname());
// 導入數據庫等後續處理...
}
}
parallel 擴展或進程池finfo_file 排除圖片/視頻等非文本文件極少數文件不同行/段使用不同編碼(如日誌文件),需逐行檢測:
function readMixedEncoding(string $filePath): Generator
{
$fp = fopen($filePath, 'rb');
$detector = new EncodingDetector();
while (($line = fgets($fp)) !== false) {
$enc = $detector->detectByConsistency($line) ?? 'UTF-8';
yield mb_convert_encoding($line, 'UTF-8', $enc);
}
fclose($fp);
}
掌握「BOM 優先 → UTF-8 嚴格驗證 → 候選編碼往返驗證」三級檢測策略,能解決 99% 的中文編碼亂碼問題。