如何从PHP中的HTML列表中提取结构化文本?

时间:2012-12-29 17:47:14

标签: php html-parsing

我有这个字符串:

<ul>
  <li id="1">Page 1</li>
  <li id="2">Page 2
    <ul>
      <li id="3">Sub Page A</li>
      <li id="4">Sub Page B</li>
      <li id="5">Sub Page C
        <ul>
          <li id="6">Sub Sub Page I</li>
        </ul>
      </li>
    </ul>
  </li>
  <li id="7">Page 3
    <ul>
      <li id="8">Sub Page D</li>
    </ul>
  </li>
  <li id="9">Page 4</li>
</ul>

我希望用PHP分解每个信息并使其像:

----------------------------------
| ID | ORDER | PARENT | CHILDREN |
----------------------------------
|  1 |   1   |   0   |     0     |
|  2 |   2   |   0   |   3,4,5   |
|  3 |   1   |   2   |     0     |
|  4 |   2   |   2   |     0     |
|  5 |   3   |   2   |     6     |
|  6 |   1   |   5   |     0     |
|  7 |   3   |   0   |     8     |
|  8 |   1   |   7   |     0     |
|  9 |   4   |   0   |     0     |
----------------------------------

有关额外信息,此列表对我来说意味着:

ID 1是第1页(第1页),有0个父母和0个孩子,

ID 2是第2页(第2页),有0个父母和子女ID 3,4,5,

ID 3是第1个(子页A),父ID为2,子项为0,

ID 4是第2个(子页面B),父母ID为2,孩子为0,

ID 5是第3个(子网页C),父ID为2,子ID为6,

ID 6是第1个(子页面I),父亲ID为5,儿童为0,

ID 7是第3页(第3页),有0个父母和孩子ID 8,

ID 8是第1个(子页面I),父ID为7,子项为0,

ID 9是第4页(第4页),有0个父母和0个孩子。

如果这太难了,任何人都可以使用其他方法从这个字符串中获取信息吗?

3 个答案:

答案 0 :(得分:2)

这不是“字符串”,而是HTML。您需要使用DOMDocumentsimple_html_dom等HTML解析器。

请参阅http://htmlparsing.com/php.html

上的示例

答案 1 :(得分:1)

你可以在这里分解问题。一件事是解析HTML,这在DOMDocumentDOMXpath最容易完成。这是在另一个xpath表达式/查询的结果的上下文中运行一些映射。听起来可能有点复杂,但事实并非如此。在更简化的变体中,您可以在之前的Get parent element through xpath and all child elements答案中找到这一点。

在你的情况下,这有点复杂,一些伪代码。我添加了标签,因为它使事情更加明显以用于演示目的:

foreach //li ::
    ID       := string(./@id)
    ParentID := string(./ancestor::li[1]/@id)
    Label    := normalize-space(./text()[1])

如图所示,这仅返回裸数据。你也有秩序和孩子。通常不需要儿童名单(无论如何我都保留在这里)。 Order值和Children值之间的相似之处在于它们是从上下文中检索的。

E.g。在按文档顺序遍历//li节点列表时,如果每个ParentID保留一个计数器,则可以对每个子节点的顺序进行编号。

与子项目类似,就像计数器一样,需要在迭代列表时构建该值。只有在最后,每个列表项的正确值才可用。

所以这两个值都在上下文中,我以ParentID:$parents键入的数组的形式创建该上下文。每个ID它将包含两个条目:0包含Order的计数器,1包含一个数组以保存Children的ID(如果有的话)。

  

注意: 从技术上讲,这不完全正确。 Order和Children也应该在纯xpath中表达,我在这个例子中没有这样做,以显示如何添加你自己的非xpath上下文,例如如果您想要不同的订购或儿童处理。

足够理论。考虑标准设置:

$doc = new DOMDocument();
$doc->loadHTML($html);
$xp = new DOMXPath($doc);

所述映射包括它的上下文可以写成匿名函数:

$parents = [];

$map = function (DOMElement $li) use ($xp, &$parents) {

    $id       = (int)$xp->evaluate('string(./@id)', $li);
    $parentId = (int)$xp->evaluate('string(./ancestor::li[1]/@id)', $li);
    $label    = $xp->evaluate('normalize-space(./text()[1])', $li);

    isset($parents[$parentId][0]) ? $parents[$parentId][0]++ : ($parents[$parentId][0] = 1);
    $order                   = $parents[$parentId][0];
    $parents[$parentId][1][] = $id;
    isset($parents[$id][1]) || $parents[$id][1] = [];

    return array($id, $label, $order, $parentId, &$parents[$id][1]);
};

正如您所看到的,它首先包含对伪代码中的值的检索,以及第二部分中对上下文值的处理。它只是初始化ID / ParentID的上下文(如果它还不存在)。

需要应用此映射:

