使用正则表达式解析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个回答

236

使用正则表达式无法完整地解析HTML,因为它依赖于匹配开放和关闭标记,而这在正则表达式中是不可能的。

正则表达式只能匹配正则语言,但HTML是上下文无关语言,并不是正则语言(正如@StefanPochmann指出的那样,正则语言也是上下文无关的,因此上下文无关并不一定意味着不正则)。你唯一可以使用正则表达式对HTML进行启发式分析,但这在每种情况下都不起作用。应该可以提供一个HTML文件,任何正则表达式都会匹配错误。


27
迄今为止最佳答案。如果它只能匹配常规语法,那么我们需要一个无限大的正则表达式来解析像HTML这样的上下文无关文法。当这些问题有清晰的理论答案时,我很喜欢。 - ntownsend
2
我认为我们正在讨论 Perl 类型的正则表达式,但实际上它们并不是真正的正则表达式。 - Hank Gay
6
实际上,.Net正则表达式可以使用平衡组和精心设计的表达式,在某种程度上匹配具有开放和关闭标签。当然,在正则表达式中包含全部内容仍然是疯狂的,它看起来像伟大的代码Chtulhu,并可能召唤真正的Cthulhu。最终,它仍然无法适用于所有情况。据说,如果你编写一个可以正确解析任何HTML的正则表达式,宇宙将会崩溃。 - Alex Paven
5
一些正则表达式库可以执行递归正则表达式(有效地使它们成为非正则表达式 :) - Ondra Žižka
48
这篇答案从错误的论点中得出了正确的结论(“使用正则表达式解析HTML是个坏主意”)。当今大多数人谈到“regex”(即PCRE)时,它不仅能够解析上下文无关文法(实际上这很简单),而且还能解析上下文有关文法(请参见https://dev59.com/DWs05IYBdhLWcg3wIObS#7434814)。 - NikiC
显示剩余7条评论

37

对于快速而简单的正则表达式来说,这样做已经足够了。但是需要知道的根本事实是,构建一个可以正确解析HTML的正则表达式是不可能的。

原因在于正则表达式无法处理任意嵌套的表达式。请参见“ 正则表达式是否能用于匹配嵌套的模式?”


2
一些正则表达式库可以执行递归正则表达式(有效地使它们成为非正则表达式 :))。 - Ondra Žižka

32

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

假设您有一个HTML文件,您想从其中提取<img>标签中的URL。

<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'>
或者
<img src=http://example.com/whatever.jpg>
或者
<img border=0 src="http://example.com/whatever.jpg">
或者
<img
    src="http://example.com/whatever.jpg">

否则您将开始收到来自假阳性的反馈

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

看起来很简单,如果只是针对一个不会改变的文件可能确实很简单,但如果是处理任意HTML数据,使用正则表达式只会在未来带来烦恼。


4
这似乎是正确答案——尽管今天的正则表达式不仅仅是有限自动机,因此可能可以使用正则表达式解析任意HTML,但要解析任意HTML而不仅仅是一个具体页面,你必须重新实现一个HTML解析器。在正则表达式中处理这个过程将会使其变得难以理解,甚至可能需要1000倍的复杂度。 - Smit Johnth
1
嘿,安迪,我花了些时间想出了一个支持你提到的情况的表达式。https://dev59.com/unRB5IYBdhLWcg3wiHz7#40095824 让我知道你的想法! :) - Ivan Chaer
2
这个答案的论据比较过时,甚至适用性比原先提出时还要差(我认为原来就不太适用了)。 (引用OP:“如果你只是做一些简单、快速或肮脏的事情……”。) - Sz.

17

两个快速的原因:

  • 编写一个能抵御恶意输入的正则表达式很难,比使用预构建工具要难得多
  • 编写一个能与你不可避免地遇到的荒谬标记配合工作的正则表达式也很难,比使用预构建工具要难得多

