Многобайтовая безопасная функция wordwrap() для UTF-8

Функция PHP wordwrap() работает неправильно для многобайтовых строк, таких как UTF-8.

В комментариях есть несколько примеров безопасных функций mb, но с некоторыми разными тестовыми данными все они, похоже, имеют некоторые проблемы.

Функция должна принимать те же параметры, что и wordwrap().

В частности, убедитесь, что он работает для:

  • вырезать середину слова, если $cut = true, иначе не вырезать середину слова
  • не вставлять лишние пробелы в слова, если $break = ' '
  • также работаю на $break = "\n"
  • работать для ASCII и всех допустимых UTF-8

person philfreo    schedule 29.09.2010    source источник
comment
Два метода s($str)->truncate($length, $break) и < a href="https://github.com/delight-im/PHP-Str/blob/8fd0c608d5496d43adaa899642c1cce047e076dc/src/Str.php#L246" rel="nofollow noreferrer">s($str)->truncateSafely($length, $break) сделать именно это, как найдено в этой отдельной библиотеке. Первый для $cut = true, а второй для $cut = false. Они безопасны для Юникода.   -  person caw    schedule 27.07.2016


Ответы (8)


Я не нашел ни одного рабочего кода для меня. Вот что я написал. Для меня это работает, хотя, возможно, это не самое быстрое.

function mb_wordwrap($str, $width = 75, $break = "\n", $cut = false) {
    $lines = explode($break, $str);
    foreach ($lines as &$line) {
        $line = rtrim($line);
        if (mb_strlen($line) <= $width)
            continue;
        $words = explode(' ', $line);
        $line = '';
        $actual = '';
        foreach ($words as $word) {
            if (mb_strlen($actual.$word) <= $width)
                $actual .= $word.' ';
            else {
                if ($actual != '')
                    $line .= rtrim($actual).$break;
                $actual = $word;
                if ($cut) {
                    while (mb_strlen($actual) > $width) {
                        $line .= mb_substr($actual, 0, $width).$break;
                        $actual = mb_substr($actual, $width);
                    }
                }
                $actual .= ' ';
            }
        }
        $line .= trim($actual);
    }
    return implode($break, $lines);
}
person Fosfor    schedule 14.02.2011
comment
У меня тоже хорошо получилось! - person Ben Sinclair; 23.10.2014
comment
Пользуюсь несколько лет, но не активно. В любом случае, я включил эту функцию в класс php, который я поместил в GitHub под MIT, и мне просто нужно убедиться, что все в порядке - noreferrer">gist.github.com/AliceWonderMiscreations/ - person Alice Wonder; 26.12.2017
comment
пробовал этот код с PHP 5.6 и у меня не работал =( Требуется установить ini_set и mb_internal_encoding? - person Marcelo Bezerra bovino; 25.01.2018
comment
@AliceWonder Ссылку больше не нашел, но в целом без проблем :) - person Fosfor; 22.05.2018

/**
 * wordwrap for utf8 encoded strings
 *
 * @param string $str
 * @param integer $len
 * @param string $what
 * @return string
 * @author Milian Wolff <[email protected]>
 */

function utf8_wordwrap($str, $width, $break, $cut = false) {
    if (!$cut) {
        $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){'.$width.',}\b#U';
    } else {
        $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){'.$width.'}#';
    }
    if (function_exists('mb_strlen')) {
        $str_len = mb_strlen($str,'UTF-8');
    } else {
        $str_len = preg_match_all('/[\x00-\x7F\xC0-\xFD]/', $str, $var_empty);
    }
    $while_what = ceil($str_len / $width);
    $i = 1;
    $return = '';
    while ($i < $while_what) {
        preg_match($regexp, $str,$matches);
        $string = $matches[0];
        $return .= $string.$break;
        $str = substr($str, strlen($string));
        $i++;
    }
    return $return.$str;
}

Общее время: 0.0020880699 хорошее время :)

