목차
PHP内核探索之变量(7)- 不平凡的字符串,内核不平凡
一、  字符串基础
二、   字符串操作相关函数(部分)
三、多字节字符串
php教程 php手册 PHP内核探索之变量(7)- 不平凡的字符串,内核不平凡

PHP内核探索之变量(7)- 不平凡的字符串,内核不平凡

Jun 13, 2016 am 09:08 AM

PHP内核探索之变量(7)- 不平凡的字符串,内核不平凡

切,一个字符串有什么好研究的。

别这么说,看过《平凡的世界》么,平凡的字符串也可以有不平凡的故事。试看:

(1)       在C语言中,strlen计算字符串的时间复杂度是?PHP中呢?

(2)       在PHP中,怎样处理多字节字符串?PHP对unicode的支持如何?

同样是字符串,为什么c语言与C++/PHP/Java的均不相同?

数据结构决定算法,这句话一点不假。

那么我们今天就来掰一掰,PHP中的字符串结构,以及相关字符串函数的实现。

一、  字符串基础

  字符串可以说是PHP中遇到最多的数据结构之一了(另外一个比较常用的是数组,见PHP内核探索之变量(4)- 数组操作)。而由于PHP语言的特性和应用场景,使得我们日常的很多工作,实际上都是在处理字符串。也正是这个原因,PHP为开发者提供了丰富的字符串操作函数(初步统计约有100个,这个数量相当可观)。那么,在PHP中,字符串是怎样实现的呢?与C语言又有什么区别呢?

 1.  PHP中字符串的表现形式

  在PHP中使用字符串有四种常见的形式:

(1)    双引号

         这种形式比较常见:$str=”this is \0 a string”; 而且以双引号包含的字符串中可以包含变量、控制字符等:$str = "this is $name, aha.\n";

(2)     单引号

  单引号包含的字符都被认为是raw的,因此不会解析单引号中的变量,控制字符等:

$string = "test";
$str = 'this is $string, aha\n';
echo $str;
로그인 후 복사

(3) Heredoc

Heredoc比较适合较长的字符串表示,且对于多行的字符串表示更加灵活多样。与双引号表示形式类似,heredoc中也可以包含变量。常见的形式是:

$string ="test string";
$str = <<<STR
This is a string \n,
My string is $string
STR;

echo $str;
로그인 후 복사

(4) nowdoc(5.3+支持)

nowdoc和heredoc是如此的类似,以至于我们可以把它们当做是一对儿亲兄弟。nowdoc的起始标志符是用单引号括起来的,与单引号相似,它不会解析其中的变量,格式控制符等:

$s = <<<'EOT'
this is $str
this is \t test;
EOT;

echo $s;
로그인 후 복사

2. PHP中字符串的结构

  之前提到过,PHP中变量是用Zval(PHP内核探索之变量(1)Zval)这样一个结构体来存储的。Zval的结构是:

<span>struct _zval_struct {
    zvalue_value value;       </span><span>/*</span><span> value </span><span>*/</span><span>
    zend_uint refcount__gc;   </span><span>/*</span><span> variable ref count </span><span>*/</span><span>
    zend_uchar type;          </span><span>/*</span><span> active type </span><span>*/</span><span>
    zend_uchar is_ref__gc;    </span><span>/*</span><span> if it is a ref variable </span><span>*/</span><span>
};</span>
로그인 후 복사

而变量的值是zvalue_value这样一个共用体:

<span>typedef union _zvalue_value {
    long lval;
    </span><span>double</span><span> dval;
    struct {                    </span><span>/*</span><span> string </span><span>*/</span><span>
        char </span>*<span>val;
        int len;
    } str;
    HashTable </span>*<span>ht;
    zend_object_value obj;
} zvalue_value;</span>
로그인 후 복사

我们从中抽取出字符串的结构:

<span>struct {
    char </span>*<span>val;
    int len;
} str;</span>
로그인 후 복사

现在比较清楚了,PHP中字符串在底层实际上是一个结构体,该结构体包含了指向字符串的指针和该字符串的长度。

那么为什么这么做呢?换句话说,这样做有什么好处呢?我们接下来,将PHP的字符串与C语言的字符串做一个对比,以解释采用这样一种结构来存储字符串的优势。

3.  与c语言字符串的比较

我们知道,在c语言中,一个字符串可以用两种常见的形式存储,一种是使用指针方式,另一种是使用字符数组。我们接下来的说明,都以c语言的字符数组的方式来存储字符串。

(1) PHP字符串是二进制安全的,而C字符串不是

我们经常会提到”二进制安全”这一术语,那么二进制安全究竟是什么意思呢?

