PHP核心探索之變數- 不平凡的字串
切,一個字串有什麼好研究的。 別這麼說,看過《平凡的世界》麼,平凡的字串也可以有不平凡的故事。試看: (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 = << When [1] => I [2] => am [3] => down [4] => and [5] => oh [6] => my [7] => soul [8] => so [9] => weary [10] => When [11] => troubles......)這一特性有什麼作用呢?比如英文分詞。還記得“單詞統計”的問題嗎?str_word_count可以輕鬆完成單詞統計TopK的問題: $s = file_get_contents("./word");$a = array_count_values(str_word_count($s, 1)) ;arsort( $a );print_r( $a ); /*Array( [I] => 10 [me] => 7 [raise] => 6 [up] => 6 [You] => 6 [am] => 6 [on] => 6 [can] => 4 [and] => 4 [be] => 3 [so] => 3 ……);*/(3)$format = 2 $format=2時,返回的是一個關聯陣列: $a = str_word_count($s, 2);print_r($a); /*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 ...)*/配合其他數組函數,可以實現更加多樣化的功能.例如,配合array_flip,可以計算某個單詞最後一次出現的位置: $t = array_flip(str_word_count($s, 2));print_r($t);而如果配合了array_unique之後再array_flip,則可以計算某個單詞第一次出現的位置: $t = array_flip( array_unique(str_word_count($s, 2)) );print_r($t); Array( [When] => 0 [I] => 5 [am] => 7 [down] => 10 [and] => 15 [oh] => 20 [my] => 23 [soul] => 26 [so] => 32 [weary] => 35 [troubles] => 46 [come] => 55 [heart] => 67 ...)3. similar_text 這是除了levenshtein()函數之外另一個計算兩個字串相似性的函數: int similar_text ( string $first , string $second [, float &$percent ] )$t1 = "You raise me up, so I can stand on mountains";$t2 = "You raise me up, to walk on stormy seas";$percent = 0; echo similar_text($t1, $t2, $percent).PHP_EOL;//26echo $percent;// 62.650602409639撇開具體的使用不談,我很好奇底層對於字串的相似性是如何定義的。 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 * 200 / (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: String cnSubstr(string $str, int $start, int $len, [$encode=’GBK’]);//注意參數中$start, $len都是字元數而不是位元組數。我們以UTF-8編碼為例,來說明UTF8編碼下中文的截取思路。 (1) 編碼範圍: UTF-8的編碼範圍(utf-8使用1-6個位元組編碼字元,實際上只使用了1-4位元組): 1個位元組:00——7F2個位元組:C080——DFBF3個字元:E08080——EFBFBF4個字元:F0808080——F7BFBFBF據此, 可以根據第一個位元組的範圍確定該字元所佔的位元組數: $ord = ord($str{$i});$ord < 192 單位元組和控制字元192 <= $ord < 224 雙位元組224<= $ord < 240 三位元組中文並沒有四個位元組的字元(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; } }簡單的測試一下: $str = "這是中文的字串string,還有abs· ";for($i = 0; $i < 10; $i++){ echo cnSubstr( $str, $i, 3, 'utf8').PHP_EOL;}最後貼一下ThinkPHP extend中提供的msubstr函數(這是用Regex做的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;}由於文章篇幅問題,更多的問題,這裡不再細說。還是那句話,有任何問題,歡迎指出。
http://www.bkjia.com/PHPjc/976800.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/976800.htmlTechArticlePHP核心探索之變數- 不平凡的字串 切,一個字串有什麼好研究的。 別這麼說,看過《平凡的世界》麼,平凡的字串也可以有不平凡的...