Как итерации символа строки 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
?
Используйте preg_split . С модификатором «u» он поддерживает Unicode UTF-8.
$chrArray = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
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; }elseif($char < 248){ $bytes = 4; }elseif($char == 252){ $bytes = 5; }else{ $bytes = 6; } $str = substr($string, $pointer, $bytes); $pointer += $bytes; return $str; } }
Это я использовал для циклического преобразования многобайтового строкового символа с помощью 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;
В ответ на комментарии, отправленные @Pekla и @Col. Шрапнель Я сравнил preg_split
с mb_substr
.
На изображении показано, что preg_split
занимает 1.2 mb_substr
, а mb_substr
почти 25 mb_substr
.
Вот код функций:
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; }
Используя замечательную функцию 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
Вы можете проанализировать каждый байт строки и определить, является ли это одним (ASCII) символом или началом многобайтового символа :
Кодировка UTF-8 имеет переменную ширину, каждый символ представлен от 1 до 4 байтов. Каждый байт имеет 0-4 ведущих последовательных '1' бита, за которым следует бит '0', чтобы указать его тип. 2 или более бит «1» указывает первый байт в последовательности этого количества байтов.
вы должны пройти через строку и вместо увеличения позиции на 1, прочитать текущий символ в полном объеме, а затем увеличить позицию на длину, что характер.
Статья Википедии содержит таблицу интерпретации для каждого символа [retrieved 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 ........
У меня была такая же проблема, как и у 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
получает правильное значение в этом случае. Мне пришлось добавить второй указатель. Один отслеживает многобайтовый символ в строке, другой отслеживает однобайтовый символ. mb_substr
значение используется для substr
(поскольку на самом деле это mb_substr
), а однобайтное значение используется для извлечения байта следующим образом: $string[$index]
.
Очевидно, что если PHP когда-либо решает исправить доступ [] к работе с многобайтовыми значениями, это не удастся. Но также, это исправление не понадобилось бы в первую очередь.
Я думаю, что наиболее эффективным решением было бы работать через строку, используя mb_substr. На каждой итерации цикла mb_substr вызывается дважды (чтобы найти следующий символ и оставшуюся строку). Он перенесет только оставшуюся строку на следующую итерацию. Таким образом, основные накладные расходы на каждой итерации будут искать следующий символ (выполняется дважды), который занимает от одного до пяти или около того операций в зависимости от длины байта символа.
Если это описание неясно, дайте мне знать, и я предоставлю рабочую функцию PHP.