使用正则表达式解析HTML:为什么不推荐?

236

在stackoverflow上,似乎每一个使用正则表达式从HTML中获取信息的提问者都会不可避免地得到一个“答案”,该答案说不要使用正则表达式解析HTML。

为什么不呢?我知道有所谓的“真正”的HTML解析器,比如Beautiful Soup,我相信它们很强大和有用,但是如果你只是做一些简单、快速或粗糙的事情,那么为什么要使用这么复杂的东西,当几个正则表达式语句也可以工作得很好呢?

此外,是否有一些基本的东西我没有理解,使得正则表达式在解析中总是不好的选择?


3
我认为这是一个重复的问题,与https://dev59.com/B3VC5IYBdhLWcg3w9GPM相同。 - jcrossley3
27
因为只有查克·诺里斯才能使用正则表达式解析HTML(正如这个著名的Zalgo事情所解释的那样:https://dev59.com/X3I-5IYBdhLWcg3wq6do)。 - takeshin
1
这个问题促使我提出了另一个相关的问题。如果您感兴趣:为什么不能使用正则表达式解析HTML/XML:通俗易懂的正式解释 - mac
小心Zalgo。 - Kelly S. French
此问题已添加到Stack Overflow正则表达式FAQ,位于“常见验证任务”下。 - aliteralmind
显示剩余2条评论
18个回答

3

这个表达式用于提取HTML元素的属性,支持以下内容:

  • 未引用/引用的属性
  • 单引号/双引号
  • 在属性中转义引号
  • 等号周围的空格
  • 任意数量的属性
  • 仅检查标签内的属性
  • 转义注释
  • 处理属性值中的不同引号

(?:\<\!\-\-(?:(?!\-\-\>)\r\n?|\n|.)*?-\-\>)|(?:<(\S+)\s+(?=.*>)|(?<=[=\s])\G)(?:((?:(?!\s|=).)*)\s*?=\s*?[\"']?((?:(?<=\")(?:(?<=\\)\"|[^\"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!\"|')(?:(?!\/>|>|\s).)+))[\"']?\s*)

点击查看。使用“gisx”标志可以更好地运行,如演示所示。


1
非常有趣。虽然不易读懂,可能很难调试,但仍然是令人印象深刻的工作! - Eric Duminil
这仍然模糊地假定HTML格式良好。如果没有上下文匹配,这将匹配在通常不希望匹配它们的上下文中出现的明显URL,例如在<script>标记内部的JavaScript代码片段中。 - tripleee

2

有些情况下,使用正则表达式从HTML中提取信息是正确的方法 - 这取决于具体情况。

总的来说,人们普遍认为这是一个不好的主意。然而,如果已知HTML结构(且不太可能更改),那么这仍然是一种有效的方法。


2
请记住,尽管HTML本身不规则,但您正在查看的页面的某些部分可能是规则的。
例如,如果网页正常工作,则使用正则表达式来抓取
标签是完全合理的,因为嵌套标签是错误的。
我最近只使用Selenium和正则表达式进行了一些网络爬虫。 我成功的原因是所需数据放在了一个<form>中,并以简单的表格格式呈现(因此我甚至可以指望<table><tr><td>不是嵌套的 - 这实际上是非常不寻常的)。 在某种程度上,正则表达式甚至几乎是必要的,因为我需要访问的某些结构被注释分隔。(Beautiful Soup可以获取注释,但使用Beautiful Soup抓取<!-- BEGIN --><!-- END -->块会很困难。)
然而,如果我担心嵌套的表格,我的方法就不起作用了! 我将不得不退回到Beautiful Soup。 即使如此,有时您仍然可以使用正则表达式来抓取所需的块,然后从那里深入了解。

2
“这取决于”。确实,正则表达式无法以真正准确的方式解析HTML,原因在此处已经给出。然而,如果出现错误(例如无法处理嵌套标签)的后果较小,并且在您的环境中使用正则表达式非常方便(例如当您正在使用Perl),那么请继续使用。
假设您正在解析链接到您网站的网页,可能是通过Google链接搜索找到的,您想要快速获得与您的链接相关的一般上下文的简单方法。您正在尝试运行一个小报告,可能会向您发出警报,以防止链接垃圾邮件之类的事情。
在这种情况下,误解析某些文档并不是什么大问题。除了您以外,没有人会看到这些错误,如果您非常幸运,错误很少,您可以逐个跟进。
我想我是在说这是一种权衡。有时候,实施或使用正确的解析器 - 尽管这可能很容易 - 如果准确性不是关键,则可能不值得麻烦。
只是小心你的假设。例如,如果您尝试解析将显示在公共场合的内容,那么我可以想到一些正则表达式捷径可能会产生负面影响。

1
我也尝试了使用正则表达式进行匹配。它主要用于查找与下一个HTML标签配对的内容块,但不会寻找匹配结束标签,但它会捕获关闭标签。在您自己的语言中使用堆栈来检查这些。 使用'sx'选项。如果您感到幸运,可以使用'g'选项。
(?P<content>.*?)                # Content up to next tag
(?P<markup>                     # Entire tag
  <!\[CDATA\[(?P<cdata>.+?)]]>| # <![CDATA[ ... ]]>
  <!--(?P<comment>.+?)-->|      # <!-- Comment -->
  </\s*(?P<close_tag>\w+)\s*>|  # </tag>
  <(?P<tag>\w+)                 # <tag ...
    (?P<attributes>
      (?P<attribute>\s+
# <snip>: Use this part to get the attributes out of 'attributes' group.
        (?P<attribute_name>\w+)
        (?:\s*=\s*
          (?P<attribute_value>
            [\w:/.\-]+|         # Unquoted
            (?=(?P<_v>          # Quoted
              (?P<_q>['\"]).*?(?<!\\)(?P=_q)))
            (?P=_v)
          ))?
# </snip>
      )*
    )\s*
  (?P<is_self_closing>/?)   # Self-closing indicator
  >)                        # End of tag

这个正则表达式是为Python设计的(可能适用于其他语言,但我没有尝试过),它使用了正向先行断言、负向后顾断言和命名反向引用。支持以下内容:

  • 开始标签 - <div ...>
  • 结束标签 - </div>
  • 注释 - <!-- ... -->
  • CDATA - <![CDATA[ ... ]]>
  • 自闭合标签 - <div .../>
  • 可选属性值 - <input checked>
  • 未引用/已引用的属性值 - <div style='...'>
  • 单引号/双引号 - <div style="...">
  • 转义引号 - <a title='John\'s Story'>
    (这并不是真正有效的HTML,但我是个好人)
  • 等号周围的空格 - <a href = '...'>
  • 有趣部分的命名捕获

它还可以很好地避免触发格式不正确的标签,例如当您忘记使用<>时。

如果您的正则表达式支持重复命名捕获,则很棒,但Python的re不支持(我知道regex支持,但我需要使用原始Python)。下面是您将获得的内容:

  • content - 所有标签内的内容,不包括下一个标签。可以省略。
  • markup - 包含全部内容的标签。
  • comment - 如果是注释,那么是注释的内容。
  • cdata - 如果是<![CDATA[...]]>,那么是CDATA的内容。
  • close_tag - 如果是闭合标签(</div>),那么是标签名。
  • tag - 如果是开放标签(<div>),那么是标签名。
  • attributes - 标签内的所有属性。如果没有重复组,则使用此属性获取所有属性。
  • attribute - 重复出现的每个属性。
  • attribute_name - 重复出现的每个属性名称。
  • attribute_value - 重复出现的每个属性值。如果有引号,则包括引号。
  • is_self_closing - 如果是自闭合标签,则为/;否则为空。
  • _q_v - 忽略这些;它们用于内部反向引用。

如果你的正则表达式引擎不支持重复命名捕获,则有一个部分可以用来获取每个属性。只需在attributes组上运行该正则表达式即可获取其中的每个attributeattribute_nameattribute_value

演示在此:https://regex101.com/r/mH8jSu/11


0
实际上,在PHP中使用正则表达式解析HTML是完全可行的。你只需要使用 strrpos 反向解析整个字符串,找到 < ,然后每次都使用非贪婪匹配符重复执行正则表达式来解决嵌套标签问题。这种方法不太高端,并且在处理大型内容时速度非常慢,但我在我的个人网站模板编辑器中使用了它。我实际上并没有解析HTML,而是用一些自定义标签来查询数据库条目以显示数据表格(例如我的<#if()>标签可以这样突出显示特殊条目)。我也没有准备在少数几个自己创建的标签(其中包含非XML数据)上使用XML解析器。

所以,即使这个问题已经相当老旧,但仍然会在谷歌搜索中出现。我看到了这个问题,认为“接受挑战”,并完成了代码的修复,而无需替换所有内容。决定为搜索类似原因的任何人提供不同的意见。此外,最后一个答案是4小时前发布的,因此这仍然是一个热门话题。


4
-1 表示这是一个非常糟糕的想法。你有考虑标签和闭合角括号之间的空格吗?(例如,<tag >)你有考虑注释掉的闭合标签吗?(例如,<tag> <!-- </tag> -->)你有考虑 CDATA 吗?你有考虑大小写不一致的标签吗?(例如,<Tag> </tAG>)你也考虑了这个吗? - rmunn
1
在你的几个自定义标签的特定情况下,正则表达式确实很有效。因此,在你的特定情况下使用它们并不是错误。但那不是HTML,并且说“在PHP中使用正则表达式解析HTML是完全可行的”是绝对错误和糟糕的想法。真正的HTML存在不一致性(远不止我列出的几个),这就是为什么你永远不应该使用正则表达式解析真正的HTML。请参见其他回答以及我在上面的另一个评论中链接的回答。 - rmunn
2
PHP是一种图灵完备的语言,因此它绝不是彻头彻尾的错误。所有计算上可能的事情都是可能的,包括解析HTML。标签中的空格从来不是问题,我已经适应了按顺序列出标签元素的方式。我的使用自动纠正了大小写不一致的标签,在最初阶段剥离了注释内容,在稍后添加了各种标签之后,可以轻松地添加各种标签(虽然它是区分大小写的,这是我自己的选择)。而且我很确定CDATA实际上是一个XML元素,而不是HTML元素。 - Deji
2
我的旧方法(我在这里描述过)非常低效,最近我已经开始重新编写很多内容编辑器。当涉及到这些事情时,可能性不是问题;最好的方式始终是主要关注点。真正的答案是“在PHP中没有简单的方法”。没有人说在PHP中没有办法做到或者这是一个可怕的想法,但是用正则表达式是不可能的,这是我回答中唯一的一个重大缺陷,我假设问题是指在PHP上下文中使用正则表达式,这并不一定是正确的。 - Deji

0

正则表达式对于像HTML这样的语言来说并不足够强大。当然,有一些例子可以使用正则表达式。但总体而言,它不适合用于解析。


-1
你知道吗...有很多人认为你不能做到,我觉得双方都对又对又错。你做到,但是需要比仅仅运行一个正则表达式更多的处理。以this(我在一个小时内写的)为例。它假设HTML完全有效,但根据你用来应用上述正则表达式的语言,你可以修复一些HTML以确保成功。例如,删除不应该存在的闭合标签:</img> 例如。然后,为缺少单个HTML斜杠的元素添加闭合标签等。
我会将其用于编写一个库的上下文中,该库允许我执行类似JavaScript的[x].getElementsByTagName()的HTML元素检索。我只需将我在正则表达式的DEFINE部分编写的功能切割出来,并用它逐个步进到元素树中。

那么,这将是验证HTML的最终100%答案吗?不是。但这是一个开始,再多做一些工作就可以完成。然而,在一个正则表达式执行中尝试完成它并不实际,也不高效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接