UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,由Ken Thompson於1992年創建,現在已經標準化爲RFC 3629。UTF-8用1到4個字節編碼Unicode字符。
那如何判斷一個字符到底是用幾個字節表示的呢,這個從一個UTF-8字符的首字節中就可以判斷,UTF-8編碼規則:如果只有一個字節則其最高二進制位爲0;如果是多字節,其第一個字節從最高位開始,連續的二進制位值爲1的個數決定了其編碼的字節數,其餘各字節均以10開頭。UTF-8轉換表表示如下:
從文件開頭通過每次讀取一個字符的首字節,判斷其中連續的1的個數就可以得知這個字符佔用幾個字節,如果最高位是0,則該字符是個單字節字符(即ASCII碼),然後跳過相應的字節數,再去讀下一個字符的首字節,依此類推,就可以很容易的統計一個文件中的字符數了。
附上代碼:
<?php
class Utf8Counter {
/**
* 使用utf8字符首字節特徵,用以確定單個字符的字節數,utf8編碼的特徵如下
* 1111 110x 6字節
* 1111 10xx 5字節
* 1111 0xxx 4字節 當前的UTF-8編碼一個字符最多用4個字節表示
* 1110 xxxx 3字節,漢字多爲3字節編碼
* 110x xxxx 2字節
* 0xxx xxxx 1字節,兼容ASCII碼
*/
const UTF8_FIRST_BYTE_FEATURE = [
// 0xfc => 6,
// 0xfa => 5,
0xf0 => 4,
0xe0 => 3,
0xc0 => 2,
];
const BUFFER_LEN = 1024;
/**
* 忽略UTF-8文件可能存在的BOM頭
* @param resource $fp
* @return bool
*/
private static function clearBomHeader($fp)
{
// BOM頭是固定的3個字節:0xEF,0xBB,0xBF
if (fread($fp, 3) === "\xEF\xBB\xBF") {
return true;
}
// 把文件指針重新移到文件開頭
rewind($fp);
return false;
}
/**
* 根據UTF-8字符首字節確定該字符的字節數
* @param string $char
* @return int|mixed
*/
public static function getCharByteCount($char)
{
$ascii = ord($char);
if (($ascii & 0x80) == 0) {
return 1;
}
foreach (self::UTF8_FIRST_BYTE_FEATURE as $bits => $byteCount) {
if (($ascii & $bits) == $bits) {
return $byteCount;
}
}
throw new \RuntimeException('非法的UTF-8字符');
}
/**
* @param $string
* @param null $absence 最後一個字符缺少的字節數,例如給定的字符串最後一個字符長度是3字節,但只提供了其中前兩個字節,則該值爲1。
* @return int
*/
public static function getStringCounter($string, &$absence = null)
{
$counter = 0;
$pos = 0;
$len = strlen($string);
if (!$len) {
return 0;
}
while (true) {
$byteCount = self::getCharByteCount($string{$pos});
$pos += $byteCount;
$counter++;
if ($pos >= $len) {
$absence = $pos - $len;
break;
}
}
return $counter;
}
/**
* @param string $file 文件路徑
* @return int
*/
public static function getFileCounter($file)
{
$counter = 0;
if (!is_file($file)) {
throw new \InvalidArgumentException('incorrect file name');
}
$fp = fopen($file, 'rb');
if (!$fp) {
throw new \RuntimeException('請確保文件存在並可讀');
}
self::clearBomHeader($fp);
$absence = 0;
while (!feof($fp)) {
$buf = fread($fp, self::BUFFER_LEN);
if (!$buf) {
break;
}
if ($absence > 0) {
$buf = substr($buf, $absence);
}
$counter += self::getStringCounter($buf, $absence);
}
@fclose($fp);
return $counter;
}
/**
* 統計文件夾中所有文件的字符數總和
* @param string $dir 目錄
* @param string|array $extName 只處理指定的擴展名,支持字符串和數組,'.txt' 或 ['.txt', '.php', '.html', '.md', ...]
* @return int
*/
public static function getDirCounter($dir, $extName = '')
{
$counter = 0;
if (!is_dir($dir)) {
throw new \InvalidArgumentException('不合法的目錄');
}
$dir = rtrim($dir, "/\\");
if ($extName && is_string($extName)) {
$extName = [$extName];
}
$fileArr = scandir($dir);
foreach ($fileArr as $file) {
if ($file == '.' || $file == '..') {
continue;
}
$file = $dir . '/' . $file;
if (is_dir($file)) {
$counter += self::getDirCounter($file, $extName);
} else {
if (!$extName
|| (is_array($extName) && in_array('.' . pathinfo($file, PATHINFO_EXTENSION), $extName))) {
$counter += self::getFileCounter($file);
}
}
}
return $counter;
}
}
調用:
<?php
echo Utf8Counter::getStringCounter('😂😭🙂🐷你好中國😈👹') . PHP_EOL;
echo Utf8Counter::getFileCounter('foo/1112.txt') . PHP_EOL;
echo Utf8Counter::getDirCounter('foo', '.txt') . PHP_EOL;
執行結果:
C:\Users\zhangyi\PhpstormProjects\demo>php utf8wc.php
10
9
34217