在 HTML 中搜索 2 个短语(忽略所有标记)并去除其他所有内容到目前为止,我尝试过什么

2022-08-30 15:37:26

我有一个字符串中存储的html代码,例如:

$html = '
        <html>
        <body>
        <p>Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>.</p>
        </body>
        </html>
        ';

然后我有两个句子存储在变量中:

$begin = 'Hello 進撃の巨人!';
$end = 'Lorem ipsum.';

我想搜索这两个句子,并剥离它们之前和之后的所有内容。所以会变成:$html$html

$html = 'Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>.';

我怎样才能做到这一点?请注意,和 变量没有 html 标记,但 中的句子很可能具有如上所示的标记。$begin$end$html

也许是正则表达式方法?

到目前为止,我尝试过什么

  • 一种方法。问题是句子中包含标签,使得 和 句子不匹配。我可以在跑步之前,但随后我显然会没有标签。strpos()$html$begin$endstrip_tags($html)strpos()$html

  • 搜索变量的一部分,如 ,但这从来都不安全,并且会给出许多匹配项。Hello


答案 1

这是一个简短的,但我相信 - 基于懒惰点匹配正则表达式的工作解决方案(可以通过创建一个更长的,展开的正则表达式来改进,但应该足够了,除非你有非常大的文本块)。

$html = "<html>\n<body>\n<p><p>H<div>ello</div><script></script> <em>進&nbsp;&nbsp;&nbsp;撃の巨人</em>!</p>\nrandom code\nrandom code\n<p>Lorem <span>ipsum<span>.</p>\n</body>\n </html>";
$begin = 'Hello     進撃の巨人!';
$end = 'Lorem ipsum.';
$begin = preg_replace_callback('~\s++(?!\z)|(\s++\z)~u', function ($m) { return !empty($m[1]) ? '' : ' '; }, $begin);
$end = preg_replace_callback('~\s++(?!\z)|(\s++\z)~u', function ($m) { return !empty($m[1]) ? '' : ' '; }, $end);
$begin_arr = preg_split('~(?=\X)~u', $begin, -1, PREG_SPLIT_NO_EMPTY);
$end_arr = preg_split('~(?=\X)~u', $end, -1, PREG_SPLIT_NO_EMPTY);
$reg = "(?s)(?:<[^<>]+>)?(?:&#?\\w+;)*\\s*" .  implode("", array_map(function($x, $k) use ($begin_arr) { return ($k < count($begin_arr) - 1 ? preg_quote($x, "~") . "(?:\s*(?:<[^<>]+>|&#?\\w+;))*" : preg_quote($x, "~"));}, $begin_arr, array_keys($begin_arr)))
        . "(.*?)" . 
        implode("", array_map(function($x, $k) use ($end_arr) { return ($k < count($end_arr) - 1 ? preg_quote($x, "~") . "(?:\s*(?:<[^<>]+>|&#?\\w+;))*" : preg_quote($x, "~"));}, $end_arr, array_keys($end_arr))); 
echo $reg .PHP_EOL;
preg_match('~' . $reg . '~u', $html, $m);
print_r($m[0]);

观看 IDEONE 演示

算法:

  • 通过将分隔符字符串拆分为单个字形(因为这些可以是Unicode字符,我建议使用)并通过添加可选的标记匹配模式来内爆,从而创建动态正则表达式模式。preg_split('~(?<!^)(?=\X)~u', $end)(?:<[^<>]+>)?
  • 然后,在匹配任何字符(包括换行符)时启用 DOTALL 模式,并将匹配从前导分隔符到尾随分隔符的 0 个以上字符。(?s)..*?

