Кой е най-добрият начин за разделяне на низ в масив от Unicode знаци в PHP?

В PHP, кой е най-добрият начин за разделяне на низ в масив от Unicode знаци? Ако входът не е непременно UTF-8?

Искам да знам дали наборът от Unicode знаци във входен низ е подмножество на друг набор от Unicode знаци.

Защо не отидете направо към семейството функции mb_, тъй като първите няколко отговора не го направиха?


person joeforker    schedule 08.09.2009    source източник
comment
Осъзнаваш ли, че сравняването на Unicode знаци е нетривиално, в зависимост от вида на сравнението, което искаш? Например, можете да напишете ü като U+00DC или като U+0075 U+0308.   -  person derobert    schedule 09.09.2009
comment
Да, осъзнавам това. Ако стане проблем, тогава ще трябва да трансформирам входа в една от нормалните форми на Unicode преди разделянето.   -  person joeforker    schedule 09.09.2009
comment
Има функция mb_ от PHP 7.4.   -  person Patryk Godowski    schedule 21.01.2021


Отговори (8)


Можете да използвате модификатора 'u' с PCRE регулярен израз; вижте Модификатори на шаблони (цитиране):

u (PCRE8)

Този модификатор включва допълнителна функционалност на PCRE, която е несъвместима с Perl. Шаблонните низове се третират като UTF-8. Този модификатор е достъпен от PHP 4.1.0 или по-нова версия на Unix и от PHP 4.2.3 на win32. UTF-8 валидността на модела се проверява от PHP 4.3.5.

Например, като се има предвид този код:

header('Content-type: text/html; charset=UTF-8');  // So the browser doesn't make our lives harder
$str = "abc 文字化け, efg";

$results = array();
preg_match_all('/./', $str, $results);
var_dump($results[0]);

Ще получите неизползваем резултат:

array
  0 => string 'a' (length=1)
  1 => string 'b' (length=1)
  2 => string 'c' (length=1)
  3 => string ' ' (length=1)
  4 => string '�' (length=1)
  5 => string '�' (length=1)
  6 => string '�' (length=1)
  7 => string '�' (length=1)
  8 => string '�' (length=1)
  9 => string '�' (length=1)
  10 => string '�' (length=1)
  11 => string '�' (length=1)
  12 => string '�' (length=1)
  13 => string '�' (length=1)
  14 => string '�' (length=1)
  15 => string '�' (length=1)
  16 => string ',' (length=1)
  17 => string ' ' (length=1)
  18 => string 'e' (length=1)
  19 => string 'f' (length=1)
  20 => string 'g' (length=1)

Но с този код:

header('Content-type: text/html; charset=UTF-8');  // So the browser doesn't make our lives harder
$str = "abc 文字化け, efg";

$results = array();
preg_match_all('/./u', $str, $results);
var_dump($results[0]);

(Забележете 'u' в края на регулярния израз)

Получавате това, което искате:

array
  0 => string 'a' (length=1)
  1 => string 'b' (length=1)
  2 => string 'c' (length=1)
  3 => string ' ' (length=1)
  4 => string '文' (length=3)
  5 => string '字' (length=3)
  6 => string '化' (length=3)
  7 => string 'け' (length=3)
  8 => string ',' (length=1)
  9 => string ' ' (length=1)
  10 => string 'e' (length=1)
  11 => string 'f' (length=1)
  12 => string 'g' (length=1)

Надявам се това да помогне :-)

person Pascal MARTIN    schedule 08.09.2009

Малко по-просто от preg_match_all:

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

Това ви връща едноизмерен масив от знаци. Няма нужда от съвпадащ обект.

person mpen    schedule 26.05.2015
comment
Този отговор е този, който има най-голям смисъл, т.е. логично целта е да се раздели, ние не се интересуваме от съпоставянето на всеки отделен знак (въпреки че същото може да се направи във фонов режим). Бях на път да отговоря на въпроса с вашето решение, но с малка разлика: ограничението (3-ти параметър) може да има NULL вместо -1, защото «-1, 0 или NULL означава, че няма ограничение и, както е стандартно в PHP, можете да използвате NULL за преминаване към параметъра за флагове». - person Armfoot; 20.11.2015

Опитайте тази:

preg_match_all('/./u', $text, $array);
person JasonWoof    schedule 08.09.2009

Струва си да се спомене, че от PHP 7.4 има вградена функция, mb_str_split, това прави това.

$chars = mb_str_split($str);

За разлика от preg_split('//u', $str) това поддържа кодировки, различни от UTF-8.

person Ruby Tunaley    schedule 13.12.2020
comment
Така че, нека гласуваме за този отговор. :) @joeforker, може би да проверите този отговор като правилен? - person Patryk Godowski; 21.01.2021

Ако по някаква причина начинът на regex не е достатъчен за вас. Веднъж написах Zend_Locale_UTF8, което е изоставено, но може да ви помогне, ако решите да го направите сами.

По-специално погледнете класа Zend_Locale_UTF8_PHP5_String, който чете низове в Unicode и за да работи с тях, ги разделя на единични символи (които очевидно могат да се състоят от множество байтове).

