网上看到一篇文章 discuz和ecshop截取字符串的两个函数,比较了一下两个版本的函数,都各有局限,只能在特定的前提下使用,但是学习一下有利于拓宽思路,了解PHP的扩展功能。
下面先给出两个版本函数的源代码以及简单测试,最后我会给出一个实用性更强的字符串截取函数。需要注意的是:这里讨论的字符串截取问题都是针对UTF-8编码的中文字符串。
discuz版本
1
/*
*
2
* [discuz] 基于PHP没有安装 mb_substr 等扩展截取字符串,如果截取中文字则按2个字符计算
3
* @param $string 要截取的字符串
4
* @param $length 要截取的字符数
5
* @param $dot 替换截掉部分的结尾字符串
6
* @return 返回截取后的字符串
7
*/
8
function cutstr(
$string,
$length,
$dot = '...') {
9
//
如果字符串小于要截取的长度则直接返回
10
// 此处使用strlen获取字符串长度有很大的弊病,比如对字符串“新年快乐”要截取4个中文字符,
11
// 那么必须知道这4个中文字符的字节数,否则返回的字符串可能会是“新年快乐...”
12
if (
strlen(
$string) <=
$length) {
13
return
$string;
14 }
15
16
//
转换原字符串中htmlspecialchars
17
$pre =
chr(1);
18
$end =
chr(1);
19
$string =
str_replace (
array ('&', '"', '<', '>' ),
array (
$pre . '&' .
$end,
$pre . '"' .
$end,
$pre . '<' .
$end,
$pre . '>' .
$end ),
$string );
20
21
$strcut = '';
//
初始化返回值
22
23
// 如果是utf-8编码(这个判断有点不全,有可能是utf8)
24
if (
strtolower ( CHARSET ) == 'utf-8') {
25
//
初始连续循环指针$n,最后一个字位数$tn,截取的字符数$noc
26
$n =
$tn =
$noc = 0;
27
while (
$n <
strlen (
$string ) ) {
28
$t =
ord (
$string [
$n] );
29
30
if (
$t == 9 ||
$t == 10 || (32 <=
$t &&
$t <= 126)) {
31
//
如果是英语半角符号等,$n指针后移1位,$tn最后字是1位
32
$tn = 1;
33
$n++;
34
$noc++;
35 }
elseif (194 <=
$t &&
$t <= 223) {
36
//
如果是二字节字符$n指针后移2位,$tn最后字是2位
37
$tn = 2;
38
$n += 2;
39
$noc += 2;
40 }
elseif (224 <=
$t &&
$t <= 239) {
41
//
如果是三字节(可以理解为中字词),$n后移3位,$tn最后字是3位
42
$tn = 3;
43
$n += 3;
44
$noc += 2;
45 }
elseif (240 <=
$t &&
$t <= 247) {
46
$tn = 4;
47
$n += 4;
48
$noc += 2;
49 }
elseif (248 <=
$t &&
$t <= 251) {
50
$tn = 5;
51
$n += 5;
52
$noc += 2;
53 }
elseif (
$t == 252 ||
$t == 253) {
54
$tn = 6;
55
$n += 6;
56
$noc += 2;
57 }
else {
58
$n++;
59 }
60
61
//
超过了要取的数就跳出连续循环
62
if (
$noc >=
$length) {
63
break;
64 }
65 }
66
67
//
这个地方是把最后一个字去掉,以备加$dot
68
if (
$noc >
$length) {
69
$n -=
$tn;
70 }
71
72
$strcut =
substr (
$string, 0,
$n );
73
74 }
else {
75
//
并非utf-8编码的全角就后移2位
76
for (
$i = 0;
$i <
$length;
$i ++) {
77
$strcut .=
ord (
$string [
$i] ) > 127 ?
$string [
$i] .
$string [++
$i] :
$string [
$i];
78 }
79 }
80
81
//
再还原最初的htmlspecialchars
82
$strcut =
str_replace(
array (
$pre . '&' .
$end,
$pre . '"' .
$end,
$pre . '<' .
$end,
$pre . '>' .
$end ),
array ('&', '"', '<', '>' ),
$strcut );
83
84
$pos =
strrpos (
$strcut,
chr ( 1 ) );
85
if (
$pos !==
false) {
86
$strcut =
substr (
$strcut, 0,
$pos );
87 }
88
89
return
$strcut .
$dot;
//
最后把截取加上$dot输出
90
}
discuz版本的最大缺陷在于使用 strlen 获取原始字符串的长度,并用来和传入的要截取长度参数(字节数)进行比较,由于UTF-8的中文字符的字节数是不固定的,所以就会面临这样的窘境:如果要截取4个中文字符应该指定多大的截取长度呢?8字节还是12字节呢?。。。这是无法预计的,也正是因为这个问题discuz的cutstr实际是有bug的,通过下面的测试结果能看出:
$str1 = "欲穷千里目";
echo my_cutstr(
$str1, 10, "...")."\n";
//
输出:欲穷千里目... [这是一个bug,想想是什么原因导致?]
echo my_cutstr(
$str1, 15, "...")."\n";
//
输出:欲穷千里目
导致上述bug的原因在与cutstr函数在截取字符的时候是将一个中文字按2个字符算,那么5个中文字就是10字符,而原始字符串的长度是15字节,所以cutstr认为“成功地”从15字符的串上截取了10个字符,然后加上了“尾巴”。要解决这个bug只要在判断一下返回的子串是否和原始串相同,如果相同就不加“尾巴”。
ecshop版
1
/*
*
2
* [ecshop] 基于PHP的 mb_substr,iconv_substr 这两个扩展来截取字符串,中文字符都是按1个字符长度计算;
3
* 该函数仅适用于utf-8编码的中文字符串。
4
*
5
* @param $str 原始字符串
6
* @param $length 截取的字符数
7
* @param $append 替换截掉部分的结尾字符串
8
* @return 返回截取后的字符串
9
*/
10
function sub_str(
$str,
$length = 0,
$append = '...') {
11
$str =
trim(
$str);
12
$strlength =
strlen(
$str);
13
14
if (
$length == 0 ||
$length >=
$strlength) {
15
return
$str;
16 }
elseif (
$length < 0) {
17
$length =
$strlength +
$length;
18
if (
$length < 0) {
19
$length =
$strlength;
20 }
21 }
22
23
if (
function_exists('mb_substr') ) {
24
$newstr = mb_substr(
$str, 0,
$length, 'utf-8');
25 }
elseif (
function_exists('iconv_substr') ) {
26
$newstr =
iconv_substr(
$str, 0,
$length, 'utf-8');
27 }
else {
28
//
$newstr = trim_right(substr($str, 0, $length));
29
$newstr =
substr(
$str, 0,
$length);
30 }
31
32
if (
$append &&
$str !=
$newstr) {
33
$newstr .=
$append;
34 }
35
36
return
$newstr;
37 }
ecshop版的特点和缺点都在于将中文字符算作一个字符,如果原始字符串中不含中文,比如:abcd1234,如果本意是要截取4个中文字符或者8个英文字符,那么使用ecshop的版本就得不到期望的结果,返回值的是:abcd。下面是简单的测试结果:
$str1 = "白日依山尽,黄河入海流";
echo
$str1."\n";
echo my_sub_str(
$str1, 4, "...")."\n";
//
输出:白日依山...
$str2 = "白1日2依3山4";
echo
$str2."\n";
echo my_sub_str(
$str2, 4, "...")."\n";
//
输出:白1日2...
优化版
截取中文字符串的大部分应用场景是“原始字符串可以是中文、英文、数字混杂的,中文字按2个字符算,英文数字按1个字符算”,针对这个需求下面给出一个实现版本:
1
/*
*
2
* 字符串截取,中文字符按2个字符计算,同时支持GBK和UTF-8编码
3
* @param $string 要截取的字符串
4
* @param $length 要截取的字符数
5
* @param $append 添加到子串后的尾巴
6
* @return 返回截取后的字符串
7
*/
8
function substring(
$string,
$length,
$append =
false) {
9
if (
$length <= 0 ) {
10
return '';
11 }
12
13
//
检测原始字符串是否为UTF-8编码
14
$is_utf8 =
false;
15
$str1 = @
iconv("UTF-8", "GBK",
$string);
16
$str2 = @
iconv("GBK", "UTF-8",
$str1);
17
if (
$string ==
$str2 ) {
18
$is_utf8 =
true;
19
20
//
如果是UTF-8编码,则使用GBK编码的
21
$string =
$str1;
22 }
23
24
$newstr = '';
25
for (
$i = 0;
$i <
$length;
$i ++) {
26
$newstr .=
ord (
$string[
$i]) > 127 ?
$string[
$i] .
$string[++
$i] :
$string[
$i];
27 }
28
29
if (
$is_utf8 ) {
30
$newstr = @
iconv("GBK", "UTF-8",
$newstr);
31 }
32
33
if (
$append &&
$newstr !=
$string) {
34
$newstr .=
$append;
35 }
36
37
return
$newstr;
38 }
测试结果见下(GBK和UTF-8的结果一致):
$str1 = "白日依山尽,黄河入海流";
echo substring(
$str1, 4, "...")."\n";
//
输出:白日...
echo substring(
$str1, 5, "...")."\n";
//
输出:白日依...
$str2 = "12白34日56依78山";
echo substring(
$str2, 4, "...")."\n";
//
输出:12白...
echo substring(
$str2, 5, "...")."\n";
//
输出:12白3...
本文系原创,欢迎转载,请注明出处!