wikipedia中对二进制安全(Binary Safe)的定义是:

Binary-safe is a computer programming term mainly used in connection with <span>string</span> manipulating functions. <br />A binary-safe <span>function</span> is essentially one that treats its input <span>as</span> a raw stream of data without any specific format. <br />It should thus work with all 256 possible values that a character can take (assuming 8-bit characters).
로그인 후 복사

  翻译过来就是:

二进制安全是计算机编程的术语,主要用于字符串操作函数。一个二进制安全的函数,本质上是指它将输入看做是原始的数据流(raw)而不包含任何特殊的格式

那么为什么C字符串不是二进制安全的?我们知道,在C语言中,以字符数组表示的字符串总是以\0结尾的,这个\0便是C字符串的specific format, 用于标识字符串的结束。更近一步说,如果一个字符串中本身包含了\0且并不是该字符串的结尾,那么在C中,\0后面的所有数据都会被忽略(感觉就像是 字符串被莫名其妙的截断了)。这也意味着,C字符串只合适保存简单的文本,而不能用于保存图片、视频、其他文件等二进制数据。而在PHP中,我们可以使用$str = file_get_contents(“filename”);保存图片、视频等二进制数据。

(2) 效率对比

由于C字符串中使用\0来标志字符串的结束,因此,对于strlen函数而言,获取字符串长度的操作需要顺序遍历字符串,直到遇到\0为止,因此strlen函数的时间复杂度是O(n)。而在PHP中,字符串是以:

<span>struct{
      char </span>*<span>val;
      int len;
} str;</span>
로그인 후 복사

这样一种结构体来表示的,因而获取字符串的长度只需要通过常量的时间便可以完成:

<span>#define</span> Z_STRLEN(zval)          (zval).value.str.len
로그인 후 복사

当然,仅仅是strlen函数的性能,无法支持“PHP中string比c字符串的效率更高”的结论(一个很明显的原因是PHP是构建在C语言之上的高级语言),而仅仅说明,在时间复杂度上,PHP字符串比C字符串更加高效。

(3) 很多C字符串函数存在缓冲区溢出的漏洞

缓存区溢出是C语言中常见的漏洞,这种安全隐患经常是致命的。一个典型的缓存区溢出的例子如下:

<span>void</span> str2Buf(<span>char</span> *<span>str) {
    </span><span>char</span> buffer[<span>16</span><span>];
    strcpy(buffer,str);
}</span>
로그인 후 복사

这个函数将str的内容copy到buffer数组中,而buffer数组的大小是16,因此如果str的长度大于16,便会发生缓冲区溢出的问题。

除了strcpy,还有gets, strcat, fprintf等字符串函数也会有缓冲区溢出的问题。

PHP中并没有strcpy与strcat之类的函数,实际上由于PHP语言的简洁性,并不需要提供strcpy和strcat之类的函数。例如我们要复制一个字符串,直接使用=即可:

$str = <span>"</span><span>this is a string</span><span>"</span><span>;
$str_copy </span>= $str;
로그인 후 복사

  由于PHP中变量共享zval的特性,并不会有空间浪费.而简单的.连接符可以轻松实现字符串连接:

$str = <span>"</span><span>this is</span><span>"</span><span>;
$str .</span>= <span>"</span><span>test string</span><span>"</span><span>;
echo $str;</span>
로그인 후 복사

  关于字符串连接符过程中的内存分配和管理,可以查看zend引擎部分的实现,这里暂时忽略。

二、 字符串操作相关函数(部分)