РЕДАКТИРАНЕ: Току-що повторих, че svn-браузърът на ZF не работи, така че копирах важните методи за удобство:

/**
 * Returns the UTF-8 code sequence as an array for any given $string.
 *
 * @access protected
 * @param string|integer $string
 * @return array
 */
protected function _decode( $string ) {

    $string     = (string) $string;
    $length     = strlen($string);
    $sequence   = array();

    for ( $i=0; $i<$length; ) {
        $bytes      = $this->_characterBytes($string, $i);
        $ord        = $this->_ord($string, $bytes, $i);

        if ( $ord !== false )
            $sequence[] = $ord;

        if ( $bytes === false )
            $i++;
        else
            $i  += $bytes;
    }

    return $sequence;

}

/**
 * Returns the UTF-8 code of a character.
 *
 * @see http://en.wikipedia.org/wiki/UTF-8#Description
 * @access protected
 * @param string $string
 * @param integer $bytes
 * @param integer $position
 * @return integer
 */
protected function _ord( &$string, $bytes = null, $pos=0 )
{
    if ( is_null($bytes) )
        $bytes = $this->_characterBytes($string);

    if ( strlen($string) >= $bytes ) {

        switch ( $bytes ) {
            case 1:
                return ord($string[$pos]);
                break;

            case 2:
                return  ( (ord($string[$pos])   & 0x1f) << 6 ) +
                        ( (ord($string[$pos+1]) & 0x3f) );
                break;

            case 3:
                return  ( (ord($string[$pos])   & 0xf)  << 12 ) + 
                        ( (ord($string[$pos+1]) & 0x3f) << 6 ) +
                        ( (ord($string[$pos+2]) & 0x3f) );
                break;

            case 4:
                return  ( (ord($string[$pos])   & 0x7)  << 18 ) + 
                        ( (ord($string[$pos+1]) & 0x3f) << 12 ) + 
                        ( (ord($string[$pos+1]) & 0x3f) << 6 ) +
                        ( (ord($string[$pos+2]) & 0x3f) );
                break;

            case 0:
            default:
                return false;
        }
    }

    return false;
}
/**
 * Returns the number of bytes of the $position-th character.
 *
 * @see http://en.wikipedia.org/wiki/UTF-8#Description
 * @access protected
 * @param string $string
 * @param integer $position
 */
protected function _characterBytes( &$string, $position = 0 ) {
    $char       = $string[$position];
    $charVal    = ord($char);

    if ( ($charVal & 0x80) === 0 )
        return 1;

    elseif ( ($charVal & 0xe0) === 0xc0 )
        return 2;

    elseif ( ($charVal & 0xf0) === 0xe0 )
        return 3;

    elseif ( ($charVal & 0xf8) === 0xf0)
        return 4;
    /*
    elseif ( ($charVal & 0xfe) === 0xf8 )
        return 5;
    */

    return false;
}
person André Hoffmann    schedule 08.09.2009

function str_split_unicode($str, $l = 0) {
    if ($l > 0) {
        $ret = array();
        $len = mb_strlen($str, "UTF-8");
        for ($i = 0; $i < $len; $i += $l) {
            $ret[] = mb_substr($str, $i, $l, "UTF-8");
        }
        return $ret;
    }
    return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
var_dump(str_split_unicode("لأآأئؤة"));

изход:

array (size=7)
  0 => string 'ل' (length=2)
  1 => string 'أ' (length=2)
  2 => string 'آ' (length=2)
  3 => string 'أ' (length=2)
  4 => string 'ئ' (length=2)
  5 => string 'ؤ' (length=2)
  6 => string 'ة' (length=2)

за повече информация: http://php.net/manual/en/function.str-split.php

person Hussein Feras    schedule 02.02.2019

Успях да напиша решение, използвайки mb_*, включително пътуване до UTF-16 и обратно в вероятно глупав опит да ускоря индексирането на низове:

$japanese2 = mb_convert_encoding($japanese, "UTF-16", "UTF-8");
$length = mb_strlen($japanese2, "UTF-16");
for($i=0; $i<$length; $i++) {
    $char = mb_substr($japanese2, $i, 1, "UTF-16");
    $utf8 = mb_convert_encoding($char, "UTF-8", "UTF-16");
    print $utf8 . "\n";
}

Имах повече късмет, избягвайки mb_internal_encoding и просто уточнявайки всичко при всяко mb_* повикване. Сигурен съм, че ще приключа с решението preg.

person joeforker    schedule 09.09.2009

най-добрият начин за разделяне с дължина: току-що промених функцията laravel str_limit():

    public static function split_text($text, $limit = 100, $end = '')
{
    $width=mb_strwidth($text, 'UTF-8');
    if ($width <= $limit) {
        return $text;
    }
    $res=[];
    for($i=0;$i<=$width;$i=$i+$limit){
        $res[]=rtrim(mb_strimwidth($text, $i, $limit, '', 'UTF-8')).$end;
    }
     return $res;
}
person Solivan    schedule 27.05.2018