需求是為一個多維陣列物件的資料按行儲存到檔案,需要鍵值對區分層級,對每個物件描述清晰。類似的格式如下:
上圖中的資料對應的就是如下的陣列(php 語言):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$arr = array( '10003' => array( 'id' => 10003, 'tokentime' => 400), '10005' => array( 'id' => 10005, <p> 'cookie' => array(</p> 'num' => 20 ), 'vcode' => array( 'length' => 6 ), ), ); |
每個id對應一條資料物件,但是這個資料物件可能是多維的資料,需要用檔案對使用者可讀性友好的儲存起來,這裡很容易就可以想到使用json格式也是可以的,將陣列json_encode後儲存到檔案,讀取的時候使用json_decode解析為陣列或物件即可。這確實是一種方便的辦法,但是本案例使用的就是這中按行儲存的格式。因此需要自行實現解析與儲存的過程。
通過上述格式的分析,可以很容易聯想到ini檔案的格式,中括號區分的是區塊,下面對應鍵值對,但是這裡有一個注意點是每個區塊開頭的點浩(“.”)代表了區塊的層級關係,正式這種層級關係表明了陣列的多維性和認為可讀性,也是非常巧妙的一種方式。
但是,需要考慮的一個細節問題是,php的parse_ini_file函式對ini檔案讀取時,如果區塊名稱相同,那麼會新的區塊內容會覆蓋之前的區塊。php官方手冊中介紹如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
array parse_ini_file ( string $filename [, bool $process_sections = false [, int$scanner_mode = INI_SCANNER_NORMAL ]] ) parse_ini_file() 載入一個由 filename 指定的 ini 檔案,並將其中的設定作為一個聯合陣列返回。 ini 檔案的結構和 php.ini 的相似。 引數 filename 要解析的 ini 檔案的檔名。 process_sections 如果將最後的 process_sections 引數設為 TRUE,將得到一個多維陣列,包括了配置檔案中每一節的名稱和設定。process_sections 的預設值是 FALSE。 scanner_mode Can either be INI_SCANNER_NORMAL (default) or INI_SCANNER_RAW. If INI_SCANNER_RAW is supplied, then option values will not be parsed. |
也就是[.10003]區塊中出現了[..cookie]區塊,此時讀取檔案後,[.10005]區塊內的[..cookie]區塊會覆蓋前者的內容,當然也可以不使用parse_ini_file函式,而選擇對檔案按行讀取進行判斷這樣也是可以的。此處本人還是使用了parse_ini_file函式,因為上述檔案格式算是標準ini檔案格式的一種擴充套件,因此直接使用可以馬上返回一個陣列再對陣列進行處理來說還是更為方便。
實現策略:使用parse_ini_file函式,但是對於區塊策略使用父級的鍵名+自身鍵名的方式以避免重複。
寫入方式如下,直接先看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public function setAppInfo($app, $path){ $appStr = "[app]\n"; $ret = ''; self::readArrayRecursive(array_reverse($app, true), $ret, 'app', 1); return file_put_contents($path, $ret); } //setAppInfo helper private function readArrayRecursive($arr, &$str = '', $prefix, $level = 1){ foreach($arr as $k => $v){ if (is_array($v)){ $str .= "[" . str_repeat('.', $level) . $prefix . ':' . $k . "]\n"; $level++; self::readArrayRecursive($v, &$str, $k, $level); $level--; }else{ $str .= "$k=$v\n"; } } } |
這裡實現的本質就是要根據一個現有的多維陣列拼湊出一個含有多維陣列層級關係清晰的字串,最終寫入到指定的檔案中。
在主呼叫函式中呼叫遞迴輔助函式,遞迴方式看起來形式簡潔,但是卻不易寫出來,特別是level引數和每次傳遞的最終拼湊的字串str引數,這兩個引數非常重要。首先字串為空,prefix指定初始字首,level層級為1,對陣列元素遍歷,然後每個元素判斷是否為陣列,若不為陣列直接拼湊字串,如果為陣列則先生成區塊名稱,將level遞增1後將該陣列遞迴,完成後level遞減1返回到當前層級。
讀取方式使用的策略是先用parse_ini_file函式讀取後,對陣列進行調整,調整方式也是可以使用遞迴來實現,但是既然寫入使用了遞迴,我就用棧模擬了遞迴呼叫實現了非遞迴的寫入方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public function getAppInfo($path){ //按section讀取 $appInfo = parse_ini_file($path, true); //進行同級歸併 $kstack = array(); //使用棧處理同級鍵名 $level = 0; //棧頂元素的層級 $clevel = 0; //遍歷時當前元素層級 array_push($kstack, 'app'); foreach($appInfo as $k=>$v){ if ('app' == $k){ continue; }elseif (empty($appInfo[$k])){ //若section為空則清除 unset($appInfo[$k]); continue; } $clevel = self::getLevel($k); if ($clevel > $level){ array_push($kstack, $k); $level = $clevel; }else{ $levelKeys = array(); //儲存比當前元素層級深的鍵名 $kl = $level; do{ $kk = array_pop($kstack); $levelKeys[] = $kk; $kl = self::getLevel($kk); }while($kl != $clevel); //獲取所有比當前元素層級深的鍵名,最後一個與當前元素同級 $upKey = array_pop($levelKeys); foreach($levelKeys as $downKey){ $realK = explode(":", $downKey); $appInfo[$upKey][$realK[count($realK)-1]] = $appInfo[$downKey]; unset($appInfo[$downKey]); } //將當前元素與取出的最後一個同級元素壓棧 array_push($kstack, $upKey, $k); $level = $kl; } } while(count($kstack) > 1){ $kk = array_pop($kstack); $level = self::getLevel($kk); $kkArr = array($kk); $kkUp = array_pop($kstack); $kkUpl = self::getLevel($kkUp); while ($kkUpl == $level){ $kkArr[] = $kkUp; $kkUp = array_pop($kstack); $kkUpl = self::getLevel($kkUp); } foreach($kkArr as $down){ $realK = explode(':', $down); $appInfo[$kkUp][$realK[count($realK)-1]] = $appInfo[$down]; unset($appInfo[$down]); } array_push($kstack, $kkUp); } return $appInfo['app']; } //getAppInfo helper private function getLevel($key){ $level = 0; while('.' == substr($key, 0, 1)){ $level++; $key = substr($key, 1); } return $level; } |
主要策略是用kstack棧暫存當前區塊下大於等於當前遍歷元素的level的元素的鍵名,最終遍歷完成之後,在對棧進行清理,返回最終清理完的陣列即可。
上述實現的方式只是一種策略,正如我之前提到的可以考慮使用json格式或者xml格式,或者就算使用ini格式也可以按行讀取而不使用parse_ini_file函式,因此實現方式多樣,而且本文這種方式也不一定好,謹在此記錄分享。