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

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

38 голосов
/ 08 сентября 2010

Как перебрать строковый символ в кодировке 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?

Ответы [ 7 ]

56 голосов
/ 08 сентября 2010

Используйте preg_split . С модификатором "u" он поддерживает кодировку UTF-8.

$chrArray = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
32 голосов
/ 16 января 2013

Прерывание разбиения завершится неудачей на очень больших строках с исключением памяти , а 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;
22 голосов
/ 08 сентября 2010

В ответ на комментарии, опубликованные @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;
}
8 голосов
/ 28 ноября 2016

Используя 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'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]);
    }
}

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

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
3 голосов
/ 08 сентября 2010

Вы можете проанализировать каждый байт строки и определить, является ли он одиночным (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
  ........
2 голосов
/ 18 июня 2013

У меня была та же проблема, что и у 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 получает правильное значение в этом случае.Мне пришлось добавить второй указатель.Один отслеживает многобайтовый символ в строке, другой отслеживает однобайтовый символ.Многобайтовое значение используется для substr (поскольку оно на самом деле mb_substr), в то время как однобайтовое значение используется для извлечения байта следующим образом: $string[$index].

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

1 голос
/ 01 марта 2016

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...