毫无疑问,研究字符串的目的并不只是为了知道它的结构和特性,而是为了更好的使用它。我们日常的工作中,恐怕有一般以上的工作都是在与字符串打交道:如处理一个日期串、加密一个密码、获取用户信息、正则表达式匹配替换、字符串替换、格式化一个串等等。可以说,在PHP开发中,你无法避免与字符串的直接或者间接接触(就像无法摆脱呼吸)。正因为如此,PHP为开发者提供了大量的、丰富的字符串操作函数( http://cn2.php.net/manual/en/ref.strings.php),这对于90%以上的字符串操作,已经基本足够。

由于字符串函数众多,不可能一一说明。这里只挑选几个比较典型的字符串操作函数 来做简单的说明(我相信80%以上的PHPer对于字符串的操作函数掌握的非常的好)。

在开始说明之前,有必要强调一下字符串函数的使用原则,理解和掌握这些原则对于高效、熟练使用字符串函数非常关键,这些原则包括(不仅限于):

(1) 如果你的操作既可以使用正则表达式,也可以使用字符串。那么优先考虑字符串操作

正则表达式是处理文本的绝好工具,尤其对于模式查找、模式替换这一类应用,正则可以说是无往不利。正因为如此,正则表达式在很多场合都被滥用。如果对于你的字符串操作,既可以使用字符串函数完成,也可以使用正则表达式完成,那么,请优先选择字符串操作函数,因为正则表达式在一定场合下会有严重的性能问题。

(2) 注意false与0

  PHP是弱变量类型,相信不少phper开始都深受其害

var_dump( <span>0</span> == <span>false</span>);<span>//</span><span>bool(true)</span>
var_dump( <span>0</span> === <span>false</span>);<span>//</span><span>bool(false)</span>
로그인 후 복사

  等等,这与字符串操作函数有什么关系?

  在PHP中,有一类函数用于查找(如strpos, stripos),这类查找函数在查找成功时,返回的是子串在原串中的index,如strpos:

var_dump(strpos(<span>"</span><span>this is abc</span><span>"</span>, <span>"</span><span>abc</span><span>"</span>));
로그인 후 복사

  而在查找不成功时,返回的是false:

<span>var_dump</span>(<span>strpos</span>("this is abc", "angle"));<span>//</span><span>false</span>
로그인 후 복사

  这里便有一个坑:字符串的索引也是以0开始的!如果子串刚好在源串的起始位置出现,那么,简单的==比较便无法区分究竟strpos是不是成功:

<span>var_dump</span>(<span>strpos</span>("this is abc", "this"));
로그인 후 복사

  因此我们一定是要用===来比较的:

<span>if</span>((<span>strpos</span>("this is abc", "this")) === <span>false</span><span>){
    </span><span>//</span><span> not found</span>
}
로그인 후 복사

  (3) 多看手册,避免重复造轮子

相信不少PHPer面试都碰到过这样的问题:如何翻转一个字符串?由于题目中只提及“如何“,而并没有限制”不使用PHP内置函数“。那么对于本题,最简洁的方法自然是使用strrev函数。另一个说明不应该重复造轮子的函数是levenshtein函数,这个函数如同其名字一样,返回的是两个字符串的编辑距离。作为动态规划(DP)的典型代表案例之一,我想编辑距离很多人都不陌生。碰到这类问题,你还准备DP搞起吗?一个函数搞定它:

<span>$str1</span> = "this is test"<span>;
</span><span>$str2</span> = "his is tes"<span>;
</span><span>echo</span> <span>levenshtein</span>(<span>$str1</span>, <span>$str2</span>);
로그인 후 복사

在某些情况下,我们都应该尽可能的“懒“,不是吗。

以下是字符串操作函数节选(对于最常见的操作,请直接参考手册)

1.  strlen

此标题一出,我猜想大多数人的表情是这样的:

或者是这样的:

我要说的,并不是这个函数本身,而是这个函数的返回值。

int strlen ( string $string )
Returns the length of the given string.
로그인 후 복사

虽然手册上明确指出“strlen函数返回给定字符串的长度”,但是,并没有对长度单位做任何说明,长度究竟是指”字符的个数“还是说”字符的字节数“。而我们要做的,并不是臆想,而是测试:

在GBK编码格式下:

<span>echo</span> <span>strlen</span>(&ldquo;这是中文&rdquo;);<span>//</span><span>8</span>
로그인 후 복사

说明strlen函数返回的是字符串的字节数。那么又有问题了,如果是utf-8编码,由于中文在utf8编码的情况下,每个中文使用3个byte,因而,我们期望的结果应该是12:

<span>echo</span> <span>strlen</span>(&ldquo;这是中文&rdquo;);<span>//</span><span>12</span>
로그인 후 복사

这说明:strlen计算字符串的长度依赖于当前的编码格式,其值并不是唯一的!这在某些情况下,自然是无法满足要求的。这时,多字节扩展mbstring便有它的发挥余地了:

<span>echo</span> mb_strlen("这是中文", "GB2312");<span>//</span><span>4</span>
로그인 후 복사

关于这点,在多字节处理中会有相应说明,这里略过。

2. str_word_count

str_word_count是另一个比较强大的且容易忽略的字符串函数。

<span>mixed</span> <span>str_word_count</span> ( <span>string</span> <span>$string</span> [, int <span>$format</span> = 0 [, <span>string</span> <span>$charlist</span> ]] )
로그인 후 복사

其中$format的不同值可以使str_word_count函数有不同的行为。 现在,我们手头有这样的文本:

When I am down and, oh my soul,<span> so weary
When troubles come and my heart burdened be
Then</span>,<span> I am still and wait here in the silence
Until you come and sit awhile with me
You raise me up</span>,<span> so I can stand on mountains
You raise me up</span>,<span> to walk on stormy seas
I am strong</span>,<span> when I am on your shoulders

You raise me up&hellip; To more than I can ber
You raise me up</span>,<span> so I can stand on mountains
You raise me up</span>,<span> to walk on stormy seas
I am strong</span>,<span> when I am on your shoulders
You raise me up</span>, To more than I can be。
로그인 후 복사

那么:

(1)$format = 0

$format=0, $format返回的是文本中的单词的个数

echo str_word_count(file_get_contents(“word”)); //112

(2)$format = 1

$format=1时,返回的是文本中全部单词的数组:

<span>print_r</span>(<span>file_get_contents</span>(&ldquo;word&rdquo;),1 );
로그인 후 복사
<span>Array</span><span>
(
    [</span>0] =><span> When
    [</span>1] =><span> I
    [</span>2] =><span> am
    [</span>3] =><span> down
    [</span>4] =><span> and
    [</span>5] =><span> oh
    [</span>6] =><span> my
    [</span>7] =><span> soul
    [</span>8] =><span> so
    [</span>9] =><span> weary
    [</span>10] =><span> When
    [</span>11] =><span> troubles
</span>......<span>
)</span>
로그인 후 복사

这一特性有什么作用呢?比如英文分词。还记得“单词统计”的问题么?str_word_count可以轻松完成单词统计TopK的问题:

<span>$s</span> = <span>file_get_contents</span>("./word"<span>);
</span><span>$a</span> = <span>array_count_values</span>(<span>str_word_count</span>(<span>$s</span>, 1<span>)) ;
</span><span>arsort</span>( <span>$a</span><span> );
</span><span>print_r</span>( <span>$a</span><span> );

</span><span>/*</span><span>
Array
(
    [I] => 10
    [me] => 7
    [raise] => 6
    [up] => 6
    [You] => 6
    [am] => 6
    [on] => 6
    [can] => 4
    [and] => 4
    [be] => 3
    [so] => 3
    &hellip;&hellip;
);</span><span>*/</span>
로그인 후 복사

(3)$format = 2

$format=2时,返回的是一个关联数组

<span>$a</span> = <span>str_word_count</span>(<span>$s</span>, 2<span>);
</span><span>print_r</span>(<span>$a</span><span>);

</span><span>/*</span><span>
Array
(
    [0] => When
    [5] => I
    [7] => am
    [10] => down
    [15] => and
    [20] => oh
    [23] => my
    [26] => soul
    [32] => so
    [35] => weary
    [41] => When
    [46] => troubles
    [55] => come
    ...
)</span><span>*/</span>
로그인 후 복사

配合其他数组函数,可以实现更加多样化的功能.例如,配合array_flip,可以计算某个单词最后一次出现的位置:

<span>$t</span> = <span>array_flip</span>(<span>str_word_count</span>(<span>$s</span>, 2<span>));
</span><span>print_r</span>(<span>$t</span>);
로그인 후 복사

而如果配合了array_unique之后再array_flip,则可以计算某个单词第一次出现的位置:

<span>$t</span> = <span>array_flip</span>( <span>array_unique</span>(<span>str_word_count</span>(<span>$s</span>, 2<span>)) );
</span><span>print_r</span>(<span>$t</span><span>);

</span><span>Array</span><span>
(
    [When] </span>=> 0<span>
    [I] </span>=> 5<span>
    [am] </span>=> 7<span>
    [down] </span>=> 10<span>
    [and] </span>=> 15<span>
    [oh] </span>=> 20<span>
    [my] </span>=> 23<span>
    [soul] </span>=> 26<span>
    [so] </span>=> 32<span>
    [weary] </span>=> 35<span>
    [troubles] </span>=> 46<span>
    [come] </span>=> 55<span>
    [heart] </span>=> 67
    ...<span>
)</span>
로그인 후 복사

3. similar_text

这是除了levenshtein()函数之外另一个计算两个字符串相似度的函数:

int <span>similar_text</span> ( <span>string</span> <span>$first</span> , <span>string</span> <span>$second</span> [, <span>float</span> &<span>$percent</span> ] )
로그인 후 복사
<span>$t1</span> = "You raise me up, so I can stand on mountains"<span>;
</span><span>$t2</span> = "You raise me up, to walk on stormy seas"<span>;
</span><span>$percent</span> = 0<span>;

</span><span>echo</span> <span>similar_text</span>(<span>$t1</span>, <span>$t2</span>, <span>$percent</span>).<span>PHP_EOL</span>;<span>//</span><span>26</span>
<span>echo</span> <span>$percent</span>;<span>//</span><span> 62.650602409639</span>
로그인 후 복사

撇开具体的使用不谈,我很好奇底层对于字符串的相似度是如何定义的。

Similar_text函数实现位于 ext/standard/string.c 中,摘取其关键代码:

PHP_FUNCTION(similar_text){
    char *t1, *t2;
    zval **percent = NULL;
    int ac = ZEND_NUM_ARGS();
    int sim;
    int t1_len, t2_len;
       
    /* 参数解析 */
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|Z", &t1, &t1_len, &t2, &t2_len, &percent) == FAILURE) {
        return;
    }
        
    /* set percent to double type */
    if (ac > 2) {
        convert_to_double_ex(percent);
    }

   /* t1_len == 0 && t2_len == 0 */
    if (t1_len + t2_len == 0) {
        if (ac > 2) {
            Z_DVAL_PP(percent) = 0;
        }
        RETURN_LONG(0);
    }       
    
    /* 计算字符串相同个数 */
    sim = php_similar_char(t1, t1_len, t2, t2_len);
    
    /* 相似百分比 */
    if (ac > 2) {
        Z_DVAL_PP(percent) = sim * 200.0 / (t1_len + t2_len);
    }
 
    RETURN_LONG(sim);
}
로그인 후 복사