正则表达式详细信息

  • '~(?<!^)(?=\X)~u匹配除每个字形之前的字符串开头以外的所有位置
  • (示例最终正则表达式) + + - 前导和尾随分隔符,其中包含用于标记匹配的可选子模式,并且内部有一个(捕获可能不是必需的)。(?s)(?:<[^<>]+>)?(?:&#?\w+;)*\s*H(?:\s*(?:<[^<>]+>|&#?\w+;))*e(?:\s*(?:<[^<>]+>|&#?\w+;))*l(?:\s*(?:<[^<>]+>|&#?\w+;))*l(?:\s*(?:<[^<>]+>|&#?\w+;))*o(?:\s*(?:<[^<>]+>|&#?\w+;))* (?:\s*(?:<[^<>]+>|&#?\w+;))*進(?:\s*(?:<[^<>]+>|&#?\w+;))*撃(?:\s*(?:<[^<>]+>|&#?\w+;))*の(?:\s*(?:<[^<>]+>|&#?\w+;))*巨(?:\s*(?:<[^<>]+>|&#?\w+;))*人(?:\s*(?:<[^<>]+>|&#?\w+;))*\!(?:\s*(?:<[^<>]+>|&#?\w+;))*(.*?)L(?:\s*(?:<[^<>]+>|&#?\w+;))*o(?:\s*(?:<[^<>]+>|&#?\w+;))*r(?:\s*(?:<[^<>]+>|&#?\w+;))*e(?:\s*(?:<[^<>]+>|&#?\w+;))*m(?:\s*(?:<[^<>]+>|&#?\w+;))* (?:\s*(?:<[^<>]+>|&#?\w+;))*i(?:\s*(?:<[^<>]+>|&#?\w+;))*p(?:\s*(?:<[^<>]+>|&#?\w+;))*s(?:\s*(?:<[^<>]+>|&#?\w+;))*u(?:\s*(?:<[^<>]+>|&#?\w+;))*m(?:\s*(?:<[^<>]+>|&#?\w+;))*\.(.*?)
  • ~u修饰符是必需的,因为要处理 Unicode 字符串。
  • 更新:要考虑 1 个以上的空格,和 模式中的任何空格都可以替换为子模式,以匹配输入字符串中任何类型的 1+ 空格字符。beginend\s+
  • 更新 2:辅助和必需的,以考虑输入字符串中的 1+ 空格。$begin = preg_replace('~\s+~u', ' ', $begin);$end = preg_replace('~\s+~u', ' ', $end);
  • 要考虑 HTML 实体,请向可选部分添加另一个子模式:,它也将匹配并像实体一样。它还在前面附加了匹配可选的空格,并用(可以是零个或多个)进行量化。&#?\\w+;&nbsp;&#123;\s**

答案 2

我真的很想写一个正则表达式解决方案。但是我之前有一些很好和复杂的解决方案。所以,这是一个非正则表达式解决方案。

简短说明:主要问题是保留HTML标签。如果去除HTML标签,我们可以很容易地搜索文本。所以:剥离这些!我们可以很容易地在剥离的内容中搜索,并生成我们想要剪切的子字符串。然后,尝试从 HTML 中删除此子字符串,同时保留标记。

优势:

  • 搜索很容易,独立于HTML,如果需要,您也可以使用正则表达式进行搜索
  • 需求是可扩展的:您可以轻松添加完整的多字节支持,对实体和空格折叠的支持等
  • 相对较快(有可能,直接正则表达式可以更快)
  • 不接触原始HTML,并适应其他标记语言

此方案的静态实用程序类:

class HtmlExtractUtil
{

    const FAKE_MARKUP = '<>';
    const MARKUP_PATTERN = '#<[^>]+>#u';

    static public function extractBetween($html, $startTextToFind, $endTextToFind)
    {
        $strippedHtml = preg_replace(self::MARKUP_PATTERN, '', $html);
        $startPos = strpos($strippedHtml, $startTextToFind);
        $lastPos = strrpos($strippedHtml, $endTextToFind);

        if ($startPos === false || $lastPos === false) {
            return "";
        }

        $endPos = $lastPos + strlen($endTextToFind);
        if ($endPos <= $startPos) {
            return "";
        }

        return self::extractSubstring($html, $startPos, $endPos);
    }

    static public function extractSubstring($html, $startPos, $endPos)
    {
        preg_match_all(self::MARKUP_PATTERN, $html, $matches, PREG_OFFSET_CAPTURE);
        $start = -1;
        $end = -1;
        $previousEnd = 0;
        $stripPos = 0;
        $matchArray = $matches[0];
        $matchArray[] = [self::FAKE_MARKUP, strlen($html)];
        foreach ($matchArray as $match) {
            $diff = $previousEnd - $stripPos;
            $textLength = $match[1] - $previousEnd;
            if ($start == (-1)) {
                if ($startPos >= $stripPos && $startPos < $stripPos + $textLength) {
                    $start = $startPos + $diff;
                }
            }
            if ($end == (-1)) {
                if ($endPos > $stripPos && $endPos <= $stripPos + $textLength) {
                    $end = $endPos + $diff;
                    break;
                }
            }
            $tagLength = strlen($match[0]);
            $previousEnd = $match[1] + $tagLength;
            $stripPos += $textLength;
        }

        if ($start == (-1)) {
            return "";
        } elseif ($end == (-1)) {
            return substr($html, $start);
        } else {
            return substr($html, $start, $end - $start);
        }
    }

}

用法:

$html = '
<html>
<body>
<p>Any string before</p>
<p>Hello <em>進撃の巨人</em>!</p>
random code
random code
<p>Lorem <span>ipsum<span>.</p>
<p>Any string after</p>
</body>
</html>
';
$startTextToFind = 'Hello 進撃の巨人!';
$endTextToFind = 'Lorem ipsum.';

$extractedText = HtmlExtractUtil::extractBetween($html, $startTextToFind, $endTextToFind);

header("Content-type: text/plain; charset=utf-8");
echo $extractedText . "\n";

推荐