Как перебирать строку UTF-8 в PHP?

Как выполнить итерацию символа строки UTF-8 символом с помощью индексации?

При доступе к строке UTF-8 с оператором скобки $str[0] символ, закодированный в 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?

Ответ 2

Preg 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;
    }
}

Это я использовал для циклического прохождения многобайтовой строки char по char, и если я изменил его на код ниже, разница в производительности огромна:

function nextchar($string, &$pointer){
    if(!isset($string[$pointer])) return false;
    return mb_substr($string, $pointer++, 1, 'UTF-8');
}

Использование его для зацикливания строки 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;

Ответ 3

В ответ на комментарии, отправленные @Pekla и @Col. Шрапнель Я сравнил preg_split с mb_substr.

alt text

Изображение показывает, что 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;
}

Ответ 4

Используя Lajos Meszaros 'замечательная функция в качестве вдохновения, я создал многобайтовый класс итераторов строк.

// 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 a single byte, set it to one
        if($iChar < 128) {
            $this->iSize    = 1;
        }

        // Else, it 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 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]);
    }
}

Его можно использовать так

foreach(new MbStrIterator("Kąt") as $c) {
    echo "{$c}\n";
}

Который выведет

K
ą
t

Или, если вы действительно хотите узнать положение стартового байт, а также

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

Который выведет

0: K
1: ą
3: t

Ответ 5

Вы можете проанализировать каждый байт строки и определить, является ли это одним (ASCII) символом или началом многобайтового символа:

Кодировка UTF-8 имеет переменную ширину, каждый символ представлен от 1 до 4 байтов. Каждый байт имеет 0-4 ведущих последовательных '1' бита, за которым следует бит '0', чтобы указать его тип. 2 или более бит "1" указывает первый байт в последовательности этого количества байтов.

вы бы прошли через строку и вместо увеличения позиции на 1, прочитали текущий символ в полном объеме, а затем увеличьте позицию на длину, указанную символом.

В статье в Википедии есть таблица интерпретации для каждого символа [извлечено 2010-10-01]:

   0-127 Single-byte encoding (compatible with US-ASCII)
 128-191 Second, third, or fourth byte of a multi-byte sequence
 192-193 Overlong encoding: start of 2-byte sequence, 
         but would encode a code point ≤ 127
  ........

Ответ 6

У меня была такая же проблема, как и у OP, и я стараюсь избегать регулярного выражения в PHP, поскольку он терпит неудачу или даже падает с длинными строками. Я использовал ответ Mészáros Lajos с некоторыми изменениями, так как у меня mbstring.func_overload установлено значение 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;
    }
}

С mbstring.func_overload, установленным в 7, substr на самом деле вызывает mb_substr. Таким образом, substr получает правильное значение в этом случае. Мне пришлось добавить второй указатель. Один отслеживает многобайтовый char в строке, другой отслеживает однобайтный char. Многобайтовое значение используется для substr (так как оно фактически mb_substr), а однобайтное значение используется для извлечения байта следующим образом: $string[$index].

Очевидно, если PHP когда-либо решает исправить доступ к [] для правильной работы с многобайтовыми значениями, это не удастся. Но и это исправление в первую очередь не понадобилось.

Ответ 7

Я думаю, что наиболее эффективным решением было бы работать через строку, используя mb_substr. На каждой итерации цикла mb_substr вызывается дважды (чтобы найти следующий символ и оставшуюся строку). Он перенесет только оставшуюся строку на следующую итерацию. Таким образом, основные накладные расходы на каждой итерации будут искать следующий символ (выполняется дважды), который занимает от одного до пяти или около того операций в зависимости от длины байта символа.

Если это описание неясно, дайте мне знать, и я предоставлю рабочую функцию PHP.