可以看出,字符串相似个数是通过 php_similar_char 函数实现的,而相似百分比则是通过公式:

percent = sim * <span>200</span> / (t1串长度 + t2串长度)
로그인 후 복사

来定义的。

php_similar_char的具体实现:

static int php_similar_char(const char *txt1, int len1, const char *txt2, int len2)
{
    int sum;
    int pos1 = 0, pos2 = 0, max;

    php_similar_str(txt1, len1, txt2, len2, &pos1, &pos2, &max);
    if ((sum = max)) {
        if (pos1 && pos2) {
            sum += php_similar_char(txt1, pos1,txt2, pos2);
        }

        if ((pos1 + max < len1) && (pos2 + max < len2)) {
            sum += php_similar_char(txt1 + pos1 + max, len1 - pos1 - max,txt2 + pos2 + max, len2 - pos2 - max);
        }
    }

    return sum;
}
로그인 후 복사

这个函数通过调用php_similar_str来完成字符串相似个数的统计,而php_similar_str返回字符串s1与字符串s2的最长相同字符串长度:

static void php_similar_str(const char *txt1, int len1, const char *txt2, int len2, int *pos1, int *pos2, int *max)
{
    char *p, *q;
    char *end1 = (char *) txt1 + len1;
    char *end2 = (char *) txt2 + len2;
    int l;
    *max = 0;
    
    /* 查找最长串 */
    for (p = (char *) txt1; p < end1; p++) {
        for (q = (char *) txt2; q < end2; q++) {
            for (l = 0; (p + l < end1) && (q + l < end2) && (p[l] == q[l]); l++);
            if (l > *max) {
                *max = l;
                *pos1 = p - txt1;
                *pos2 = q - txt2;
            }
        }
    }
}
로그인 후 복사

  php_similar_str匹配完成之后,原始的串被划分为三个部分

