Я хотел бы преобразовать приведенный ниже текст во вложенный массив, что-то похожее на структуру базы данных MPTT.
Я получаю данные из сценария оболочки и должен отображать его на веб-сайте. Не контролируйте формат: /
Существует много информации о массиве -> список, но не так много происходит другим способом.
Любой вход был бы оценен, спасибо.
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
У вас уже есть отсортированный список деревьев. Каждая следующая строка является либо дочерним элементом предыдущей строки, либо родным братом. Таким образом, вы можете обрабатывать список, получать имя элемента, получать уровень, в котором элемент находится под его отступом, и создать из него элемент.
1 Line <=> 1 Element (level, name)
Таким образом, каждый элемент имеет имя и ноль или более детей. Из ввода можно также сказать, на каком уровне он принадлежит.
Элемент может быть представлен как массив, в котором первым значением является имя, а второе значение – это массив для дочерних элементов.
По мере сортировки списка мы можем использовать простую карту, которая на уровне является псевдонимом для детей определенного уровня. Таким образом, с уровнем, который имеет каждый элемент, мы можем добавить его в стек:
$self = array($element, array()); $stack[$level][] = &$self; $stack[$level + 1] = &$self[1];
Как показывает этот пример кода, стек / карта для текущего уровня получает $self
мере добавления детей:
$stack[$level][] = &$self;
Стек для уровня один выше, get – это ссылка на детей $self
(индекс 1
):
$stack[$level + 1] = &$self[1];
Итак, теперь для каждой строки нам нужно найти уровень. Как показывает этот стек, уровень последовательно пронумерован: 0, 1, 2, ...
но на входе это всего лишь несколько пробелов.
Небольшой вспомогательный объект может выполнять работу по сбору / группировке количества символов в строке до уровней, заботясь о том, что – если уровень еще не существует для отступа – он добавляется, но только если выше.
Это решает проблему, которая на вашем входе не имеет отношения 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
и return – за строку – отступы и имя. Поскольку я хочу иметь больше комфорта, я помещаю его в функцию, которая всегда возвращает мне массив, поэтому я могу использовать его в итераторе:
$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)
Имя – это строка здесь, дочерние элементы – массив. Итак, это уже сделано технически с массивом / деревом. Но это довольно обременительно, потому что вы хотите иметь возможность выводить древовидную структуру. Вы можете сделать это, выполняя рекурсивные вызовы функций или реализуя рекурсивный итератор.
Позвольте мне привести пример рекурсивного итератора:
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 в сочетании с рекурсивным итератором, см. Следующий вопрос, в котором есть пример для вывода HTML на основе <ul><li>
:
Итак, как это выглядит как все вместе? Код для просмотра сразу как сущность на github .
В отличие от моего предыдущего ответа, который довольно долгий и объясняет все этапы, можно также сделать то же самое, но более сжатым.
strtok
preg_match
затем «на» линии, делая отображение более имманентным Levels
могут быть сжаты в массив, считающийся само собой разумеющимся, что вход правильный. На этот раз для вывода это рекурсивная функция, а не итератор, которая выливает вложенный список <ul>
. Пример кода ( демо ):
// 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"; }
с// 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"; }
с// 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"; }
Изменить: Следующее идет еще на один шаг, чтобы полностью удалить массив деревьев и сделать вывод напрямую. Это немного безумно, потому что оно смешивает переупорядочивание данных и выходных данных, что затягивает вещи, поэтому их легко изменить. Также предыдущий пример уже выглядит загадочным, это выходит за рамки добра и зла ( Демо ):
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>
необходимо открыть или закрыть вокруг текущего элемента.