如何在PHP中迭代UTF-8字符串?

如何在PHP中迭代UTF-8字符串?,php,utf-8,Php,Utf 8,如何使用索引逐个字符迭代UTF-8字符串 使用括号运算符访问UTF-8字符串时,UTF编码字符由2个或更多元素组成 例如: $str = "Kąt"; $str[0] = "K"; $str[1] = "�"; $str[2] = "�"; $str[3] = "t"; 但我想: $str[0] = "K"; $str[1] = "ą"; $str[2] = "t"; 使用mb_substr是可能的,但速度非常慢,即 mb_substr($str, 0, 1) = "K" mb_substr

如何使用索引逐个字符迭代UTF-8字符串

使用括号运算符访问UTF-8字符串时,UTF编码字符由2个或更多元素组成

例如:

$str = "Kąt";
$str[0] = "K";
$str[1] = "�";
$str[2] = "�";
$str[3] = "t";
但我想:

$str[0] = "K";
$str[1] = "ą";
$str[2] = "t";
使用
mb_substr
是可能的,但速度非常慢,即

mb_substr($str, 0, 1) = "K"
mb_substr($str, 1, 1) = "ą"
mb_substr($str, 2, 1) = "t"
是否有其他方法可以在不使用
mb_substr
的情况下逐个字符交互字符串?

使用。它支持UTF-8 unicode

$chrArray = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);

您可以分析字符串的每个字节,并确定它是单个(ASCII)字符还是:

UTF-8编码是可变宽度的,每个字符由1到4个字节表示。每个字节有0–4个前导连续的“1”位,后跟一个“0”位以指示其类型。2个或更多的“1”位表示多个字节序列中的第一个字节

您可以遍历字符串,而不是将位置增加1,而是完整读取当前字符,然后将位置增加该字符的长度

维基百科文章为每个字符提供了解释表:


在回答@Pekla和@Col.Shrapnel发表的评论时,我将
preg_split
mb_substr
进行了比较

图像显示,
preg_split
花费了1.2秒,而
mb_substr
几乎花费了25秒

以下是函数的代码:

function split_preg($str){
    return preg_split('//u', $str, -1);     
}

