将制表符/空格分隔的行转换为嵌套数组

时间:2012-03-21 01:02:33

标签: php multidimensional-array

我想将下面的文本转换为嵌套数组,就像使用MPTT数据库结构一样。

我从shell脚本获取数据,需要在网站上显示。无法控制格式:/

有很多关于数组的信息 - >列表,但没有太多其他方式。

感谢任何意见,谢谢。

cat, true cat
       => domestic cat, house cat, Felis domesticus, Felis catus
           => kitty, kitty-cat, puss
           => mouser
           => alley cat
           => tom, tomcat
               => gib
           => Angora, Angora cat
           => Siamese cat, Siamese
               => blue point Siamese
       => wildcat
           => sand cat
           => European wildcat, catamountain, Felis silvestris
           => cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
           => ocelot, panther cat, Felis pardalis
           => manul, Pallas's cat, Felis manul
           => lynx, catamount
               => common lynx, Lynx lynx
               => Canada lynx, Lynx canadensis

2 个答案:

答案 0 :(得分:5)

您已经在这里找到了排序树列表。每一行都是前一行的子项或兄弟。因此,您可以处理列表,获取项目的名称,通过缩进获得项目所在的级别,并从中创建一个元素。

1 Line <=> 1 Element (level, name)

所以每个元素都有一个名字,零个或多个孩子。从输入中也可以看出它属于哪个级别。

一个元素可以表示为一个数组,其中第一个值是名称,第二个值是子元素的数组。

当列表被排序时,我们可以使用一个简单的映射,每个级别是某个级别的子级别的别名。因此,对于每个元素具有的级别,我们可以将其添加到堆栈中:

    $self = array($element, array());
    $stack[$level][] = &$self;
    $stack[$level + 1] = &$self[1];

正如此代码示例所示,当儿童添加时,当前级别的堆栈/地图正在获得$self

    $stack[$level][] = &$self;

第一级的堆栈,得到$self(索引1)子项的引用:

    $stack[$level + 1] = &$self[1];

所以现在每行,我们需要找到关卡。正如此堆栈所示,级别按顺序编号:0, 1, 2, ...但在输入中它只是一些空格。

一个小帮助对象可以完成将字符串中的字符数收集/分组到各个级别的工作,注意 - 如果缩进不存在一个级别 - 它会被添加,但只有在更高的情况下才会添加。 / p>

这解决了在输入中缩进的大小与其索引之间没有1:1关系的问题。至少不是一个明显的。

此助手对象是名为Levels的示例,并实现__invoke以提供缩进级别,同时在必要时透明地添加新级别:

$levels = new Levels();
echo $levels(''); # 0
echo $levels('    '); # 1
echo $levels('    '); # 1
echo $levels('      '); # 2
echo $levels(' '); # Throws Exception, this is smaller than the highest one

所以现在我们可以将缩进变成这个级别。该级别允许我们运行堆栈。堆栈允许构建树。细

逐行解析可以使用正则表达式轻松完成。因为我很懒,我只使用preg_match_all并返回 - 每行 - 缩进和名称。因为我想要更多的安慰,我将它包装成一个总是返回一个数组的函数,所以我可以在迭代器中使用它:

$matches = function($string, $pattern)
{
    return preg_match_all($pattern, $string, $matches, PREG_SET_ORDER)
        ? $matches : array();
};

在输入上使用类似

的模式
/^(?:(\s*)=> )?(.*)$/m

每行会给我一个数组,即:

array(whole_line, indent, name)

你看到这里的模式?它接近

1 Line <=> 1 Element (level, name)

Levels对象的帮助下,可以映射,所以只需要调用映射函数:

function (array $match) use ($levels) {
    list(, $indent, $name) = $match;
    $level = $levels($indent);
    return array($level, $name);
};

array(line, indent, name)array(level, name)。要使其可访问,可由另一个可注入Levels的函数返回:

$map = function(Levels $levels) {
    return function ...
};
$map = $map(new Levels());

所以,一切都是为了从所有行读取。但是,这需要放在树中。记住添加到堆栈:

function($level, $element) use (&$stack) {
    $self = array($element, array());
    $stack[$level][] = &$self;
    $stack[$level + 1] = &$self[1];
};

$element是这里的名字)。这实际上需要堆栈,堆栈实际上是树。因此,让我们创建另一个返回此函数的函数,并允许将每一行推入堆栈以构建树:

$tree = array();
$stack = function(array &$tree) {
    $stack[] = &$tree;
    return function($level, $element) use (&$stack) {
        $self = array($element, array());
        $stack[$level][] = &$self;
        $stack[$level + 1] = &$self[1];
    };
};
$push = $stack($tree);

所以最后要做的就是只处理一个元素:

foreach ($matches($input, '/^(?:(\s*)=> )?(.*)$/m') as $match) {
    list($level, $element) = $map($match);
    $push($level, $element);
}

所以现在使用$input这会创建一个数组,只有(根)子节点在它的第一级,然后有一个array,每个节点有两个条目:

