php计算字符串截取的问题

我页面上有个字符串过长需要用省略号代替的功能,现在我用strlen和substr实现,发现中文和英文截取文字长度不一样,导致中文截取的过少,英文的截取的和设置的长度的一样。有没有什么好的方法统一中文和英文一样!表示无语啊!

阅读 6.9k
8 个回答

php中编码是UTF-8的话占3个字节;是GB2312的话占2个字节。推荐你把所有字符设置成同一种编码字符处理。php除了strlensubstr之外,还有带mb_开头的啊!可以指定字符串编码格式例如mb_strlenmb_substr

$len = mb_strlen($string, 'UTF-8');
$newString = $len>60?mb_substr($string, 0, 60, 'UTF-8'):$string;

试试看

让多余的字符串显示为省略号,截取的方式是一种很落后的方式,而且字符串截取对于中文和英文截取结果不一样。HTML5中可以直接通过css来控制:

overflow: hidden;  //溢出部分影藏
white-space: nowrap;  //文本不进行换行
text-overflow: ellipsis;   //当文本溢出包含元素时显示省略号

这三个组合使用即可。

    /**
     * 字符串截取方法(支持中英文,截取长度包含省略符)
     * @param  string $string   字符串
     * @param  integer $length  截取长度
     * @param  string $dot      省略符
     * @param  string $charset  编码
     * @return string
     */
    function strCut($string, $length, $dot = '...', $charset = 'UTF-8') {
        $charset = 'UTF-8';
        $strlen = strlen($string);
        if($strlen <= $length) return $string;
        $string = str_replace(
            array(' ','&nbsp;', '&', '"', '\'', '“', '”', '—', '<', '>', '·', '…'), 
            array(' ',' ', '&', '"', "'", '“', '”', '—', '<', '>', '·', '…'),
            $string
        );
        $strcut = '';
        if (strtolower($charset) == 'utf-8') {
            $length = intval($length-strlen($dot)-$length/3);
            $n = $tn = $noc = 0;
            while ($n < strlen($string)) {
                $t = ord($string[$n]);
                if ($t == 9 || $t == 10 || (32 <= $t && $t <= 126)) {
                    $tn = 1; $n++; $noc++;
                } elseif(194 <= $t && $t <= 223) {
                    $tn = 2; $n += 2; $noc += 2;
                } elseif(224 <= $t && $t <= 239) {
                    $tn = 3; $n += 3; $noc += 2;
                } elseif(240 <= $t && $t <= 247) {
                    $tn = 4; $n += 4; $noc += 2;
                } elseif(248 <= $t && $t <= 251) {
                    $tn = 5; $n += 5; $noc += 2;
                } elseif($t == 252 || $t == 253) {
                    $tn = 6; $n += 6; $noc += 2;
                } else {
                    $n++;
                }
                if ($noc >= $length) {
                    break;
                }
            }
            if ($noc > $length) {
                $n -= $tn;
            }
            $strcut = substr($string, 0, $n);
            $strcut = str_replace(
                array('∵', '&', '"', "'", '“', '”', '—', '<', '>', '·', '…'), 
                array(' ', '&', '"', '\'', '“', '”', '—', '<', '>', '·', '…'),
                $strcut
            );
        } else {
            $dotlen = strlen($dot);
            $maxi = $length - $dotlen - 1;
            $current_str = '';
            $search_arr = array('&',' ', '"', "'", '“', '”', '—', '<', '>', '·', '…','∵');
            $replace_arr = array('&','&nbsp;', '"', '\'', '“', '”', '—', '<', '>', '·', '…',' ');
            $search_flip = array_flip($search_arr);
            for ($i = 0; $i < $maxi; $i++) {
                $current_str = ord($string[$i]) > 127 ? $string[$i].$string[++$i] : $string[$i];
                if (in_array($current_str, $search_arr)) {
                    $key = $search_flip[$current_str];
                    $current_str = str_replace($search_arr[$key], $replace_arr[$key], $current_str);
                }
                $strcut .= $current_str;
            }
        }
        return $strcut.$dot;
    }

推荐使用这个方法来避免一下尴尬:

(1) substr截取中文会出现乱码的情况:

$string = '中文字符中文字符';
var_dump(substr($string, 0, 10));