function split_mb($str){
    $length = mb_strlen($str);
    $chars = array();
    for ($i=0; $i<$length; $i++){
        $chars[] = mb_substr($str, $i, 1);
    }
    $chars[] = "";
    return $chars;
}
函数拆分预处理($str){
返回preg_split('//u',$str,-1);
}
函数拆分\u mb($str){
$length=mb_strlen($str);
$chars=array();

对于($i=0;$iPreg-split将故障转移非常大的字符串,并出现内存异常,mb_-substr确实很慢,因此这里有一个简单有效的代码,我相信您可以使用:

function nextchar($string, &$pointer){
    if(!isset($string[$pointer])) return false;
    $char = ord($string[$pointer]);
    if($char < 128){
        return $string[$pointer++];
    }else{
        if($char < 224){
            $bytes = 2;
        }elseif($char < 240){
            $bytes = 3;
        }else{
            $bytes = 4;
        }
        $str =  substr($string, $pointer, $bytes);
        $pointer += $bytes;
        return $str;
    }
}
使用它将字符串与下面的代码循环10000次,第一个代码的运行时间为3秒,第二个代码的运行时间为13秒:

function microtime_float(){
    list($usec, $sec) = explode(' ', microtime());
    return ((float)$usec + (float)$sec);
}

$source = 'árvíztűrő tükörfúrógépárvíztűrő tükörfúrógépárvíztűrő tükörfúrógépárvíztűrő tükörfúrógépárvíztűrő tükörfúrógép';

$t = Array(
    0 => microtime_float()
);

for($i = 0; $i < 10000; $i++){
    $pointer = 0;
    while(($chr = nextchar($source, $pointer)) !== false){
        //echo $chr;
    }
}

$t[] = microtime_float();

echo $t[1] - $t[0].PHP_EOL.PHP_EOL;
函数microtime\u float(){
列表($usec,$sec)=爆炸('',微时间());
返回((浮动)$usec+(浮动)$sec);
}
$source='rvíztűrőtükörógépárv ztűrőtükörórórárárírőtükörórórérépárórérétürórőrórérérépáréróréréréréréréréréréréréréréréréréréréréréréré;
$t=数组(
0=>microtime_float()
);
对于($i=0;$i<10000;$i++){
$pointer=0;
while($chr=nextchar($source,$pointer))!==false){
//echo$chr;
}
}
$t[]=微时间浮点数();
echo$t[1]-$t[0].PHP_EOL.PHP_EOL;

我遇到了与OP相同的问题,我试图避免PHP中的正则表达式,因为它会失败,甚至会因长字符串而崩溃。我使用了一些更改,因为我将
mbstring.func_重载设置为7

function nextchar($string, &$pointer, &$asciiPointer){
   if(!isset($string[$asciiPointer])) return false;
    $char = ord($string[$asciiPointer]);
    if($char < 128){
        $pointer++;
        return $string[$asciiPointer++];
    }else{
        if($char < 224){
            $bytes = 2;
        }elseif($char < 240){
            $bytes = 3;
        }elseif($char < 248){
            $bytes = 4;
        }elseif($char = 252){
            $bytes = 5;
        }else{
            $bytes = 6;
        }
        $str =  substr($string, $pointer++, 1);
        $asciiPointer+= $bytes;
        return $str;
    }
}
函数nextchar($string,&$pointer,&$asciiPointer){
如果(!isset($string[$asciiPointer])返回false;
$char=ord($string[$asciiPointer]);
如果($char<128){
$pointer++;
返回$string[$asciiPointer++];
}否则{
如果($char<224){
$bytes=2;
}elseif($char<240){
$bytes=3;
}elseif($char<248){
$bytes=4;
}elseif($char=252){
$bytes=5;
}否则{
$bytes=6;
}
$str=substr($string,$pointer++,1);
$asciiPointer+=$bytes;
返回$str;
}
}
mbstring.func_重载设置为7时,
substr
实际上调用
mb_substr
。因此
substr
在这种情况下获得了正确的值。我必须添加第二个指针。一个指针跟踪字符串中的多字节字符,另一个指针跟踪单字节字符。多字节值用于
substr
(因为它实际上是
mb_substr
),而单字节值用于以这种方式检索字节:
$string[$index]


显然,如果PHP决定修复[]访问以正确使用多字节值,这将失败。但是,这个修复从一开始就不需要。

我认为最有效的解决方案是使用mb_substr处理字符串。在循环的每次迭代中,mb_substr将被调用两次(查找下一个字符和剩余的字符串)。它将只将剩余的字符串传递给下一次迭代。这样,每次迭代的主要开销将是查找下一个字符(完成两次),这只需要一到五次左右的操作,具体取决于字符的字节长度

如果这个描述不清楚,请告诉我,我将提供一个可用的PHP函数。

以“奇妙的函数”为灵感,我创建了一个多字节字符串迭代器类

// Multi-Byte String iterator class
class MbStrIterator implements Iterator
{
    private $iPos   = 0;
    private $iSize  = 0;
    private $sStr   = null;

    // Constructor
    public function __construct(/*string*/ $str)
    {
        // Save the string
        $this->sStr     = $str;

        // Calculate the size of the current character
        $this->calculateSize();
    }

    // Calculate size
    private function calculateSize() {

        // If we're done already
        if(!isset($this->sStr[$this->iPos])) {
            return;
        }

        // Get the character at the current position
        $iChar  = ord($this->sStr[$this->iPos]);

        // If it's a single byte, set it to one
        if($iChar < 128) {
            $this->iSize    = 1;
        }

        // Else, it's multi-byte
        else {

            // Figure out how long it is
            if($iChar < 224) {
                $this->iSize = 2;
            } else if($iChar < 240){
                $this->iSize = 3;
            } else if($iChar < 248){
                $this->iSize = 4;
            } else if($iChar == 252){
                $this->iSize = 5;
            } else {
                $this->iSize = 6;
            }
        }
    }

    // Current
    public function current() {

        // If we're done
        if(!isset($this->sStr[$this->iPos])) {
            return false;
        }

        // Else if we have one byte
        else if($this->iSize == 1) {
            return $this->sStr[$this->iPos];
        }

        // Else, it's multi-byte
        else {
            return substr($this->sStr, $this->iPos, $this->iSize);
        }
    }

    // Key
    public function key()
    {
        // Return the current position
        return $this->iPos;
    }

    // Next
    public function next()
    {
        // Increment the position by the current size and then recalculate
        $this->iPos += $this->iSize;
        $this->calculateSize();
    }

    // Rewind
    public function rewind()
    {
        // Reset the position and size
        $this->iPos     = 0;
        $this->calculateSize();
    }

    // Valid
    public function valid()
    {
        // Return if the current position is valid
        return isset($this->sStr[$this->iPos]);
    }
}
哪个会输出

K
ą
t
0: K
1: ą
3: t
或者如果你真的想知道起始字节的位置

foreach(new MbStrIterator("Kąt") as $i => $c) {
    echo "{$i}: {$c}\n";
}
哪个会输出

K
ą
t
0: K
1: ą
3: t

定义“极其缓慢”。您是否分析了您的应用程序并发现这些mb_substr调用是一个瓶颈?在第二次阅读您的问题后,我意识到您想要一种不使用mb_substr的方法。我已删除了我的答案。@Col.Shrapnel:是的,50%的处理时间是由
mb_substr
完成的。整个用户对web服务器的请求中,有50%的处理时间是由
mb_substr
完成的ver,从连接到断开连接?我简直不敢相信。你的整个脚本在每个请求上都以相同的方式进行解析。没有人注意到这一点。你的mb解析占用了整个请求时间的哪一部分?我很惊讶没有其他人提出这一点,但是如果你想要最快的解决方案,并且可以承受高达4倍的字符串内存开销,那么u每个4字节的固定宽度字符-如果您需要随机访问字符串中的任何字符,这可能是最有效的解决方案,除非您处理非常大的文件,否则内存开销可能是可以接受的。这非常优雅,但我很难想象这比
mb_substr()更快
@Pekka可能是这样。使用
mb_substr
对str的长度是二次的
0: K
1: ą
3: t