PHP統計UTF-8編碼文件中的字符數

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

 

本人目前正在翻譯MySQL8.0官方文檔,感興趣的可以來看看,歡迎star和fork:

https://github.com/zhyee/Mysql8.0_Reference_Manual_Translation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章