关于正则表达式在解析上的适用性: 它们并不适用。您是否曾见过解析大多数语言所需的正则表达式?


2
哇?两年后还有人踩我?如果有人想知道,我没有说“因为在理论上是不可能的”,因为问题明确要求“快速而肮脏”,而不是“正确”。提问者显然已经阅读了覆盖理论上不可能的领域的答案,但仍然不满意。 - Hank Gay
1
5年后,您终于获得了一次点赞。 :) 至于为什么您可能会收到负评,我不太合适发表评论,但个人而言,我更希望看到一些示例或解释,而不是结束时的修辞性问题。 - Adam Jensen
3
在已发货产品或内部工具中进行的所有快速而草率的HTML解析最终都会成为一个巨大的安全漏洞或者等待发生的错误。必须要极力反对这种做法。如果能够使用正则表达式,那么就可以使用适当的HTML解析器。 - Kuba hasn't forgotten Monica

17

就解析而言,正则表达式可以在“词法分析”(词法分析器)阶段中很有用,这里输入被分成标记。但它在实际的“构建解析树”阶段中不太有用。

对于HTML解析器,我期望它只接受格式良好的HTML,而这需要超出正则表达式所能做的能力(它们无法“计数”,并确保一定数量的开放元素与相同数量的关闭元素平衡)。


8

由于有许多方法可以“搞砸”HTML,而浏览器会以相当宽松的方式处理它,但要复制浏览器的宽松行为以覆盖所有情况需要付出相当大的努力,因此您的正则表达式不可避免地会在某些特殊情况下失败,并可能在系统中引入严重的安全漏洞。


1
非常正确,大多数HTML看起来都很糟糕。我不明白一个失败的正则表达式如何会引入严重的安全漏洞。你能举个例子吗? - ntownsend
4
例如,你认为已从HTML中删除了所有脚本标记,但你的正则表达式无法覆盖一个特殊情况(比如只在IE6上有效):砰,你就有了一个XSS漏洞! - Tamas Czinege
1
这只是一个严格的假设例子,因为大多数真实世界的例子都太复杂了,无法适应这些注释,但你可以通过快速搜索该主题来找到一些例子。 - Tamas Czinege
3
提及安全角度点赞。当你与整个互联网接口时,你不能写粗制滥造的“大部分时间可用”的代码。 - j_random_hacker

7
问题在于,大多数询问HTML和正则表达式相关问题的用户之所以这样做,是因为他们找不到适用的正则表达式。这时候我们需要考虑一下,在使用DOM或SAX解析器等类似工具时,是否会更加容易。这些工具是针对XML文档结构进行优化和构建的。
当然,有些问题可以很容易地通过正则表达式来解决。但重点在于“容易”。
如果您只想查找所有看起来像http://.../的URL地址,那么使用正则表达式即可。但如果您想查找所有位于class为“mylink”的a元素中的URL,则最好使用适当的解析器。

5

正则表达式并不适用于处理嵌套标签结构,而且在处理真实HTML时,要处理所有可能的边缘情况最好也是很复杂的(最坏的情况下是不可能的)。


5

我认为答案在计算理论中。要使用正则表达式解析语言,它必须根据定义是“正则”的(链接)。HTML不是一种正则语言,因为它没有满足正则语言的许多标准(这与html代码中固有的许多嵌套级别有很大关系)。如果你对计算理论感兴趣,我建议这本书


2
我确实读过那本书。只是没想到 HTML 是一种无上下文语言。 - ntownsend

4

HTML/XML被分为标记和内容。使用正则表达式只能进行词法标记解析。我猜您可以推断出内容。这对于SAX分析器来说是一个不错的选择。标记和内容可以传递给用户定义的函数,从而可以跟踪元素的嵌套/闭合。

就解析标签本身而言,可以使用正则表达式,并用于从文档中剥离标签。

经过多年的测试,我已经找到了浏览器解析标签的秘密,包括良好的和错误的标记。

这些常规元素的解析形式如下:

这些标记的核心使用此正则表达式

 (?:
      " [\S\s]*? " 
   |  ' [\S\s]*? ' 
   |  [^>]? 
 )+

你会注意到[^>]?是其中一个选择项。这将匹配来自不正确格式的标记中不平衡引号。
它还是正则表达式中最根本的问题。使用它时,会触发贪婪匹配。如果被动使用,则没有问题。但是,如果你强制匹配,并将其与所需的属性/值对交织在一起,而没有提供足够的防护措施以防止回溯,那么你就会遇到失控的噩梦。
这是纯粹的普通标记的一般形式。请注意 [\w:] 代表标记名称?实际上,代表标记名称的合法字符是一个包含大量Unicode字符的列表。
 <     
 (?:
      [\w:]+ 
      \s+ 
      (?:
           " [\S\s]*? " 
        |  ' [\S\s]*? ' 
        |  [^>]? 
      )+
      \s* /?
 )
 >

另外,我们还看到您无法仅搜索特定的标签而不解析所有标签。 我是说你可以,但它必须使用像(*SKIP)(*FAIL)之类的动词组合,但仍然必须解析所有标签。

原因是标记语法可能隐藏在其他标记中等等。

因此,要被动解析所有标签,需要像下面这样的正则表达式。 这个特定的正则表达式也匹配不可见内容

随着新的HTML或xml或任何其他开发新构造的出现,只需将其添加为其中一种选择即可。


网页注释-我从未见过一个web页面(或xhtml / xml)会有问题。如果你发现一个,请让我知道。

性能注释-它很快。这是我看过的最快的标记解析器(可能有更快的,谁知道)。
我有几个具体版本。 它也非常适合作为刮板(如果你是亲身体验者)。


完整的原始正则表达式

<(?:(?:(?:(script|style|object|embed|applet|noframes|noscript|noembed)(?:\s+(?>"[\S\s]*?"|'[\S\s]*?'|(?:(?!/>)[^>])?)+)?\s*>)[\S\s]*?</\1\s*(?=>))|(?:/?[\w:]+\s*/?)|(?:[\w:]+\s+(?:"[\S\s]*?"|'[\S\s]*?'|[^>]?)+\s*/?)|\?[\S\s]*?\?|(?:!(?:(?:DOCTYPE[\S\s]*?)|(?:\[CDATA\[[\S\s]*?\]\])|(?:--[\S\s]*?--)|(?:ATTLIST[\S\s]*?)|(?:ENTITY[\S\s]*?)|(?:ELEMENT[\S\s]*?))))>

格式化的外观

 <
 (?:
      (?:
           (?:
                # Invisible content; end tag req'd
                (                             # (1 start)
                     script
                  |  style
                  |  object
                  |  embed
                  |  applet
                  |  noframes
                  |  noscript
                  |  noembed 
                )                             # (1 end)
                (?:
                     \s+ 
                     (?>
                          " [\S\s]*? "
                       |  ' [\S\s]*? '
                       |  (?:
                               (?! /> )
                               [^>] 
                          )?
                     )+
                )?
                \s* >
           )

           [\S\s]*? </ \1 \s* 
           (?= > )
      )

   |  (?: /? [\w:]+ \s* /? )
   |  (?:
           [\w:]+ 
           \s+ 
           (?:
                " [\S\s]*? " 
             |  ' [\S\s]*? ' 
             |  [^>]? 
           )+
           \s* /?
      )
   |  \? [\S\s]*? \?
   |  (?:
           !
           (?:
                (?: DOCTYPE [\S\s]*? )
             |  (?: \[CDATA\[ [\S\s]*? \]\] )
             |  (?: -- [\S\s]*? -- )
             |  (?: ATTLIST [\S\s]*? )
             |  (?: ENTITY [\S\s]*? )
             |  (?: ELEMENT [\S\s]*? )
           )
      )
 )
 >

您的正则表达式格式错误 - Cemstrian

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