array(name, children)

Name在这里是一个字符串,children是一个数组。所以这已经在技术上完成了数组/树的列表。但它相当繁琐,因为您希望能够输出树结构。您可以通过执行递归函数调用或实现递归迭代器来实现。

让我给出一个Recursive Iterator示例:

class TreeIterator extends ArrayIterator implements RecursiveIterator
{
    private $current;

    public function __construct($node)
    {
        parent::__construct($node);
    }

    public function current()
    {
        $this->current = parent::current();
        return $this->current[0];
    }

    public function hasChildren()
    {
        return !empty($this->current[1]);
    }

    public function getChildren()
    {
        return new self($this->current[1]);
    }
}

这只是一个数组迭代器(因为所有节点都是一个数组,以及所有子节点),对于当前节点,它返回名称。如果要求孩子,它会检查是否有孩子,并再次提供TreeIterator。这使得使用它很简单,例如输出为文本:

$treeIterator = new RecursiveTreeIterator(
    new TreeIterator($tree));

foreach ($treeIterator as $val) echo $val, "\n";

输出:

\-cat, true cat
  |-domestic cat, house cat, Felis domesticus, Felis catus
  | |-kitty, kitty-cat, puss
  | |-mouser
  | |-alley cat
  | |-tom, tomcat
  | | \-gib
  | |-Angora, Angora cat
  | \-Siamese cat, Siamese
  |   \-blue point Siamese
  \-wildcat
    |-sand cat
    |-European wildcat, catamountain, Felis silvestris
    |-cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
    |-ocelot, panther cat, Felis pardalis
    |-manul, Pallas's cat, Felis manul
    \-lynx, catamount
      |-common lynx, Lynx lynx
      \-Canada lynx, Lynx canadensis

如果您正在寻找与递归迭代器结合使用的更多HTML输出控件,请参阅以下问题,其中包含基于<ul><li>的HTML输出的示例:

那么这一起看起来怎么样?要一次审核的代码为a gist on github

答案 1 :(得分:3)

与我以前的答案相比,这个答案相当长,并解释了所有步骤,也可以做同样但更多的压缩。

  • 可以使用strtok
  • 进行分割
  • preg_match然后“在”线上使映射更加内在
  • 可以将Levels压缩成一个理所当然的输入是正确的数组。

这次输出,它是一个递归函数,而不是迭代器,它会溢出嵌套的<ul>列表。示例代码(Demo):

// build tree
$tree = $levels = array();
$stack[1] = &$tree;
for ($line = strtok($input, $token = "\n"); $line; $line = strtok($token)) {
    if (!preg_match('/^(?:(\s*)=> )?(.*)$/', $line, $self)) {
        continue;
    }
    array_shift($self);
    $indent = array_shift($self);
    $level = @$levels[$indent] ? : $levels[$indent] = count($levels) + 1;
    $stack[$level][] = &$self;
    $stack[$level + 1] = &$self[];
    unset($self);
}
unset($stack);

// output
tree_print($tree);
function tree_print(array $tree, $in = '') {
    echo "$in<ul>\n";
    $i = $in . '  ';
    foreach ($tree as $n)
        printf("</li>\n", printf("$i<li>$n[0]") && $n[1] && printf($i, printf("\n") & tree_print($n[1], "$i  ")));

    echo "$in</ul>\n";
}

编辑: 以下内容甚至更进一步完全删除树阵列并直接执行输出。这有点疯狂,因为它混合了数据和输出的重新排序,这使得事物变得紧密,所以不容易改变。前面的例子看起来也很神秘,这超出了善恶(Demo):

echo_list($input);

function echo_list($string) {
    foreach ($m = array_map(function($v) use (&$l) {
        return array(@$l[$i = &$v[1]] ? : $l[$i] = count($l) + 1, $v[2]);
    }, preg_match_all('/^(?:(\s*)=> )?(.*)$/m', $string, $m, PREG_SET_ORDER) ? $m : array()) as $i => $v) {
        $pi = str_repeat("    ", $pl = @$m[$i - 1][0]); # prev
        $ni = str_repeat("    ", $nl = @$m[$i + 1][0]); # next
        (($v[0] - $pl) > 0) && printf("$pi<ul>\n");     # is child of prev
        echo '  ' . str_repeat("    ", $v[0] - 1), "<li>$v[1]"; # output self
        if (!$close = (($nl - $v[0]) * -1)) echo "</li>\n"; # has sibling
        else if ($close < 0) echo "\n";                     # has children
        else for (printf("</li>\n$ni" . str_repeat("    ", $close - 1) . "</ul>\n"); --$close;) # is last child
                echo $ni, $nn = str_repeat("    ", $close - 1), "  </li>\n",
                     $ni, $nn, "</ul>\n";
    }
}

再次删除strtok,然后回到使用preg_match_all的想法。此外,它还存储了所有已解析的行,因此可以查看后面和后面的内容,以确定需要在当前元素周围打开或关闭多少<ul>个元素。

相关问题