如何获取 HTML 内容中最中间元素的字符串位置?

How to get the string position of the middle-most element within HTML content?

我正在处理来自所见即所得编辑器的 HTML 格式的新闻文章,我需要找到它的中间部分,但是在 visual/HTML 上下文中,这意味着一个空的地方在两个根元素之间。比如说,如果您想将文章分成两页,那么每页的段落数应尽可能相同。

所有的根元素似乎都以段落的形式出现,这很容易计数,一个简单的

$p_count = substr_count($article_text, '<p');

Returns 开头段落标记的总数,然后我可以查找第 ($p_count/2) 次出现的段落的 strpos

但问题在于嵌入的推文,其中包含段落,有时出现在 blockquote > p 下,有时出现在 center > blockquote > p.

所以我求助于 DOMDocument。这个小片段给了我中间的第 n 个元素(即使元素是 div 而不是段落,这很酷):

$dom = new DOMDocument();
$dom->loadHTML($article_text);
$body = $dom->getElementsByTagName('body');
$rootNodes = $body->item(0)->childNodes;

$empty_nodes = 0;
foreach($rootNodes as $node) {
    if($node->nodeType === XML_TEXT_NODE && strlen(trim($node->nodeValue)) === 0) {
        $empty_nodes++;
    }
}

$total_elements = $rootNodes->length - $empty_nodes;
$middle_element = floor($total_elements / 2);

但是我现在如何在我的原始 HTML 源中找到这个中间元素的字符串偏移量,以便我可以指向文章文本字符串中的这个中间位置?特别是考虑到 DOMDocument 将我给它的 HTML 转换成一个完整的 HTML 页面(带有文档类型和头部等等),所以它的输出 HTML 比我的大原创HTML文章来源。

好的,我解决了。

我所做的是匹配文章中的所有 HTML 标签,使用 preg_match_allPREG_OFFSET_CAPTURE 标志,它会记住模式匹配的字符偏移量。然后我依次遍历所有这些,并计算我在哪个深度;如果它是一个开始标签,我计算深度 +1,并计算结束 -1(注意 self-closing 标签)。每次在结束标记后深度变为零,我都将其视为关闭的另一个根元素。如果最后我到达深度 0,我假设我算对了。

现在,我可以将我计数的根元素的数量除以 2 得到 middle-ish 个(奇数为 +-1),然后查看该元素的偏移量preg_match_all 先前报告的指数。

如果有人需要做同样的事情,完整的代码如下。

如果使用正则表达式编写 is_self_closing() 函数然后检查 in_array($self_closing_tags) 而不是 foreach 循环,它可能会加速,但在我的情况下它没有足以让我费心了。

function calculate_middle_of_article(string $text, bool $debug=false): ?int {
    
    function is_self_closing(string $input, array $self_closing_tags): bool {
        foreach($self_closing_tags as $tag) {
            if(substr($input, 1, strlen($tag)) === $tag) {
                return true;
            }
        }
        return false;
    }

    $self_closing_tags = [
        '!--',
        'area',
        'base',
        'br',
        'col',
        'embed',
        'hr',
        'img',
        'input',
        'link',
        'meta',
        'param',
        'source',
        'track',
        'wbr',
        'command',
        'keygen',
        'menuitem',
    ];

    $regex = '/<("[^"]*"|\'[^\']*\'|[^\'">])*>/';
    preg_match_all($regex, $text, $matches, PREG_OFFSET_CAPTURE);
    $debug && print count($matches[0]) . " tags found   \n";

    $root_elements = [];
    $depth = 0;

    foreach($matches[0] as $match) {
        if(!is_self_closing($match[0], $self_closing_tags)) {
            $depth+= (substr($match[0], 1, 1) === '/') ? -1 : 1;
        }
        $debug && print "level {$depth} after tag: " . htmlentities($match[0]) . "\n";

        if($depth === 0) {
            $root_elements[]= $match;
        }
    }

    $ok = ($depth === 0);
    $debug && print ($ok ? 'ok' : 'not ok') . "\n";

    // has to end at depth zero to confirm counting is correct
    if(!$ok) {
        return null;
    }

    $debug && print count($root_elements) . " root elements\n";

    $element_index_at_middle = floor(count($root_elements)/2);
    $half_char = $root_elements[$element_index_at_middle][1];
    $debug && print "which makes the half the {$half_char}th character at the {$element_index_at_middle}th element\n";

    return $half_char;
}