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


207

似乎在stackoverflow上,每个问询者都在使用正则表达式从HTML中获取某些信息,每个问题不可避免地会有一个“答案”,说不使用正则表达式来解析HTML。

为什么不?我知道那里有没有引号的“真实” HTML解析器,例如Beautiful Soup,而且我敢肯定它们功能强大且有用,但是如果您只是在做简单,快速或肮脏的事情,那为什么呢?当使用一些正则表达式语句就可以了吗?

此外,对于正则表达式,我是否不了解某些基本知识,因而使它们成为一般解析的错误选择?



23
因为只有Chuck Norris 才能使用正则表达式解析HTML(正如Zalgo着名的东西所解释的那样:stackoverflow.com/questions/1732348/…)。
Takehin 2010年

1
这个问题促使我问另一个与某种意义相关的问题。如果您有兴趣:为什么无法使用正则表达式来解析HTML / XML:用外行的术语进行的正式解释
mac


该问题已添加到“通用验证任务”下的“ 堆栈溢出正则表达式常见问题解答 ”中。
aliteralmind 2014年

Answers:


212

正则表达式无法进行整个HTML解析,因为它取决于匹配开始和结束标记,而正则表达式则无法实现。

正则表达式只能匹配正则语言,但是HTML是无上下文语言,不是正则语言(正如@StefanPochmann所指出的,正则语言也是无上下文的,因此无上下文不一定意味着不正则)。您可以对HTML的正则表达式执行的唯一操作是启发式,但这并不适用于所有条件。应该有可能呈现一个HTML文件,该文件将被任何正则表达式错误地匹配。


26
到目前为止最好的答案。如果它只能匹配常规语法,那么我们将需要一个无限大的正则表达式来解析上下文无关的语法,例如HTML。我喜欢这些东西有明确的理论答案。
ntownsend

2
我以为我们在讨论Perl类型的正则表达式,实际上它们不是正则表达式。
Hank Gay

5
实际上,.Net正则表达式可以使用平衡组和精心设计的表达式来在某种程度上将开幕式与结束标记进行匹配。当然,将所有这些内容包含在一个正则表达式中还是很疯狂的,它看起来像很棒的代码Chtulhu,也可能会召唤出真正的代码。最后,它仍然不适用于所有情况。他们说,如果编写一个可以正确解析任何HTML的正则表达式,则Universe将折叠到其自身上。
Alex Paven

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

43
-1这个答案从错误的参数(“因为HTML不是常规语言”)中得出正确的结论(“用Regex解析HTML是一个坏主意”)。如今,大多数人在说“ regex”(PCRE)时不仅具有解析上下文无关文法(实际上是微不足道的)的能力,而且还具有上下文敏感文法的能力(请参阅stackoverflow.com/questions/7434272/ ……)。
NikiC 2011年


23

(来自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而不仅仅是一个具体的页面,您必须在regexp中重新实现HTML解析器和正则表达式肯定会变得不可读1000倍。
史密斯·约翰斯

1
嗨,安迪,我花时间想出一个支持您提到的案例的表达方式。stackoverflow.com/a/40095824/1204332让我知道您的想法!:)
伊万·查尔

2
这个答案的推理方式已经过时了,并且在今天的应用甚至比最初的应用要少(我认为并非如此)。(引用OP:“如果您只是在做简单,快速或肮脏的事情……”。)
Sz。

16

有两个快速原因:

  • 编写可经受恶意输入的正则表达式很难;比使用预建工具更困难
  • 编写一个可与​​您不可避免地会被使用的可笑标记一起使用的正则表达式很困难;比使用预建工具更困难

关于正则表达式是否适合一般解析:它们不适合。您是否见过解析大多数语言所需的各种正则表达式?


2
哇?2年后投票否决?万一有人想知道,我没有说“因为这在理论上是不可能的”,因为这个问题清楚地问到了“快速而肮脏”而不是“正确”的问题。OP显然已经阅读了涵盖理论上不可能的领域的答案,但仍然不满意。
汉克·盖伊

1
超过5年后再投票。:)至于为什么您可能会收到反对意见,我没有资格说,但就个人而言,我希望看到一些例子或说明,而不是最后的口头问题。
亚当·詹森

3
基本上,在运送产品或内部工具时进行的所有快速而肮脏的html解析最终都是一个巨大的安全漏洞,或者是一个等待发生的错误。一定不要灰心。如果可以使用正则表达式,则可以使用适当的html解析器。
恢复莫妮卡

16

就解析而言,正则表达式在“词法分析”(词法分析器)阶段很有用,该阶段将输入分解为标记。在实际的“构建分析树”阶段中,它的用处较小。

对于HTML解析器,我希望它仅接受格式正确的HTML,并且需要使用正则表达式无法执行的功能(它们不能“计数”,并确保给定数量的打开元素由相同数量平衡)结束元素)。


8

因为有很多“起草” HTML的方法,浏览器将以相当自由的方式处理HTML,但是要花费很多精力来重现浏览器的自由行为以覆盖所有带有正则表达式的情况,因此您的正则表达式不可避免地会因某些特殊原因而失败情况,这可能会在您的系统中引入严重的安全漏洞。


