Каков наилучший способ разбить строку на массив символов 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
Начиная с PHP 7.4 существует функция mb_.   -  person Patryk Godowski    schedule 21.01.2021


Ответы (8)


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

у (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 чтобы перейти к параметру flags». - 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

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

В частности, взгляните на класс Zend_Locale_UTF8_PHP5_String, который читает строки Unicode и для работы с ними разбивает их на отдельные символы (которые, очевидно, могут состоять из нескольких байтов).

EDIT: я только что узнал, что 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