结果:string(10) "中文字�"
原因:中文占3个字符,substr可能会把某个中文截取了一部分,使中文乱码。

(2) mb_substr截取会出现字符太长的情况:

$string = '中文字符englishword';
var_dump(mb_substr($string, 0, 10));

结果:string(18) "中文字符englis"
原因:中文占3个字符,输出结果的字符串实际占18个字符,并不是期望的10个字符。

使用情景:微信支付商品名称有128个字符限制,在UTF-8编码下,中英文字符的总字符长度要控制在128个以内,个人觉得用这个方法比较合适。

其它答案错误的理解

其实题主的问题是:中文英文截取的长度一样,这里的长度并非指字符长度(Length、Byte),而是像素宽度(Width)

此理论与UTF-8中汉字是3个字符无关,其实某些生僻的汉字、Emoji占有4个字符。

UTF-8最长是6个字符长(1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx)。


按照汉字(东亚字系)的字体设计,一般情况下 1个汉字 ≈ 2个英文(数字、符号等) 的 像素宽度
如:

1a  12ab  123abc   1234abcd
汉  汉字   汉字汉   汉字汉字

可以看到1个汉字 ≈ 2个英文的width

在早期的网站中,一般使用SimSun(宋体),在SimSun的设计中,英文的宽度 == 1/2的汉字。
随着互联网的发展,一般的字体已经不能满足大家的需要,所以字体百花齐放的今天,只能 ≈ (约等于)
比如segmentfault的字符方案中,英文多出了1个宽度,但是不影响整体效果

要保证截取后的字符等长

实现

前端:(这是最好的方案)

overflow: hidden;  //此句必须
white-space: nowrap;  //对于无需换行的场景,可以设置width/height为固定值
text-overflow: ellipsis;   //此行必须,但是Firefox部分版本不兼容

后端: mb_substr,cutStr(能署名代码来自于Discuz!可以吗?)的答案,都是错误的,这些得到的结果汉字宽度大于英文的宽度

请查看以下代码:以UTF-8为例子

/**
 * 移除字符串的BOM
 *
 * @param  string $str 输入字符串
 * @return string 输出字符串
 */
function removeBOM($str)
{
    $str_3 = substr($str, 0, 3);
    if ($str_3 == pack('CCC',0xef,0xbb,0xbf)) //utf-8
        return substr($str, 3);
    return $str;
}

/**
 * 按UTF-8分隔为数组,效率比MB_Substr高
 * 0xxxxxxx
 * 110xxxxx 10xxxxxx
 * 1110xxxx 10xxxxxx 10xxxxxx
 * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 * 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 * 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 *
 * @param string $str 输入utf-8字符串
 * @return array 返回成一段数组
 */
function str_split_utf8($str)
{
    return preg_match_all('/./u', removeBOM($str), $out) ? $out[0] : FALSE;
}

/**
 * 按非ascii字符占有几个字宽的方式切分字符串,并且不会将汉字切成半个
 * 所谓字宽是指,使用默认字体显示时,非ascii字符相比英文字符所占大小,比如:宋体、微软雅黑中,汉字占两个宽度
 * @example $ansi_width = 2 表示汉字等非英文字符按照两个字宽长度
 * @example $ansi_width = 1 表示所有字符按一个字宽长度
 *
 * @param string $string 原始字符
 * @param integer $offset 开始偏移,使用方法和substr一样,可以为负数
 * @param integer $length 长度,使用方法和substr一样,可以为负数
 * @param integer $ansi_width 汉字等非英文字符按照几个字符来处理
 * @return string 返回裁减的字符串
 */
function substr_ansi($string, $offset, $length = 0, $ansi_width = 1)
{
    if (empty($string)) return $string;;
    $data = str_split_utf8($string);
    if (empty($data)) return $string;
 
    $as = $_as = array();
    $_start = $_end = 0;
 
    foreach($data as $k => $v)
        $as[$k] = strlen($v) > 1 ? $ansi_width : 1;
 
    $_as_rev = array_reverse($as,true);
    $_as = $offset < 0 ? $_as_rev : $as; 
    $n = 0; $_offset = abs($offset);
    foreach($_as as $k => $v) {
        if ($n >= $_offset) {
            $_start = $k;
            break;
        }
        $n += $v;
    }
    //echo $_start,',';
    $_as = $length <= 0 ? $_as_rev : $as;
    end($_as); list($_end) = each($_as); reset($_as);//给$_end 设定默认值,一直到结尾
    $n = 0; $_length = abs($length);
    foreach($_as as $k => $v) {
        if ($k >= $_start) {
            if ($n >= $_length) {
                $_end = $k + ($length <= 0 ? 1 : 0);
                break;
            }
            $n += $v;
        }
    }
    //echo $_end,'|||||';
    if ($_end <= $_start)
        return '';
 
    $_data = array_slice($data, $_start, $_end - $_start);
 
    return implode('',$_data);
}

