需求是为一个**数组对象的数据按行存储到文件,需要键值对区分层级,对每个对象描述清晰。类似的格式如下:
上图中的数据对应的就是如下的数组(php 语言):$arr = array(
'10003' => array(
'id' => 10003,
'tokentime' => 400),
'10005' => array(
'id' => 10005,
'cookie' => array( 'num' => 20
),
'vcode' => array(
'length' => 6
),
),
);
每个id对应一条数据对象,但是这个数据对象可能是**的数据,需要用文件对用户可读性友好的存储起来,这里很容易就可以想到使用json格式也是可以的,将数组json_encode后存储到文件,读取的时候使用json_decode解析为数组或对象即可。这确实是一种方便的办法,但是本案例使用的就是这中按行存储的格式。因此需要自行实现解析与存储的过程。
通过上述格式的分析,可以很容易联想到ini文件的格式,中括号区分的是区块,下面对应键值对,但是这里有一个注意点是每个区块开头的点浩(“.”)代表了区块的层级关系,正式这种层级关系表明了数组的**性和认为可读性,也是非常巧妙的一种方式。
但是,需要考虑的一个细节问题是,php的parse_ini_file函数对ini文件读取时,如果区块名称相同,那么会新的区块内容会覆盖之前的区块。php官方手册中介绍如下: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函数,但是对于区块策略使用父级的键名+自身键名的方式以避免重复。
写入方式如下,直接先看代码: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函数读取后,对数组进行调整,调整方式也是可以使用递归来实现,但是既然写入使用了递归,我就用栈模拟了递归调用实现了非递归的写入方式: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函数,因此实现方式多样,而且本文这种方式也不一定好,谨在此记录分享。