1
没错,大多数HTML似乎太可怕了。我不明白失败的正则表达式会如何导致严重的安全漏洞。能给我举个例子吗?
ntownsend

4
ntownsend:例如,您认为您已经从HTML中剥离了所有脚本标签,但是您的正则表达式无法解决特殊情况(也就是说,仅适用于IE6):繁荣,您具有XSS漏洞!
2009年

1
这是一个严格的假设示例,因为大多数现实世界中的示例太复杂而无法放入这些注释中,但是您可以通过快速浏览主题来找到一些例子。
2009年

3
+1表示安全角度。当您与整个Internet交互时,您将无法编写骇人的“大多数时间都能工作”的代码。
j_random_hacker 2009年

7

问题在于,大多数提出与HTML和正则表达式有关的问题的用户都会这样做,因为他们找不到自己的有效正则表达式。然后,人们必须考虑使用DOM或SAX解析器或类似工具是否会使一切都变得容易。为了处理类似XML的文档结构,对它们进行了优化和构造。

当然,有些问题可以使用正则表达式轻松解决。但是重点在于轻松

如果您只想查找所有看起来像http://.../正则表达式都很好的URL 。但是,如果要查找a类元素中具有“ mylink”类的所有URL,则最好使用适当的解析器。


6

正则表达式不是设计用来处理嵌套标记结构的,而最好的处理复杂的(在最坏的情况下是不可能的)处理由真实HTML获得的所有可能的边缘情况。


6

我相信答案就在于计算理论。对于要使用正则表达式解析的语言,其定义必须为“常规”(链接)。HTML不是常规语言,因为它不满足常规语言的许多标准(与html代码固有的许多嵌套级别有关)。如果您对计算理论感兴趣,我会推荐本书。


1
我实际上已经读过那本书。我只是没有想到HTML是上下文无关的语言。
ntownsend

4

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

  • 未引用/引用的属性,
  • 单引号/双引号,
  • 属性中的转义引号,
  • 等号周围的空格,
  • 任何数量的属性,
  • 仅检查标记内的属性,
  • 转义评论,以及
  • 在属性值中管理不同的引号。

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

检查一下。如演示中所示,它与“ gisx”标志一起使用效果更好。


1
那很有趣。不可读,可能很难调试,但仍然:令人印象深刻的工作!
埃里克·杜米尼尔

这仍然模糊地假设HTML的格式正确。如果没有上下文匹配,则会在您通常不希望与它们匹配的上下文中(例如,<script>标记中的一段JavaScript代码)匹配明显的URL 。
Tripleee'4

4

HTML / XML分为标记和内容。正则表达式仅在进行词法标签解析时有用。我想您可以推断出内容。对于SAX解析器来说,这将是一个不错的选择。标签和内容可以传递给用户定义的功能,在该功能中可以跟踪元素的嵌套/关闭。

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

经过多年的测试,我发现浏览器解析格式正确和格式错误的标签的秘密。

普通元素使用以下形式解析:

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

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

您会注意到这[^>]?是其中的一种交替。这将匹配格式错误的标签中不平衡的报价。

它也是所有正则表达式最邪恶的根源。它的使用方式将触发颠簸,以满足其贪婪的,必须匹配的量化容器。

如果以被动方式使用,则永远不会有问题。但是,如果您通过将某些内容与所需的属性/值对相互穿插来强迫某些内容匹配,并且没有提供足够的防止回溯的保护,则这将是一场失控的噩梦。

这是普通旧标签的一般形式。注意[\w:]标记名称的 代表吗?实际上,代表标签名称的合法字符是令人难以置信的Unicode字符列表。

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

继续,我们还看到您只是在不解析所有标签的情况下无法搜索特定标签。我的意思是可以,但是必须使用动词的组合,例如(* SKIP)(* FAIL),但仍然必须解析所有标签。

原因是标签语法可能隐藏在其他标签等内部。

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

随着新的HTML或xml或任何其他新的结构的开发,只需将其添加为替代之一即可。


网页说明-我从未见过
遇到此问题的网页(或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]*? )
           )
      )
 )
 >

3

不过,“取决于”。确实,由于此处给出的所有原因,正则表达式不能且不能以正确的精度解析HTML。但是,如果出错的后果很小(例如不处理嵌套标签),并且正则表达式在您的环境中非常方便(例如,当您入侵Perl时),请继续。

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

在这种情况下,对某些文档进行错误的解析不会有什么大不了的。除了错误以外,没有人会看到错误,而且如果您很幸运,将很少有可以单独跟进的。

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

请小心您的假设。我可以想到一些正则表达式快捷方式会适得其反的方法,例如,当您尝试解析将公开显示的内容时。


3

肯定在某些情况下,使用正则表达式解析HTML中的某些信息是正确的方法-它在很大程度上取决于特定情况。

上面的共识是,总的来说这是一个坏主意。但是,如果HTML结构已知(并且不太可能更改),那么它仍然是有效的方法。


3

请记住,尽管HTML本身不是常规的,但是您正在查看的页面部分可能是常规的。