第一部分是最长串的左边部分,这一部分含有相似串,但是却不是最长的;

第二部分是最长相似串部分;

第三部分是最长串的右边部分,与第一部分相似,这一部分含有相似串,但是也不是最长的。因而要递归对第一部分和第三部分求相似串的长度:

/* 最长的串左边部分相似串 */
if (pos1 && pos2) {
    sum += php_similar_char(txt1, pos1,txt2, pos2);
}

/* 右半部分相似串 */
if ((pos1 + max < len1) && (pos2 + max < len2)) {
    sum += php_similar_char(txt1 + pos1 + max, len1 - pos1 - max, txt2 + pos2 + max, len2 - pos2 - max);
}
로그인 후 복사

匹配的过程如下图所示:

对于字符串函数的更多解释,可以参考PHP的在线手册,这里不再一一列举。

三、多字节字符串

  迄今为止,我们讨论的所有的字符串和相关操作函数都是单字节的。然而这个世界是如此的丰富多彩,就好比有红瓤的西瓜也有黄瓤的西瓜一样,字符串也不例外。如我们常用的中文汉字在GBK编码的情况下,实际上是使用两个字节来编码的。多字节字符串不仅仅局限于中文汉字,还包括日文,韩文等等多个国家的文字。正因为如此,对于多字节字符串的处理显得异常重要。

  字符和字符集是编程过程中不可避免总是要遇到的术语。如果有童鞋对于这一块的内容并不是特别清晰,建议移步《编码大事1字符编码基础-字符和字符集,》

  由于我们日常中使用较多的是中文,因而我们以中文字符串截取为例, 重点研究中文字符串的问题。

