【问题标题】:Multi-byte safe wordwrap() function for UTF-8UTF-8 的多字节安全 wordwrap() 函数
【发布时间】:2011-04-19 00:31:00
【问题描述】:

PHP 的 wordwrap() 函数无法正确处理 UTF-8 等多字节字符串。

cmets中有几个mb安全函数的例子,但是根据一些不同的测试数据,它们似乎都有一些问题。

该函数应采用与wordwrap() 完全相同的参数。

具体确保它适用于:

  • 如果是$cut = true,就删掉中间词,否则不要删掉中间词
  • 如果$break = ' ',则不要在单词中插入多余的空格
  • 也适用于$break = "\n"
  • 适用于 ASCII 和所有有效的 UTF-8

【问题讨论】:

标签: php string utf-8 word-wrap multibyte


【解决方案1】:

我还没有找到任何适合我的工作代码。这是我写的。对我来说它正在工作,认为它可能不是最快的。

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);
}

【讨论】:

  • 对我也很有效!
  • 我已经用了几年了,但不是很重。无论如何,我将这个函数包含在一个 php 类中,我把它作为 MIT 下的 github 上的一个要点,只需要验证它是好的 - gist.github.com/AliceWonderMiscreations/…
  • 用 PHP 5.6 尝试了这段代码,但对我没有用 =( 它需要设置 ini_set 和 mb_internal_encoding?
  • @AliceWonder 没找到链接了,不过一般没问题:)
【解决方案2】:
/**
 * wordwrap for utf8 encoded strings
 *
 * @param string $str
 * @param integer $len
 * @param string $what
 * @return string
 * @author Milian Wolff <mail@milianw.de>
 */

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 是好时间:)

【讨论】:

  • 如果不是$cut,这个函数有缺陷。如果可能,它不会提前包装(这是wordwrap 会做的。See demo。不是解决方案,但相关答案有另一个Wordwrap Regex
  • 这种行为与wordwrap() 不同,涉及空格。
  • 这一项在cut=true时对简体中文起作用
  • 这不适用于西里尔字母。断词。没找原因,打算试试别的解决办法。
【解决方案3】:

因为没有答案可以处理每个用例,所以这里有一些可以解决的问题。代码基于Drupal’s AbstractStringWrapper::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;
}

?>

【讨论】:

  • 适用于 UTF-8 中的西里尔字母。
【解决方案4】:

自定义单词边界

Unicode 文本比 8 位编码具有更多潜在的字边界,包括17 space separatorsfull width comma。此解决方案允许您为应用程序自定义单词边界列表。

更好的性能

您是否曾经对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;
}

【讨论】:

    【解决方案5】:

    只是想分享一些我在网上找到的替代品。

    <?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将单词与&lt;br&gt;结合起来。

    <?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);
    

    查看截图演示:

    【讨论】:

      【解决方案6】:
      function mb_wordwrap($str, $width = 74, $break = "\r\n", $cut = false)
              {
                  return preg_replace(
                      '~(?P<str>.{' . $width . ',}?' . ($cut ? '(?(?!.+\s+)\s*|\s+)' : '\s+') . ')(?=\S+)~mus',
                      '$1' . $break,
                      $str
                  );
              }
      

      【讨论】:

        【解决方案7】:

        这是我编写的多字节自动换行函数,灵感来自互联网上的其他人。

        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');
        

        我希望这会有所帮助。 纪尧姆

        【讨论】:

          【解决方案8】:

          这是我自己尝试的一个函数,它通过了我自己的一些测试,但我不能保证它是 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); 
          }
          

          【讨论】:

          • wordwrap() 仅在 $cutfalse 时在空格字符处换行。这就是为什么它适用于被设计为向后兼容的 UTF-8 - 未在 ASCII 中定义的字符都使用最高位集进行编码,防止与包括空格在内的 ASCII 字符发生冲突。
          • 你能澄清一下吗?例如,wordwrap() 不适用于 UTF-8。我不确定您所说的“仅在空格处换行”是什么意思
          • 在这个字符串上测试你的函数:проверка проверка
          • wordwrap 基于 字节 的数量而不是 字符 的数量进行换行。对于那些懒得测试的人,wordwrap('проверка проверка', 32) 会将每个单词单独放在一行。
          【解决方案9】:

          这个好像很好用……

          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);
          }
          

          【讨论】:

          • $break 不应该是 PHP_EOL 吗?所以它会是跨平台的?
          • 嗯。它也不会拆分长词。
          • 为什么要使用换行符来分解字符串?你不应该使用空格来代替(用于分割单词)吗?
          • 你也不应该使用explode,因为如果某些编码(如UCS-2)编码这可能会破坏一些符号。
          • 如果目标是为 PHP 的标准 wordwrap 添加多字节支持,则无论类型如何(\r\n\r\n),该函数都应保留原始换行符用于$break 的字符串。
          猜你喜欢
          • 1970-01-01
          • 2012-11-12
          • 1970-01-01
          • 2011-02-28
          • 2021-08-22
          • 2011-04-22
          • 2012-01-23
          • 2011-07-25
          • 1970-01-01
          相关资源
          最近更新 更多