person sacrebleu    schedule 10.02.2011
comment
Если не $cut, эта функция имеет недостаток. Он не будет переноситься раньше, если это возможно (что сделал бы wordwrap. См. демонстрацию. Не решение, но связанный ответ имеет другой Регулярное выражение переноса слов. - person hakre; 13.12.2011
comment
Это поведение отличается от wordwrap() в отношении пробелов. - person mpyw; 10.08.2013
comment
Это работает, когда cut=true для упрощенного китайского текста - person exiang; 19.07.2015
comment
Это не работает для кириллицы. Разбивает слова. Не стал искать причину, попробуем другое решение. - person Aleksey Kuznetsov; 01.05.2019

Поскольку ни один ответ не обрабатывал каждый вариант использования, вот что. Код основан на DrupalAbstractStringWrapper::wordWrap.

<?php

/**
 * Wraps any string to a given number of characters.
 *
 * This implementation is multi-byte aware and relies on {@link
 * http://www.php.net/manual/en/book.mbstring.php PHP's multibyte
 * string extension}.
 *
 * @see wordwrap()
 * @link https://api.drupal.org/api/drupal/core%21vendor%21zendframework%21zend-stdlib%21Zend%21Stdlib%21StringWrapper%21AbstractStringWrapper.php/function/AbstractStringWrapper%3A%3AwordWrap/8
 * @param string $string
 *   The input string.
 * @param int $width [optional]
 *   The number of characters at which <var>$string</var> will be
 *   wrapped. Defaults to <code>75</code>.
 * @param string $break [optional]
 *   The line is broken using the optional break parameter. Defaults
 *   to <code>"\n"</code>.
 * @param boolean $cut [optional]
 *   If the <var>$cut</var> is set to <code>TRUE</code>, the string is
 *   always wrapped at or before the specified <var>$width</var>. So if
 *   you have a word that is larger than the given <var>$width</var>, it
 *   is broken apart. Defaults to <code>FALSE</code>.
 * @return string
 *   Returns the given <var>$string</var> wrapped at the specified
 *   <var>$width</var>.
 */
function mb_wordwrap($string, $width = 75, $break = "\n", $cut = false) {
  $string = (string) $string;
  if ($string === '') {
    return '';
  }

  $break = (string) $break;
  if ($break === '') {
    trigger_error('Break string cannot be empty', E_USER_ERROR);
  }

  $width = (int) $width;
  if ($width === 0 && $cut) {
    trigger_error('Cannot force cut when width is zero', E_USER_ERROR);
  }

  if (strlen($string) === mb_strlen($string)) {
    return wordwrap($string, $width, $break, $cut);
  }

  $stringWidth = mb_strlen($string);
  $breakWidth = mb_strlen($break);

  $result = '';
  $lastStart = $lastSpace = 0;

  for ($current = 0; $current < $stringWidth; $current++) {
    $char = mb_substr($string, $current, 1);

    $possibleBreak = $char;
    if ($breakWidth !== 1) {
      $possibleBreak = mb_substr($string, $current, $breakWidth);
    }

    if ($possibleBreak === $break) {
      $result .= mb_substr($string, $lastStart, $current - $lastStart + $breakWidth);
      $current += $breakWidth - 1;
      $lastStart = $lastSpace = $current + 1;
      continue;
    }

    if ($char === ' ') {
      if ($current - $lastStart >= $width) {
        $result .= mb_substr($string, $lastStart, $current - $lastStart) . $break;
        $lastStart = $current + 1;
      }

      $lastSpace = $current;
      continue;
    }

    if ($current - $lastStart >= $width && $cut && $lastStart >= $lastSpace) {
      $result .= mb_substr($string, $lastStart, $current - $lastStart) . $break;
      $lastStart = $lastSpace = $current;
      continue;
    }

    if ($current - $lastStart >= $width && $lastStart < $lastSpace) {
      $result .= mb_substr($string, $lastStart, $lastSpace - $lastStart) . $break;
      $lastStart = $lastSpace = $lastSpace + 1;
      continue;
    }
  }

  if ($lastStart !== $current) {
    $result .= mb_substr($string, $lastStart, $current - $lastStart);
  }

  return $result;
}

?>
person Fleshgrinder    schedule 14.08.2013
comment
Отлично работает для кириллических слов в UTF-8. - person Aleksey Kuznetsov; 01.05.2019

Пользовательские границы слов

Текст Unicode имеет гораздо больше потенциальных границ слов, чем 8-битные кодировки, включая 17. пробелы и запятая во всю ширину . Это решение позволяет настроить список границ слов для вашего приложения.

Лучшая производительность

Вы когда-нибудь тестировали mb_* семейство встроенных модулей PHP? Они вообще плохо масштабируются. Используя пользовательский nextCharUtf8(), мы можем выполнять ту же работу, но на несколько порядков быстрее, особенно для больших строк.

<?php

function wordWrapUtf8(
  string $phrase,
  int $width = 75,
  string $break = "\n",
  bool $cut = false,
  array $seps = [' ', "\n", "\t", ',']
): string
{
  $chunks = [];
  $chunk = '';
  $len = 0;
  $pointer = 0;
  while (!is_null($char = nextCharUtf8($phrase, $pointer))) {
    $chunk .= $char;
    $len++;
    if (in_array($char, $seps, true) || ($cut && $len === $width)) {
      $chunks[] = [$len, $chunk];
      $len = 0;
      $chunk = '';
    }
  }
  if ($chunk) {
    $chunks[] = [$len, $chunk];
  }
  $line = '';
  $lines = [];
  $lineLen = 0;
  foreach ($chunks as [$len, $chunk]) {
    if ($lineLen + $len > $width) {
      if ($line) {
        $lines[] = $line;
        $lineLen = 0;
        $line = '';
      }
    }
    $line .= $chunk;
    $lineLen += $len;
  }
  if ($line) {
    $lines[] = $line;
  }
  return implode($break, $lines);
}

function nextCharUtf8(&$string, &$pointer)
{
  // EOF
  if (!isset($string[$pointer])) {
    return null;
  }

  // Get the byte value at the pointer
  $char = ord($string[$pointer]);

  // ASCII
  if ($char < 128) {
    return $string[$pointer++];
  }

  // UTF-8
  if ($char < 224) {
    $bytes = 2;
  } elseif ($char < 240) {
    $bytes = 3;
  } elseif ($char < 248) {
    $bytes = 4;
  } elseif ($char == 252) {
    $bytes = 5;
  } else {
    $bytes = 6;
  }

  // Get full multibyte char
  $str = substr($string, $pointer, $bytes);

  // Increment pointer according to length of char
  $pointer += $bytes;

  // Return mb char
  return $str;
}
person jchook    schedule 25.11.2019

Просто хочу поделиться альтернативой, которую я нашел в сети.

<?php
if ( !function_exists('mb_str_split') ) {
    function mb_str_split($string, $split_length = 1)
    {
        mb_internal_encoding('UTF-8'); 
        mb_regex_encoding('UTF-8');  

        $split_length = ($split_length <= 0) ? 1 : $split_length;

        $mb_strlen = mb_strlen($string, 'utf-8');

        $array = array();

        for($i = 0; $i < $mb_strlen; $i += $split_length) {
            $array[] = mb_substr($string, $i, $split_length);
        }

        return $array;
    }
}

Используя mb_str_split, вы можете использовать join для объединения слов с <br>.

<?php
    $text = '<utf-8 content>';

    echo join('<br>', mb_str_split($text, 20));

И, наконец, создайте своего собственного помощника, возможно, mb_textwrap

<?php

if( !function_exists('mb_textwrap') ) {
    function mb_textwrap($text, $length = 20, $concat = '<br>') 
    {
        return join($concat, mb_str_split($text, $length));
    }
}

$text = '<utf-8 content>';
// so simply call
echo mb_textwrap($text);

См. демонстрационный скриншот: демонстрация mb_textwrap

person jdme    schedule 12.09.2018

Вот функция многобайтового переноса слов, которую я закодировал, вдохновившись другими, найденными в Интернете.

function mb_wordwrap($long_str, $width = 75, $break = "\n", $cut = false) {
    $long_str = html_entity_decode($long_str, ENT_COMPAT, 'UTF-8');
    $width -= mb_strlen($break);
    if ($cut) {
        $short_str = mb_substr($long_str, 0, $width);
        $short_str = trim($short_str);
    }
    else {
        $short_str = preg_replace('/^(.{1,'.$width.'})(?:\s.*|$)/', '$1', $long_str);
        if (mb_strlen($short_str) > $width) {
            $short_str = mb_substr($short_str, 0, $width);
        }
    }
    if (mb_strlen($long_str) != mb_strlen($short_str)) {
        $short_str .= $break;
    }
    return $short_str;
}

Не забудьте настроить PHP для использования UTF-8 с:

ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

Я надеюсь, это поможет. Гийом

person Guillaume V    schedule 19.03.2013

Вот моя собственная попытка функции, которая прошла несколько моих собственных тестов, хотя я не могу обещать, что она на 100% идеальна, поэтому, если вы видите проблему, опубликуйте ее лучше.

/**
 * Multi-byte safe version of wordwrap()
 * Seems to me like wordwrap() is only broken on UTF-8 strings when $cut = true
 * @return string
 */
function wrap($str, $len = 75, $break = " ", $cut = true) { 
    $len = (int) $len;

    if (empty($str))
        return ""; 

    $pattern = "";

    if ($cut)
        $pattern = '/([^'.preg_quote($break).']{'.$len.'})/u'; 
    else
        return wordwrap($str, $len, $break);

    return preg_replace($pattern, "\${1}".$break, $str); 
}
person philfreo    schedule 29.09.2010
comment
wordwrap() переносится только на пробел, когда $cut равно false. Вот почему он работает для UTF-8, который предназначен для обратной совместимости - все символы, не определенные в ASCII, кодируются с помощью старшего набора битов, что предотвращает конфликт с символами ASCII, включая пробел. - person Arc; 30.09.2010
comment
Вы можете уточнить? Например, wordwrap() не работает для UTF-8. Я не уверен, что вы подразумеваете под обертками только в пробеле... - person philfreo; 30.09.2010
comment
проверьте свою функцию на этой строке: проверка проверка - person Yaroslav; 16.11.2013
comment
wordwrap переносится на основе количества байтов, а не количества символов. Для тех, кому лень тестировать, wordwrap('проверка проверка', 32) вынесет каждое слово на отдельную строку. - person toxalot; 25.03.2014

Этот вроде хорошо работает...

function mb_wordwrap($str, $width = 75, $break = "\n", $cut = false, $charset = null) {
    if ($charset === null) $charset = mb_internal_encoding();

    $pieces = explode($break, $str);
    $result = array();
    foreach ($pieces as $piece) {
      $current = $piece;
      while ($cut && mb_strlen($current) > $width) {
        $result[] = mb_substr($current, 0, $width, $charset);
        $current = mb_substr($current, $width, 2048, $charset);
      }
      $result[] = $current;
    }
    return implode($break, $result);
}
person philfreo    schedule 30.09.2010
comment
не должен ли $break быть скорее PHP_EOL? чтобы он был кроссплатформенным? - person ThatGuy; 25.07.2011
comment
М-м-м. он также не разбивает длинные слова. - person ThatGuy; 25.07.2011
comment
Почему вы взрываете строку, используя разрывы строк? Разве вы не должны вместо этого использовать пробелы (для разделения слов)? - person Edson Medina; 09.11.2012
comment
Вы также не должны использовать разнесение, потому что в случае некоторых кодировок (например, UCS-2) это может привести к поломке некоторых символов. - person Zebooka; 14.03.2014
comment
Если цель состоит в том, чтобы добавить многобайтовую поддержку к стандартному PHP wordwrap, функция должна сохранять исходные разрывы строк независимо от типа (\r, \n, \r\n) и независимо от строки, используемой для $break. - person toxalot; 25.03.2014