UTF-8 的多字节安全 wordwrap() 函数
Posted
技术标签:
【中文标题】UTF-8 的多字节安全 wordwrap() 函数【英文标题】:Multi-byte safe wordwrap() function for UTF-8 【发布时间】:2011-04-19 00:31:00 【问题描述】:php 的 wordwrap()
函数无法正确处理 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
将单词与<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);
查看截图演示:
【讨论】:
【参考方案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()
仅在 $cut
为 false
时在空格字符处换行。这就是为什么它适用于被设计为向后兼容的 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 出错:无效的多字节序列