中文字符串的截取

  中文字符串截取一直是个相对来说比较麻烦的问题,原因在于:

(1) PHP原生的substr函数只支持单字节字符串的截取,对于多字节的字符串略显无力

(2) PHP的扩展mbstring需要服务器的支持,事实上,很多开发环境中并没有开启mbstring扩展,对于习惯使用mbstring扩展的童鞋非常遗憾。

(3) 一个更为复杂的问题是,在UTF-8编码的情况下,虽然中文是3个字节的,但是中文的某些特殊字符(如脱字符·)实际上是双字节编码的。这无疑加大了中文字符串截取的难度(毕竟,中文字符串中不可能完全不包含特殊字符)。

头疼之余,还是要自己撸一个中文的字符串截取的库,这个字符串截取函数应该与substr有相似的函数参数列表,而且要支持中文GBK编码和UTF-8编码情况下的截取,为了效率起见,如果服务器已经开启了mbstring扩展,那么就应该直接使用mbstring的字符串截取。

API:

<span>String</span> cnSubstr(<span>string</span> <span>$str</span>, int <span>$start</span>, int <span>$len</span>, [<span>$encode</span>=&rsquo;GBK&rsquo;]);<span>//</span><span>注意参数中$start, $len都是字符数而不是字节数。</span>
로그인 후 복사

我们以UTF-8编码为例,来说明UTF8编码下中文的截取思路。

(1) 编码范围:

UTF-8的编码范围(utf-8使用1-6个字节编码字符,实际上只使用了1-4字节):

1个字节:00<span>&mdash;&mdash;7F
2个字节:C080&mdash;&mdash;DFBF
3个字符:E08080&mdash;&mdash;EFBFBF
4个字符:F0808080&mdash;&mdash;F7BFBFBF</span>
로그인 후 복사

据此, 可以根据第一个字节的范围确定该字符所占的字节数:

<span>$ord</span> = <span>ord</span>(<span>$str</span>{<span>$i</span><span>});
</span><span>$ord</span> < 192<span> 单字节和控制字符
</span>192 <= <span>$ord</span> < 224<span> 双字节
</span>224<= <span>$ord</span> < 240<span>  三字节
中文并没有四个字节的字符</span>
로그인 후 복사

(2)$start为负的情况

if( $start < 0 ){
    $start += cnStrlen_utf8( $str );
 
    if( $start < 0 ){
        $start = 0;
    }
}
로그인 후 복사

网上大多数字符串截取版本都没有处理$start< 0的情况,按照PHP substr的API设计,在$start <0 时,应该加上字符串的长度(多字节指字符数)。

其中cnStrlen_utf8用于获取字符串在utf8编码下的字符数:

function cnStrlen_utf8( $str ){
    $len  = 0;
    $i    = 0;
    $slen = strlen( $str );

    while( $i < $slen ){
        $ord = ord( $str{$i} );
        if( $ord < 127){
            $i ++;
        }else if( $ord < 224 ){
            $i += 2;
        }else{
            $i += 3;
        }
        $len ++;
    }
    
    return $len;
}
로그인 후 복사

因此UTF-8的截取算法为:

function cnSubstr_utf8( $str, $start, $len ){
    if( $start < 0 ){
        $start += cnStrlen_utf8( $str );
        
        if( $start < 0 ){
            $start = 0;
        }
    }    
    
    $slen = strlen( $str );
    
    if( $len < 0 ){
        $len += $slen - $start;
        
        if($len < 0){
            $len = 0;
        }
    }

    $i = 0;    
    $count = 0;
    
    /* 获取开始位置 */
    while( $i < $slen && $count < $start){
        $ord = ord( $str{$i} );
        
        if( $ord < 127){
            $i ++;
        }else if( $ord < 224 ){
            $i += 2;
        }else{
            $i += 3;
        }
        $count ++;
    }
    
    $count  = 0;
    $substr = '';    
    
    /* 截取$len个字符 */
    while( $i < $slen && $count < $len){
        $ord = ord( $str{$i} );
        
        if( $ord < 127){
            $substr .= $str{$i};
            $i ++;
        }else if( $ord < 224 ){
            $substr .= $str{$i} . $str{$i+1};
            $i += 2;
        }else{
            $substr .= $str{$i} . $str{$i+1} . $str{$i+2};
            $i += 3;
        }
        $count ++;
    }
    
    return $substr;
}
로그인 후 복사

而最终的cnSubstr()可以设计如下(程序还有很多优化的余地):

