程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> PHP內核探索之變量- 不平凡的字符串

PHP內核探索之變量- 不平凡的字符串

編輯:關於PHP編程

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 = <<<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的結構是:   struct _zval_struct {     zvalue_value value;       /* value */     zend_uint refcount__gc;   /* variable ref count */     zend_uchar type;          /* active type */     zend_uchar is_ref__gc;    /* if it is a ref variable */ };    而變量的值是zvalue_value這樣一個共用體:   typedef union _zvalue_value {     long lval;     double dval;     struct {                    /* string */         char *val;         int len;     } str;     HashTable *ht;     zend_object_value obj; } zvalue_value; 我們從中抽取出字符串的結構:   struct {     char *val;     int len; } str; 現在比較清楚了,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 string manipulating functions.  A binary-safe function is essentially one that treats its input as a raw stream of data without any specific format.  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中,字符串是以:   struct{       char *val;       int len; } str;  這樣一種結構體來表示的,因而獲取字符串的長度只需要通過常量的時間便可以完成:   #define Z_STRLEN(zval)          (zval).value.str.len 當然,僅僅是strlen函數的性能,無法支持“PHP中string比c字符串的效率更高”的結論(一個很明顯的原因是PHP是構建在C語言之上的高級語言),而僅僅說明,在時間復雜度上,PHP字符串比C字符串更加高效。   (3)      很多C字符串函數存在緩沖區溢出的漏洞   緩存區溢出是C語言中常見的漏洞,這種安全隱患經常是致命的。一個典型的緩存區溢出的例子如下:   void str2Buf(char *str) {     char buffer[16];     strcpy(buffer,str); }  這個函數將str的內容copy到buffer數組中,而buffer數組的大小是16,因此如果str的長度大於16,便會發生緩沖區溢出的問題。   除了strcpy,還有gets, strcat, fprintf等字符串函數也會有緩沖區溢出的問題。   PHP中並沒有strcpy與strcat之類的函數,實際上由於PHP語言的簡潔性,並不需要提供strcpy和strcat之類的函數。例如我們要復制一個字符串,直接使用=即可:   $str = "this is a string"; $str_copy = $str;   由於PHP中變量共享zval的特性,並不會有空間浪費.而簡單的.連接符可以輕松實現字符串連接:   $str = "this is"; $str .= "test string"; echo $str;   關於字符串連接符過程中的內存分配和管理,可以查看zend引擎部分的實現,這裡暫時忽略。   二、   字符串操作相關函數(部分)          毫無疑問,研究字符串的目的並不只是為了知道它的結構和特性,而是為了更好的使用它。我們日常的工作中,恐怕有一般以上的工作都是在與字符串打交道:如處理一個日期串、加密一個密碼、獲取用戶信息、正則表達式匹配替換、字符串替換、格式化一個串等等。可以說,在PHP開發中,你無法避免與字符串的直接或者間接接觸(就像無法擺脫呼吸)。正因為如此,PHP為開發者提供了大量的、豐富的字符串操作函數( http://cn2.php.net/manual/en/ref.strings.php),這對於90%以上的字符串操作,已經基本足夠。            由於字符串函數眾多,不可能一一說明。這裡只挑選幾個比較典型的字符串操作函數     來做簡單的說明(我相信80%以上的PHPer對於字符串的操作函數掌握的非常的好)。   在開始說明之前,有必要強調一下字符串函數的使用原則,理解和掌握這些原則對於高效、熟練使用字符串函數非常關鍵,這些原則包括(不僅限於):   (1)       如果你的操作既可以使用正則表達式,也可以使用字符串。那麼優先考慮字符串操作。            正則表達式是處理文本的絕好工具,尤其對於模式查找、模式替換這一類應用,正則可以說是無往不利。正因為如此,正則表達式在很多場合都被濫用。如果對於你的字符串操作,既可以使用字符串函數完成,也可以使用正則表達式完成,那麼,請優先選擇字符串操作函數,因為正則表達式在一定場合下會有嚴重的性能問題。   (2)       注意false與0       PHP是弱變量類型,相信不少phper開始都深受其害   var_dump( 0 == false);//bool(true) var_dump( 0 === false);//bool(false)   等等,這與字符串操作函數有什麼關系?     在PHP中,有一類函數用於查找(如strpos, stripos),這類查找函數在查找成功時,返回的是子串在原串中的index,如strpos:   var_dump(strpos("this is abc", "abc"));   而在查找不成功時,返回的是false:   var_dump(strpos("this is abc", "angle"));//false   這裡便有一個坑:字符串的索引也是以0開始的!如果子串剛好在源串的起始位置出現,那麼,簡單的==比較便無法區分究竟strpos是不是成功:   var_dump(strpos("this is abc", "this"));   因此我們一定是要用===來比較的:   if((strpos("this is abc", "this")) === false){     // not found }   (3)       多看手冊,避免重復造輪子。     相信不少PHPer面試都碰到過這樣的問題:如何翻轉一個字符串?由於題目中只提及“如何“,而並沒有限制”不使用PHP內置函數“。那麼對於本題,最簡潔的方法自然是使用strrev函數。另一個說明不應該重復造輪子的函數是levenshtein函數,這個函數如同其名字一樣,返回的是兩個字符串的編輯距離。作為動態規劃(DP)的典型代表案例之一,我想編輯距離很多人都不陌生。碰到這類問題,你還准備DP搞起嗎?一個函數搞定它:   $str1 = "this is test"; $str2 = "his is tes"; echo levenshtein($str1, $str2); 在某些情況下,我們都應該盡可能的“懶“,不是嗎。   以下是字符串操作函數節選(對於最常見的操作,請直接參考手冊)   1.  strlen   此標題一出,我猜想大多數人的表情是這樣的:       或者是這樣的:       我要說的,並不是這個函數本身,而是這個函數的返回值。   int strlen ( string $string ) Returns the length of the given string. 雖然手冊上明確指出“strlen函數返回給定字符串的長度”,但是,並沒有對長度單位做任何說明,長度究竟是指”字符的個數“還是說”字符的字節數“。而我們要做的,並不是臆想,而是測試:   在GBK編碼格式下:   echo strlen(“這是中文”);//8 說明strlen函數返回的是字符串的字節數。那麼又有問題了,如果是utf-8編碼,由於中文在utf8編碼的情況下,每個中文使用3個byte,因而,我們期望的結果應該是12:   echo strlen(“這是中文”);//12 這說明:strlen計算字符串的長度依賴於當前的編碼格式,其值並不是唯一的!這在某些情況下,自然是無法滿足要求的。這時,多字節擴展mbstring便有它的發揮余地了:   echo mb_strlen("這是中文", "GB2312");//4 關於這點,在多字節處理中會有相應說明,這裡略過。   2. str_word_count   str_word_count是另一個比較強大的且容易忽略的字符串函數。   mixed str_word_count ( string $string [, int $format = 0 [, string $charlist ]] ) 其中$format的不同值可以使str_word_count函數有不同的行為。 現在,我們手頭有這樣的文本:   When I am down and, oh my soul, so weary When troubles come and my heart burdened be Then, I am still and wait here in the silence Until you come and sit awhile with me You raise me up, so I can stand on mountains You raise me up, to walk on stormy seas I am strong, when I am on your shoulders   You raise me up… To more than I can ber You raise me up, so I can stand on mountains You raise me up, to walk on stormy seas I am strong, when I am on your shoulders You raise me up, 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時,返回的是文本中全部單詞的數組:   print_r(file_get_contents(“word”),1 ); Array (     [0] => 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;//26 echo $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——7F 2個字節:C080——DFBF 3個字符:E08080——EFBFBF 4個字符: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函數(這是用正則表達式做的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; } 由於文章篇幅問題,更多的問題,這裡不再細說。還是那句話,有任何問題,歡迎指出。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved