使用正则表达式删除相同类型的 html 嵌套标签的最终解决方案?

mil*_*fer 3 html php regex dom nested

我已经花了几天时间试图找到一个使用正则表达式的解决方案(在有人说之前:我知道我应该使用 PHP DOM 文档库或类似的东西,但让我们把它作为一个理论问题),寻找答案,我终于来了与我将在这个问题结束时展示的内容相提并论。

以下只是我之前尝试过的很多事情的总结。

首先,我所说的相同类型的嵌套标签是指:

Text outside any div
<div id="my_id"> bla bla
  <div>
  bla bla bla
    <div style="some style here">
      lalalalala
     </div>
   </div>
    I'm trapped in a div!
</div>
more text outside divs

<div>more divs here!
       <div id="justbeingannoying">radiohead rules</div>
</div>
Run Code Online (Sandbox Code Playgroud)

现在想象我想使用正则表达式删除所有 div及其内容。所以预期的结果是:

Text outside any div
more text outside divs
Run Code Online (Sandbox Code Playgroud)

第一个想法是匹配一切。以下正则表达式匹配具有属性(样式、id 等)的 div 标签:

/<div[^>]*>.*<\/div>/sig
Run Code Online (Sandbox Code Playgroud)

这个问题,当然,这将匹配一切的第一个“<DIV”的开始和最后一个“</ DIV>”之间,所以它将匹配“更多的文本之外的div”太(点击此处查看:HTTPS:/ /regex101.com/r/iR8mY2/1),这不是我们(我)想要的。

这可以使用U 修饰符(Ungreedy)解决

/<div[^>]*>.*<\/div>/sigU
Run Code Online (Sandbox Code Playgroud)

但是接下来我们会遇到比我们想要的的问题:它只会从第一个“< div”到第一个“”匹配(因此,如果我们删除匹配项,除了一些不匹配的标签之外,还会有文本“我被困在一个 div 中!”,这是我们不想要的)。

所以,我找到了一个解决方案,它就像嵌套括号、方括号等的魅力:

/\[([^\[\]]*+|(?R))*\]/si
Run Code Online (Sandbox Code Playgroud)

基本上,它的作用是找到一个左方括号,然后匹配任何 * 既不是左方括号也不是右方括号 * 或它的递归结构,找到一个右方括号。

我现在的工作是一个糟糕的解决方案:基本上,首先我用方括号替换所有开始标签(由于其他原因,它不能在我的代码中),然后是结束方括号的结束标签,然后我使用之前的正则表达式。我知道,这不是一个非常优雅的解决方案。

问题是我真的很想知道如何只用一个正则表达式就可以做到这一点。这似乎比以前的正则表达式的“[”和“]”是由HTML标记替换明显工作。但并没有那么容易。问题是字符的否定 ("[^.......]" 对像 "div" 这样的字符串不起作用。似乎可以通过以下方式实现类似的东西:

.+?(?=<div>)
Run Code Online (Sandbox Code Playgroud)

当然,结束标记也是如此

.+?(?=<\/div>
Run Code Online (Sandbox Code Playgroud)

这就是我或多或少地到达这个正则表达式的方式

/<div((.+?(?=<\/div>)|.+?(?=<div>))|(?R))*<\/div>/gis
Run Code Online (Sandbox Code Playgroud)

这与我之前介绍的第一个正则表达式完全一样:https : //regex101.com/r/yU8pV3/1

所以,这是我的问题:那个正则表达式有什么问题?

谢谢!

Wik*_*żew 5

免责声明

由于这个问题得到了积极的回应,我将发布一个答案,解释您的方法有什么问题,并将展示如何匹配不是特定文本的文本。

但是,我想强调的是:不要使用它来解析真实的、任意的 HTML 代码,因为正则表达式只能用于纯文本。

你的正则表达式有什么问题

在匹配结束部分之前,您的正则表达式包含<div((.+?(?=<\/div>)|.+?(?=<div>))|(?R))*部分(与 相同<div((.+?(?=<\/?div>))|(?R))*<\/div>。当您有一些分隔文本时,不要依赖简单的懒惰/贪婪点匹配(除非用于展开循环结构 - 当您知道自己在做什么时)。它的作用是这样的:

  • <div-<div字面匹配(同样,<diverse由于缺少单词边界或\s在它之后)
  • ( - 匹配的第 1 组:
    • (.+?(?=<\/div>)|.+?(?=<div>))- 匹配任何 1+ 个字符(尽可能少)直到第一个</div>或第一个<div>
    • |
    • (?R) - 递归(即插入和使用)
  • )* - 重复第 1 组零次或多次。

问题很明显:该(.+?(?=<\/?div>))部分不排除匹配<div>or </div>此分支必须仅将文本 NOT EQUAL 匹配到前导和尾随定界符

解决方案

要匹配某些特定文本以外的文本,请使用调和的贪婪标记

<div\b[^<]*>((?:(?!<\/?div\b).)+|(?R))*<\/div>\s*
             ^^^^^^^^^^^^^^^^^^^ 
Run Code Online (Sandbox Code Playgroud)

请参阅正则表达式演示。请注意,您必须使用 DOTALL 修饰符才能跨换行符匹配文本。捕获组是多余的,您可以将其删除。

这里重要的是(?:(?!<\/?div\b).)+只匹配 1 个或多个不是 a<div....></div序列的起始字符的字符。请参阅我上面链接的线程,了解它是如何工作的。

至于性能,缓和的贪婪令牌是消耗资源的。展开循环技术来拯救:

<div\b[^<]*>(?:[^<]+(?:<(?!\/?div\b)[^<]*)*|(?R))*<\/div>\s*
Run Code Online (Sandbox Code Playgroud)

看到这个正则表达式演示

现在,该标记看起来像[^<]+(?:<(?!\/?div\b)[^<]*)*: 1+ 个字符而不是<后跟 0+ 个序列,<后面没有/divdiv(作为一个完整的单词),然后是 0+ non- <s。

<div\b可能仍然匹配 in <div-tmp,因此也许<div(?:\s|>)是通过正则表达式处理此问题的更好方法。尽管如此,使用DOM解析 HTML还是容易得多