function cnSubstr( $str, $start, $len, $encode = 'gbk' ){
    if( extension_loaded("mbstring") ){
        //echo "use mbstring";
        //return mb_substr( $str, $start, $len, $encode );
    }

    $enc = strtolower( $encode );
    switch($enc){
		case 'gbk':
		case 'gb2312':
			return cnSubstr_gbk($str, $start, $len);
			break;
		case 'utf-8':
		case 'utf8':
			return cnSubstr_utf8($str, $start, $len);
			break;
		default:
			//do some warning or trigger error;
	}

}
로그인 후 복사

简单的测试一下:

<span>$str</span> = "这是中文的字符串string,还有abs&middot; "<span>;
</span><span>for</span>(<span>$i</span> = 0; <span>$i</span> < 10; <span>$i</span>++<span>){
         </span><span>echo</span> cnSubstr( <span>$str</span>,  <span>$i</span>, 3, 'utf8').<span>PHP_EOL</span><span>;
}</span>
로그인 후 복사

最后贴一下ThinkPHP extend中提供的msubstr函数(这是用正则表达式做的substr):

function msubstr($str, $start=0, $length, $charset="utf-8", $suffix=true) {
    if(function_exists("mb_substr"))
        $slice = mb_substr($str, $start, $length, $charset);
    elseif(function_exists('iconv_substr')) {
        $slice = iconv_substr($str,$start,$length,$charset);
        if(false === $slice) {
            $slice = '';
        }
    }else{
        $re['utf-8']   = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/";
        $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/";
        $re['gbk']    = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/";
        $re['big5']   = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/";
        preg_match_all($re[$charset], $str, $match);
        $slice = join("",array_slice($match[0], $start, $length));
    }
    return $suffix ? $slice.'...' : $slice;
}
로그인 후 복사

由于文章篇幅问题,更多的问题,这里不再细说。还是那句话,有任何问题,欢迎指出。

参考文献:

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

PHP에서 int형을 문자열로 변환하는 방법에 대한 자세한 설명 PHP에서 int형을 문자열로 변환하는 방법에 대한 자세한 설명 Mar 26, 2024 am 11:45 AM

PHP에서 int 유형을 문자열로 변환하는 방법에 대한 자세한 설명 PHP 개발에서 int 유형을 문자열 유형으로 변환해야 하는 경우가 종종 있습니다. 이 변환은 다양한 방법으로 수행할 수 있습니다. 이 기사에서는 독자의 이해를 돕기 위해 특정 코드 예제와 함께 몇 가지 일반적인 방법을 자세히 소개합니다. 1. PHP 내장 함수 strval()을 사용하세요. PHP는 다양한 유형의 변수를 문자열 유형으로 변환할 수 있는 내장 함수 strval()을 제공합니다. int형을 string형으로 변환해야 할 때,

Golang 문자열이 지정된 문자로 끝나는지 확인하는 방법 Golang 문자열이 지정된 문자로 끝나는지 확인하는 방법 Mar 12, 2024 pm 04:48 PM

제목: Golang에서 문자열이 특정 문자로 끝나는지 확인하는 방법 Go 언어에서는 문자열을 처리할 때 문자열이 특정 문자로 끝나는지 확인해야 하는 경우가 있습니다. 이 기사에서는 Go 언어를 사용하여 이 기능을 구현하는 방법을 소개하고 참조용 코드 예제를 제공합니다. 먼저 Golang에서 문자열이 지정된 문자로 끝나는지 확인하는 방법을 살펴보겠습니다. Golang의 문자열에 포함된 문자는 인덱싱을 통해 얻을 수 있으며, 문자열의 길이는 다음과 같습니다.

Golang에서 문자열이 특정 문자로 시작하는지 확인하는 방법은 무엇입니까? Golang에서 문자열이 특정 문자로 시작하는지 확인하는 방법은 무엇입니까? Mar 12, 2024 pm 09:42 PM

Golang에서 문자열이 특정 문자로 시작하는지 확인하는 방법은 무엇입니까? Golang으로 프로그래밍할 때 문자열이 특정 문자로 시작하는지 확인해야 하는 상황에 자주 직면하게 됩니다. 이 요구 사항을 충족하기 위해 Golang의 문자열 패키지에서 제공하는 기능을 사용할 수 있습니다. 다음에는 Golang을 사용하여 문자열이 특정 문자로 시작하는지 확인하는 방법을 구체적인 코드 예제와 함께 자세히 소개하겠습니다. Golang에서는 strings 패키지의 HasPrefix를 사용할 수 있습니다.

