UTF-8 的多字节安全 wordwrap() 函数

Posted

技术标签:

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

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

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

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

具体确保它适用于:

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

【问题讨论】:

s($str)->truncate($length, $break)s($str)->truncateSafely($length, $break) 两种方法正是这样做的,如 this standalone library 中所示。第一个用于$cut = true,第二个用于$cut = false。它们是 Unicode 安全的。 【参考方案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 separators 和full 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 的字符串。

以上是关于UTF-8 的多字节安全 wordwrap() 函数的主要内容,如果未能解决你的问题,请参考以下文章

国际化时django.po中的msgstr =“”为中文时,django-admin.py compilemessages 出错:无效的多字节序列

宽字节注入

php wordwrap()函数 语法

深度分析:面试90%被问到的多线程创建线程线程状态线程安全,一次性帮你全搞定!

优化 WordWrap 算法

php Wordwrap示例