似乎在stackoverflow上的每个问题中,提问者使用regex从HTML中获取一些信息,将不可避免地有一个“答案”,说不要使用regex解析HTML。

为什么不呢?我知道有一些所谓的“真正的”HTML解析器,比如Beautiful Soup,我相信它们是强大而有用的,但如果您只是在做一些简单、快速或简单的事情,那么当一些正则表达式语句就可以很好地工作时,为什么要麻烦使用如此复杂的东西呢?

此外,是否只是因为我不理解正则表达式的某些基本原理,才使得它们在解析中成为一个糟糕的选择?


当前回答

“这要看情况”。由于这里给出的所有原因,正则表达式不能也不能准确地解析HTML,这是事实。但是,如果错误的后果(比如不处理嵌套标记)很小,如果正则表达式在您的环境中非常方便(比如在入侵Perl时),那么就继续。

假设您正在解析链接到您站点的网页——也许您是通过谷歌链接搜索找到它们的——并且您想要一种快速方法来大致了解链接周围的上下文。您试图运行一个小报告,可能会提醒您链接垃圾邮件,诸如此类。

在这种情况下,对某些文档进行错误解析并不是什么大问题。除了你自己,没有人会发现错误,如果你足够幸运,你可以单独跟进。

我想我是说这是一种权衡。有时,实现或使用正确的解析器(尽管很简单)可能不值得麻烦,如果准确性不是至关重要的话。

小心你的假设。例如,如果您试图解析将公开显示的内容,我可以想到regexp快捷方式可能适得其反的几种情况。

其他回答

正则表达式无法解析整个HTML,因为它依赖于匹配开始标记和结束标记,而正则表达式则无法匹配。

正则表达式只能匹配常规语言,但HTML是一种与上下文无关的语言,而不是常规语言(正如@StefanPochmann所指出的,常规语言也是与上下文无关的,因此与上下文无关并不一定意味着不常规)。在HTML上使用regexp唯一能做的事情是启发式,但这并不适用于所有条件。任何正则表达式都可以错误地匹配HTML文件。

就解析而言,正则表达式在“词法分析”(lexer)阶段很有用,在这个阶段,输入被分解成标记。它在实际的“构建解析树”阶段用处不大。

对于HTML解析器,我希望它只接受格式良好的HTML,而这需要正则表达式所不能做到的功能(它们不能“计数”并确保给定数量的开始元素与相同数量的结束元素相平衡)。

这个表达式从HTML元素中检索属性。它支持:

未加引号/加引号的属性, 单引号/双引号, 属性中的转义引号, 等号周围的空格, 任意数量的属性, 只检查标签内的属性, 转义注释,以及 在一个属性值中管理不同的引号。

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

来看看。在演示中,使用“gisx”标志效果更好。

(来自http://htmlparsing.com/regexes)

假设您有一个HTML文件,您试图从中提取url < img >标签。

<img src="http://example.com/whatever.jpg">

所以你可以用Perl写一个这样的正则表达式:

if ( $html =~ /<img src="(.+)"/ ) {
    $url = $1;
}

在本例中,$url确实包含 http://example.com/whatever.jpg。但是当 你会得到这样的HTML:

<img src='http://example.com/whatever.jpg'>

or

<img src=http://example.com/whatever.jpg>

or

<img border=0 src="http://example.com/whatever.jpg">

or

<img
    src="http://example.com/whatever.jpg">

否则你就会得到假阳性

<!-- // commented out
<img src="http://example.com/outdated.png">
-->

它看起来很简单,对于一个单一的、不变的文件来说可能很简单,但是对于任意HTML数据,正则表达式只会让你将来头疼。

我也试着用正则表达式来做这个。它主要用于查找与下一个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 = '…'> 命名捕获感兴趣的位

它还可以很好地避免在格式错误的标记上触发,比如当您忘记了<或>时。

如果你的regex支持重复命名捕获,那么你是黄金,但Python re不支持(我知道regex支持,但我需要使用香草Python)。以下是你得到的结果:

content - All of the content up to the next tag. You could leave this out. markup - The entire tag with everything in it. comment - If it's a comment, the comment contents. cdata - If it's a <![CDATA[...]]>, the CDATA contents. close_tag - If it's a close tag (</div>), the tag name. tag - If it's an open tag (<div>), the tag name. attributes - All attributes inside the tag. Use this to get all attributes if you don't get repeated groups. attribute - Repeated, each attribute. attribute_name - Repeated, each attribute name. attribute_value - Repeated, each attribute value. This includes the quotes if it was quoted. is_self_closing - This is / if it's a self-closing tag, otherwise nothing. _q and _v - Ignore these; they're used internally for backreferences.

如果您的正则表达式引擎不支持重复的命名捕获,则可以使用一个被调用的部分来获取每个属性。只需在属性组上运行该正则表达式,从中获得每个属性、attribute_name和attribute_value。

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