python_python 반복 문자열 튜토리얼에서 문자열을 반복하는 방법 python_python 반복 문자열 튜토리얼에서 문자열을 반복하는 방법 Apr 02, 2024 pm 03:58 PM

1. 먼저 pycharm을 열고 pycharm 홈페이지로 들어갑니다. 2. 그런 다음 새 Python 스크립트를 생성하고 마우스 오른쪽 버튼을 클릭하고 새로 만들기를 클릭한 후 Pythonfile을 클릭합니다. 3. 문자열(코드: s="-")을 입력합니다. 4. 그런 다음 문자열의 기호를 20번 반복해야 합니다(코드: s1=s*20). 5. 인쇄 출력 코드(코드: print(s1))를 입력합니다. 6. 마지막으로 스크립트를 실행하면 하단에 반환 값이 표시됩니다. - 20번 반복됩니다.

PHP에서 16진수를 문자열로 변환할 때 중국어 문자가 깨지는 문제를 해결하는 방법 PHP에서 16진수를 문자열로 변환할 때 중국어 문자가 깨지는 문제를 해결하는 방법 Mar 04, 2024 am 09:36 AM

PHP에서 16진수 문자열을 변환할 때 중국어 문자가 깨지는 문제를 해결하는 방법 PHP 프로그래밍에서 때때로 16진수 문자열을 일반 중국어 문자로 변환해야 하는 상황에 직면합니다. 그러나 이러한 변환 과정에서 때때로 중국어 문자가 깨져 나오는 문제에 직면하게 됩니다. 이 기사에서는 PHP에서 16진수를 문자열로 변환할 때 중국어 문자가 깨지는 문제를 해결하는 방법과 구체적인 코드 예제를 제공합니다. 16진수 변환을 위해서는 hex2bin() 함수를 사용하세요. PHP에 내장된 hex2bin() 함수는 1을 변환할 수 있습니다.

PHP 문자열 일치 팁: 모호한 포함 표현식을 피하세요 PHP 문자열 일치 팁: 모호한 포함 표현식을 피하세요 Feb 29, 2024 am 08:06 AM

PHP 문자열 일치 팁: 모호한 포함 표현식 방지 PHP 개발에서 문자열 일치는 일반적으로 특정 텍스트 내용을 찾거나 입력 형식을 확인하는 데 사용되는 일반적인 작업입니다. 그러나 일치 정확도를 보장하기 위해 모호한 포함 표현식을 사용하지 말아야 할 경우도 있습니다. 이 기사에서는 PHP에서 문자열 일치를 수행할 때 모호한 포함 표현식을 방지하는 몇 가지 기술을 소개하고 구체적인 코드 예제를 제공합니다. 정확한 일치를 위해 preg_match() 함수를 사용하십시오. PHP에서는 preg_mat를 사용할 수 있습니다.

PHP 문자열 조작: 공백을 효과적으로 제거하는 실용적인 방법 PHP 문자열 조작: 공백을 효과적으로 제거하는 실용적인 방법 Mar 24, 2024 am 11:45 AM

PHP 문자열 작업: 공백을 효과적으로 제거하는 실용적인 방법 PHP 개발 시 문자열에서 공백을 제거해야 하는 상황에 자주 직면하게 됩니다. 공백을 제거하면 문자열이 더 깔끔해지고 후속 데이터 처리 및 표시가 쉬워집니다. 이 기사에서는 공백을 제거하는 몇 가지 효과적이고 실용적인 방법을 소개하고 구체적인 코드 예제를 첨부합니다. 방법 1: PHP 내장 함수인 Trim()을 사용합니다. PHP 내장 함수인 Trim()을 사용하면 문자열 양쪽 끝의 공백(공백, 탭, 개행 등 포함)을 제거할 수 있어 매우 편리하고 쉽습니다. 사용.

문자열의 마지막 두 문자를 삭제하는 PHP 기술 문자열의 마지막 두 문자를 삭제하는 PHP 기술 Mar 23, 2024 pm 12:18 PM

웹 애플리케이션 개발에 널리 사용되는 스크립팅 언어로서 PHP는 매우 강력한 문자열 처리 기능을 가지고 있습니다. 일상적인 개발 과정에서 문자열, 특히 문자열의 마지막 두 문자를 삭제해야 하는 작업을 자주 접하게 됩니다. 이 기사에서는 문자열의 마지막 두 문자를 삭제하는 두 가지 PHP 기술을 소개하고 구체적인 코드 예제를 제공합니다. 팁 1: substr 함수 사용 PHP의 substr 함수는 문자열의 일부를 반환하는 데 사용됩니다. 문자열과 시작 위치를 지정하여 쉽게 문자를 제거할 수 있습니다.

See all articles