例如,<form>嵌套标签是错误的。如果网页正常运行,则使用正则表达式来抓取a <form>是完全合理的。

我最近仅使用Selenium和正则表达式进行了一些Web抓取。我之所以无法使用它,是因为我想要的数据以形式放置<form>,并以简单的表格格式放置(因此我什至可以依靠<table><tr>并且<td>无需嵌套,这实际上是非常不寻常的)。在某种程度上,正则表达式甚至几乎是必需的,因为我需要访问的某些结构由注释分隔。(“美丽的汤”可以给您评论,但是使用“美丽的汤” 将很难抓住<!-- BEGIN --><!-- END -->阻止。)

但是,如果我不得不担心嵌套表,那么我的方法根本行不通!我本来只能依靠美丽汤。但是,即使那样,有时您仍可以使用正则表达式来获取所需的块,然后从那里向下钻取。


2

实际上,用regex进行HTML解析在PHP中是完全可行的。您只需要向后解析整个字符串,即可使用每次使用不愉快的说明符从那里strrpos查找<并重复该正则表达式来克服嵌套标签。在大型事物上并不花哨并且非常慢,但是我将其用于我自己的网站个人模板编辑器。我实际上并不是解析HTML,而是为查询数据库条目以显示数据表而制作了一些自定义标签(我的<#if()>标签可以以此方式突出显示特殊条目)。我不准备只在几个自创建的标签(其中包含非常非XML数据)上使用XML解析器。

因此,即使这个问题已经相当严重,它仍然会显示在Google搜索中。我读了一下,以为“接受了挑战”,完成了我的简单代码的修复,而不必替换所有内容。决定为任何出于类似原因的人提供不同的意见。另外,最后一个答案是在4个小时前发布的,因此这仍然是一个热门话题。


2
-1表示一个可怕的想法。您是否考虑过标签和右尖括号之间的空白?(例如,<tag >)您是否考虑了注释掉的结束标签?(例如,<tag> <!-- </tag> -->)您是否考虑过CDATA?您是否考虑了大小写不一致的标记?(例如,<Tag> </tAG>)你有没有考虑这个呢?
rmunn 2014年

1
是的,在少数几个自定义标签的特殊情况下,正则表达式效果很好。因此,并不是说您在特定情况下使用它们是错误的。不过,那不是HTML,并且说“用regex进行HTML解析在PHP中是完全可能的”完全是假的,这是一个可怕的想法。真正的HTML的不一致性(除了我列出的之外,还有很多其他原因),这就是为什么您永远不要使用正则表达式解析真正的HTML。很好,请参阅此问题的所有其他答案,以及我在上面的其他评论中链接的答案。
rmunn 2014年

2
PHP是一种图灵完备的语言,因此它根本不是假的。计算上的一切可能都是可能的,包括解析HTML。标签中的空格从来都不是问题,我已经对其进行了调整,以按顺序列出标签元素。我使用自动校正的带有不一致的大小写的标签,在开始的第一阶段就删除了注释的内容,在以后添加一些内容之后,可以轻松添加各种标签(尽管我区分大小写)。而且我很确定CDATA实际上是XML元素,而不是HTML元素。
Deji 2014年

2
我以前的方法(我在这里描述)效率很低,最近我开始重新编写许多内容编辑器。在做这些事情时,可能性不成问题。最佳方法始终是主要关注点。真正的答案是“在PHP中没有简便的方法”。没有人说无法用PHP做到这一点,或者这是一个糟糕的主意,但是对于regex来说是不可能的,老实说,我从未尝试过,但是我回答中的一个主要缺陷是我认为问题在于正则表达式在PHP的上下文中,不一定是这种情况。
Deji 2014年

2

我也为此尝试了一个正则表达式。这对于查找与下一个HTML标签配对的内容块最有用,并且它不查找匹配的 close标签,但是它将拾取close标签。用您自己的语言滚动堆栈以检查这些内容。

与“ 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-所有内容,直到下一个标签。您可以忽略这个。
  • 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


1

对于像HTML这样的语言,正则表达式还不够强大。当然,有一些示例可以使用正则表达式。但是一般来说,它不适合解析。


0

你知道...还有很多你的心态CAN NOT做到这一点,我认为在两侧护栏,每个人都对与错。你CAN做到这一点,但它需要一点点的不只是运行反对一个正则表达式更多的处理。以这个(我在一个小时内写完)为例。它假定HTML完全有效,但是根据您使用哪种语言来应用上述正则表达式,您可以对HTML进行一些修复以确保其成功。例如,删除不应存在的结束标记:</img>例如。然后,将结尾的单个HTML正斜杠添加到缺少它们的元素中,依此类推。

我将在编写库的上下文中使用此库,例如,该库将允许我执行类似于JavaScript的HTML元素检索[x].getElementsByTagName()。我只是拼接了我在正则表达式的DEFINE部分中编写的功能,并将其用于单步进入元素树的内部。

那么,这将是验证HTML的最终100%答案吗?否。但这是一个开始,只需做更多的工作就可以完成。但是,尝试在一个正则表达式执行中执行此操作既不切实际,也不有效。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.