$result = [];
foreach ($xp->query('//li') as $li) {
    list($id) = $array = $map($li);
    $result[$id] = $array;
}

这将使$result包含项目列表和$parents上下文数据。使用引用时,Children值现在需要被破坏,然后可以删除引用:

foreach ($parents as &$parent) {
    $parent[1] = implode(',', $parent[1]);
}
unset($parent, $parents);

然后使$result成为可以输出的最终结果:

echo '+----+----------------+-------+--------+----------+
| ID |     LABEL      | ORDER | PARENT | CHILDREN |
+----+----------------+-------+--------+----------+
';
foreach ($result as $line) {
    vprintf("| %' 2d | %' -14s |  %' 2d   |   %' 2d   | %-8s |\n", $line);
}
echo '+----+----------------+-------+--------+----------+
';

然后看起来像:

+----+----------------+-------+--------+----------+
| ID |     LABEL      | ORDER | PARENT | CHILDREN |
+----+----------------+-------+--------+----------+
|  1 | Page 1         |   1   |    0   |          |
|  2 | Page 2         |   2   |    0   | 3,4,5    |
|  3 | Sub Page A     |   1   |    2   |          |
|  4 | Sub Page B     |   2   |    2   |          |
|  5 | Sub Page C     |   3   |    2   | 6        |
|  6 | Sub Sub Page I |   1   |    5   |          |
|  7 | Page 3         |   3   |    0   | 8        |
|  8 | Sub Page D     |   1   |    7   |          |
|  9 | Page 4         |   4   |    0   |          |
+----+----------------+-------+--------+----------+

您可以找到Demo online here

答案 2 :(得分:0)

我留下第二个答案,因为这次演示了如何使用单一映射(伪代码):

foreach //li ::
    ID       := string(./@id)
    ParentID := string(./ancestor::li[1]/@id)
    Label    := normalize-space(./text()[1])
    Order    := count(./preceding-sibling::li)+1
    Children := implode(",", ./ul/li/@id)

因为这可以按照每个li节点完成,无论顺序如何,这可能是Iterator的完美匹配,这里是当前函数:

public function current() {

    return [
        'ID'       => $this->evaluate('number(./@id)'),
        'label'    => $this->evaluate('normalize-space(./text()[1])'),
        'order'    => $this->evaluate('count(./preceding-sibling::li)+1'),
        'parentID' => $this->evaluate('number(concat("0", ./ancestor::li[1]/@id))'),
        'children' => $this->implodeNodes(',', './ul/li/@id'),
    ];
}

完整示例(Demo)输出和代码:

+----+----------------+-------+--------+----------+
| ID |     LABEL      | ORDER | PARENT | CHILDREN |
+----+----------------+-------+--------+----------+
|  1 | Page 1         |   1   |    0   |          |
|  2 | Page 2         |   2   |    0   | 3,4,5    |
|  3 | Sub Page A     |   1   |    2   |          |
|  4 | Sub Page B     |   2   |    2   |          |
|  5 | Sub Page C     |   3   |    2   | 6        |
|  6 | Sub Sub Page I |   1   |    5   |          |
|  7 | Page 3         |   3   |    0   | 8        |
|  8 | Sub Page D     |   1   |    7   |          |
|  9 | Page 4         |   4   |    0   |          |
+----+----------------+-------+--------+----------+


class HtmlListIterator extends IteratorIterator
{
    private $xpath;

    public function __construct($html) {

        $doc = new DOMDocument();
        $doc->loadHTML($html);
        $this->xpath = new DOMXPath($doc);
        parent::__construct($this->xpath->query('//li'));
    }

    private function evaluate($expression) {

        return $this->xpath->evaluate($expression, parent::current());
    }

    private function implodeNodes($glue, $expression) {

        return implode(
            $glue, array_map(function ($a) {

                return $a->nodeValue;
            }, iterator_to_array($this->evaluate($expression, parent::current())))
        );
    }

    public function current() {

        return [
            'ID'       => $this->evaluate('number(./@id)'),
            'label'    => $this->evaluate('normalize-space(./text()[1])'),
            'order'    => $this->evaluate('count(./preceding-sibling::li)+1'),
            'parentID' => $this->evaluate('number(concat("0", ./ancestor::li[1]/@id))'),
            'children' => $this->implodeNodes(',', './ul/li/@id'),
        ];
    }
}

print_result(new HtmlListIterator($html));

function print_result($result) {

    echo '+----+----------------+-------+--------+----------+
| ID |     LABEL      | ORDER | PARENT | CHILDREN |
+----+----------------+-------+--------+----------+
';
    foreach ($result as $line) {
        vprintf("| %' 2d | %' -14s |  %' 2d   |   %' 2d   | %-8s |\n", $line);
    }
    echo '+----+----------------+-------+--------+----------+
';
}