/**
 * 按非ascii字符占有几个字宽的方式计算字符串长度
 * @example $ansi_width = 2 表示汉字等非英文字符按照两个字宽长度
 * @example $ansi_width = 1 表示所有字符按一个字节长度
 *
 * @param string $string 原始字符
 * @param integer $ansi_width 汉字等非英文字符按照几个字宽来处理
 * @return string 返回字符串长度
 */
function strlen_ansi($string, $ansi_width = 1)
{
    if (empty($string)) return 0;
    $data = str_split_utf8($string);
    if (empty($data)) return 0;
 
    $as = 0;
    foreach($data as $k => $v)
        $as += strlen($v) > 1 ? $ansi_width : 1;
    unset($data);
    return $as;
}

/**
 * smarty truncate 代码算法来自于Smarty
 * @param string
 * @param integer
 * @param string
 * @param boolean
 * @param boolean
 * @return string
 */
function truncate($string, $length = 80, $etc = '...', $break_words = false, $middle = false)
{
    if ($length == 0)
        return '';
    $ansi_as = 2;
    if (strlen_ansi($string, $ansi_as) > $length) {
        $length -= min($length, strlen_ansi($etc, $ansi_as));
        if (!$break_words && !$middle) {
            $string = preg_replace('/\s+?(\S+)?$/u', '', substr_ansi($string, 0, $length+1, $ansi_as));
        }
        if(!$middle) {
           return substr_ansi($string, 0, $length, $ansi_as) . $etc;
        } else {
            return substr_ansi($string, 0, $length/2, $ansi_as) . $etc . substr_ansi($string, -$length/2, 0,  $ansi_as);
        }
    } else {
        return $string;
    }
}

substr_ansitruncate便是你要的截取的函数

// substr_ansi ($offset, $length, $ansi_width)
// 如果ansi_width = 2,则表示将汉字当做2个宽度处理
// offset length 在实际截取过程中,以英文的长度为准即可

echo substr_ansi('汉字我爱你', 0, 5, 2);     //输出:汉字我
echo substr_ansi('汉字abc我爱你', 0, 5, 2);  //输出:汉字a
echo substr_ansi('abcdef', 0, 5, 2);        //输出:abcde

echo mb_substr('汉字我爱你', 0, 5);          //输出:汉字我爱你
echo mb_substr('汉字abc我爱你', 0, 5);       //输出:汉字abc
echo mb_substr('abcdef', 0, 5);             //输出:abcde

可以看到上面substr_ansi的截取后的像素宽度是正确的,并且,汉字不会截取半个
下面的mb_substr长度明显不一致

针对日文、GBK、GB2312、Unicode等情况,请参见:
http://www.load-page.com:8989...
由于实在没有精力,以及答主不太懂日文(韩文),有些字符集的ASCII区域无法弄清楚,但是此代码在中文方面经过生产环境的验证,已经没有什么问题。有了解东亚,欧洲等字符集的同好,欢迎私信联系我。

text-overflow:ellipsis

string mb_strimwidth ( string $str , int $start , int $width [, string $trimmarker = "" [, string $encoding = mb_internal_encoding() ]] )

PHP提供的这个函数看起来可以满足你的要求. $trimmarker 是如果长度超了, 后面添加的...这三字符.

    public static function mb_substr(&$str, $length, $encoding = 'utf-8')
    {
        return isset($str) ? mb_substr($str, 0, $length, $encoding) . ($length < mb_strlen($str, $encoding) ? '...' : '') : '';
    }

https://github.com/letwang/le...

baidu就可以了吧,我基本都是这样解决的。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
1 篇内容引用
推荐问题
宣传栏