Ответ 1
Используйте preg_split. С "u" модификатор поддерживает Unicode UTF-8.
$chrArray = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
Как выполнить итерацию символа строки 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;
}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;
В ответ на комментарии, отправленные @Pekla и @Col. Шрапнель Я сравнил 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;
}
Используя 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
Вы можете проанализировать каждый байт строки и определить, является ли это одним (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
........
У меня была такая же проблема, как и у 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 когда-либо решает исправить доступ к [] для правильной работы с многобайтовыми значениями, это не удастся. Но и это исправление в первую очередь не понадобилось.
Я думаю, что наиболее эффективным решением было бы работать через строку, используя mb_substr. На каждой итерации цикла mb_substr вызывается дважды (чтобы найти следующий символ и оставшуюся строку). Он перенесет только оставшуюся строку на следующую итерацию. Таким образом, основные накладные расходы на каждой итерации будут искать следующий символ (выполняется дважды), который занимает от одного до пяти или около того операций в зависимости от длины байта символа.
Если это описание неясно, дайте мне знать, и я предоставлю рабочую функцию PHP.