为什么无法使用正则表达式解析HTML / XML:用外行的术语进行的正式解释


117

SO的日子一天天过去,毫无疑问地需要使用正则表达式来解析(X)HTML或XML。

虽然相对容易找到一些例子来说明正则表达式在该任务中不可行,或者用一些表达该概念的表达式,但我仍然无法在正式的解释中找到为什么不能在外行人家中做这件事的正式解释。条款。

到目前为止,我在该站点上唯一能找到的正式解释可能是非常准确的,但对于自学成才的程序员也很神秘:

这里的缺点是HTML是Chomsky Type 2语法(无上下文语法),RegEx是Chomsky Type 3语法(正则表达式)

要么:

正则表达式只能匹配正则语言,而HTML是无上下文的语言。

要么:

有限自动机(它是正则表达式基础的数据结构)除了处于其所在的状态外没有其他内存,如果您具有任意深度的嵌套,则需要一个任意大的自动机,它会与有限自动机的概念相冲突。

要么:

常规语言的Pumping引理是您不能这样做的原因。

[公平地说:以上解释的大部分都链接到Wikipedia页面,但是这些答案比答案本身更容易理解]。

因此,我的问题是:有人可以用外行的形式对上述为什么不能使用正则表达式解析(X)HTML / XML的正式解释进行翻译吗?

编辑:阅读完第一个答案后,我认为我应该澄清一下:我正在寻找一种“翻译”,它也简要地解释了它试图翻译的概念:在回答的最后,读者应该有一个大概的想法-例如-“常规语言”和“无上下文语法”的含义是什么...


19
请注意,在计算机科学术语中,“正则表达式”与现代的“ regex实现”(您以编程语言使用的工具/ api)有很大不同。后者可以“记住”他们遇到的事物,甚至可以匹配递归定义的(子)模式,从而使其比理论上的“正则表达式”更匹配/解析/识别。
巴特·基尔斯

1
@Bart:这确实只适用于滥用的术语“正则表达式语言POSIX ERE纯粹是常规。
R.,GitHub上停止帮助ICE

2
@R ..,因此,您将POSIX称为“现代实现”:P。严肃地说:是的,你是对的,那些确实正常的。我应该说“ ...许多现代正则表达式实现...”“ ... PCRE正则表达式实现...”
巴特·基尔斯

4
我很难认真对待从根本上滥用严格语言的编程语言,以便将自己推销给无知的程序员...
R .. GitHub停止帮助ICE

3
@R ..,不幸的是PCRE实现被称为“正则表达式”,但是IMO如果不认真使用该语言,则迈出了一大步。我的意思是,您是否因为这个原因不认真考虑Perl,Java,Python,Ruby,JavaScript,.NET等?
巴特·基尔斯

Answers:


117

专注于这一点:

有限自动机(它是正则表达式基础的数据结构)除了处于其所在的状态外没有其他内存,如果您具有任意深度的嵌套,则需要一个任意大的自动机,它会与有限自动机的概念相冲突。

定义正则表达式等效于以下事实:可以通过有限的自动机(每个模式一个不同的自动机)执行字符串是否与模式匹配的测试。有限的自动机没有内存-没有堆栈,没有堆,没有可以涂抹的无限磁带。它所具有的只是有限数量的内部状态,每个内部状态都可以从被测试的字符串中读取一个输入单元,并使用该状态来决定移至下一个状态。作为特殊情况,它具有两个终止状态:“是,匹配”和“否,不匹配”。

另一方面,HTML具有可以任意深度嵌套的结构。要确定文件是否为有效的HTML,您需要检查所有结束标记是否与先前的开始标记匹配。要了解它,您需要知道哪个元素正在关闭。没有任何办法“记住”您看到的开始标签,没有机会。

但是请注意,大多数“ regex”库实际上不仅允许对正则表达式进行严格定义。如果它们可以匹配反向引用,那么它们已经超越了常规语言。因此,您不应该在HTML上使用正则表达式库的原因比HTML不规则的简单事实要复杂一些。


这里对有限状态自动机也有一个很好的解释:youtube.com/watch?
v=vhiiia1_hC4

55

HTML不代表常规语言的事实是一个红色鲱鱼。正则表达式和正则语言听起来有点相似,但是不相同-它们确实具有相同的起源,但是学术上的“正则语言”与当前引擎的匹配能力之间存在明显的距离。实际上,几乎所有现代正则表达式引擎都支持非正则功能-一个简单的例子是(.*)\1。它使用反向引用来匹配重复的字符序列,例如123123bonbon。递归/平衡结构的匹配使这些更加有趣。

维基百科在拉里·沃尔Larry Wall)的引用中很好地表达了这一点:

“正则表达式”仅与真正的正则表达式略有相关。但是,该术语随着我们的模式匹配引擎的功能而增长,因此在这里我不会尝试解决语言上的必要性。但是,我通常将它们称为“ regexes”(或者当我处于盎格鲁-撒克逊人的心情时,称为“ regexen”)。

如您所见,“正则表达式只能与正则语言匹配”,无非是一种常见的谬论。

那么,为什么不呢?

不将HTML与正则表达式匹配的一个很好的理由是“仅仅因为您不能意味着您应该这样做”。虽然可能,但是有更好的工具可以完成这项工作。考虑:

  • 有效的HTML比您想象的要难/复杂。
  • “有效” HTML有很多类型-例如,在HTML中有效的内容在XHTML中无效。
  • 无论如何,在互联网上找到的许多自由格式的HTML都是无效的。HTML库也很好地处理了这些问题,并针对许多常见情况进行了测试。
  • 通常,如果不对数据进行整体分析,则不可能匹配一部分数据。例如,您可能正在寻找所有标题,并最终在注释或字符串文字内进行匹配。<h1>.*?</h1>可能是找到主标题的大胆尝试,但可能会发现:

    <!-- <h1>not the title!</h1> -->

    甚至:

    <script>
    var s = "Certainly <h1>not the title!</h1>";
    </script>

最后一点是最重要的:

  • 使用专用的HTML解析器比您能想到的任何正则表达式都要好。通常,XPath提供了一种更好的表达方式来查找所需的数据,并且使用HTML解析器比大多数人意识到的要容易得多

可以在Jeff Atwood的博客:解析Html的Cthulhu Way中找到关于该主题的完整摘要,以及有关何时适当混合Regex和HTML的重要评论。

什么时候使用正则表达式解析HTML更好?

在大多数情况下,最好在库可以给您的DOM结构上使用XPath。尽管如此,在很多情况下,我还是强烈建议使用正则表达式而不是解析器库:

考虑到以下几种情况:

  • 当您需要一次性更新HTML文件时,您知道结构是一致的。
  • 当您有非常小的HTML代码段时。
  • 当您不处理HTML文件而是使用类似的模板引擎时(在这种情况下很难找到解析器)。
  • 当您要更改HTML的一部分但不是全部时,据我所知,解析器无法回答此请求:它将解析整个文档,并保存整个文档,从而更改了您不想更改的部分。

4
这是关于何时(不使用)正则表达式解析HTML的非常清晰且写得很好的文章,但这几乎不能回答我的问题。我是否可以建议您将其移至这个问题?我认为这将为您带来更多的声誉,但最重要的是,我想这将是一个将来的访客会发现它更相关的地方(@Bart Kiers对我的问题发表了评论,使访客想起“额外的力量”现代正则表达式引擎)。
Mac

1
@mac-非常感谢。实际上,我确实对此有所考虑。我知道我没有回答你的问题,但我不认为这个问题基本上是正确的-你问,解释错误的原因......你有一个好主意,不过,也许是另一个问题是更适合...
Kobi

19

因为HTML可以具有的无限嵌套,<tags><inside><tags and="<things><that><look></like></tags>"></inside></each></other>而regex不能真正解决该问题,因为它无法跟踪其继承和退出的历史。

一个说明困难的简单结构:

<body><div id="foo">Hi there!  <div id="bar">Bye!</div></div></body>

99.9%的基于regex的通用提取例程将无法正确地给我divID的所有内容foo,因为它们无法从div的结束标记中分辨出该div的结束标记bar。那是因为他们没有办法说“好吧,我现在已经进入两个div的第二个,所以我看到的下一个div结束使我退出,而第二个div是第一个的结束标记” 。程序员通常会通过针对特定情况设计特殊情况的正则表达式来进行响应,然后在将更多标签引入内部后立即破坏这些正则表达式,foo并且必须花费大量时间和精力来解决这些问题。这就是为什么人们对整件事感到生气。


1
赞赏的答案,但我的问题不是“为什么我不能使用正则表达式...”。我的问题是关于“翻译”我提供的正式解释!:)
mac

5
从某种意义上讲,这是所有这些语言的翻译,其中最直接的含义是“正则表达式只能匹配正则语言,而HTML是上下文无关的语言”和有关有限自动机的一种。确实是同样的原因。
Ianus Chiaroscuro 2011年

抱歉,可能我的问题还不清楚(欢迎提出改进建议!)。但是我在寻找一个也解释“翻译”的答案。您的答案并未阐明“常规语言”还是“无上下文语言”的概念……
mac

5
解释这些术语将与术语本身一样具有技术性,并且会干扰我所张贴的所有精确语言所具有的实际含义。
Ianus Chiaroscuro 2011年

4
<(\w+)(?:\s+\w+="[^"]*")*>(?R)*</\1>|[\w\s!']+匹配您的代码示例。
Kobi

9

常规语言是可以由有限状态机匹配的语言。

(了解有限状态机,下推机和图灵机基本上是大学四年级CS课程的课程。)

考虑以下机器,该机器可以识别字符串“ hi”。

(Start) --Read h-->(A)--Read i-->(Succeed)
  \                  \
   \                  -- read any other value-->(Fail) 
    -- read any other value-->(Fail)

这是识别常规语言的简单机器。括号中的每个表达式是一个状态,每个箭头是一个过渡。构建这样的机器将使您能够针对常规语言(因此是常规表达式)测试任何输入字符串。

HTML不仅需要了解您所处的状态,还需要了解您以前所见的内容,以匹配标记嵌套。如果将堆栈添加到计​​算机,则可以完成此操作,但是堆栈不再是“常规”的。这称为下推式机器,可以识别语法。


2
“了解有限状态机,下推式机和图灵机基本上是300级CS课程的课程。” 我了解这是为了说明该主题的难易程度/进阶程度,但是我不熟悉您所指的学校系统,请以非特定国家/地区的方式进行说明吗?谢谢!:)
mac

1
我已经更新了。我不知道很难理解,只是在堆栈溢出后进行解释。
肖恩·麦克米伦

6

正则表达式是一台具有有限(通常很小)离散状态的机器。

要使用语言元素的任意嵌套来解析XML,C或任何其他语言,您需要记住您的深度。也就是说,您必须能够计算大括号/括号/标签。

您不能用有限的内存来计数。支撑级别可能比您指定的状态还要多!您可能能够解析语言的一个子集,该子集限制了嵌套级别的数量,但这将非常繁琐。


6

语法是单词可以到达的正式定义。例如,形容词在名词之前in English grammar,但在名词之后en la gramática española。上下文无关意味着语法在所有上下文中都是通用的。上下文相关意味着在某些上下文中还有其他规则。

例如,在C#中,文件顶部的using含义不同于。一个更相关的示例是代码中的以下代码:using System;using (var sw = new StringWriter (...))

void Start ()
{
    string myCode = @"
    void Start()
    {
       Console.WriteLine (""x"");
    }
    ";
}

这是一个可以理解的答案
一个人2014年

但是,上下文无关并不意味着常规。匹配的括号的语言是上下文无关的,但不是常规的。
塔伊米尔2015年

应该添加的是,正则表达式(除非您添加Perl中存在的扩展名)等效于正则语法,这意味着它们不能描述任意深度嵌套的结构,例如任意深度平衡的括号或HTML元素的打开和关闭标签。
reinierpost

4

还有一个实际的原因,就是不使用正则表达式来解析XML和HTML,而这与计算机科学理论完全无关:正则表达式要么非常复杂,要么是错误的。

例如,编写正则表达式以匹配

<price>10.65</price>

但是,如果您的代码正确无误,则:

  • 它必须在开始和结束标记中的元素名称后留空格

  • 如果文档在名称空间中,则它应允许使用任何名称空间前缀

  • 它可能应该允许并忽略出现在开始标记中的任何未知属性(取决于特定词汇表的语义)

  • 它可能需要在十进制值之前和之后都允许有空格(再次取决于特定XML词汇表的详细规则)。

  • 它不应与看起来像元素的东西匹配,但实际上应位于注释或CDATA部分中(如果可能存在恶意数据试图欺骗解析器的情况,这一点尤其重要)。

  • 如果输入无效,则可能需要提供诊断。

当然,其中一些取决于您所应用的质量标准。我们在StackOverflow上看到了很多问题,人们不得不以特定的方式(例如,标签中没有空格)生成XML,因为它正被需要以特定方式编写的应用程序读取。如果您的代码具有长寿,那么很重要的一点是,它应该能够处理XML标准允许的任何方式编写的传入XML,而不仅仅是处理您要测试其代码的一个样本输入文档。


2

从纯粹的理论意义上讲,正则表达式无法解析XML。它们的定义方式不允许它们存储任何先前的状态,从而阻止了任意标记的正确匹配,并且它们无法渗透到嵌套的任意深度,因为嵌套需要内置到正则表达式中。

但是,现代正则表达式解析器是为开发人员提供实用程序的,而不是遵循精确定义的。这样,我们就有诸如反向引用和递归之类的东西,它们利用了先前状态的知识。使用这些,创建可以浏览,验证或解析XML的正则表达式非常简单。

考虑一下,

(?:
    <!\-\-[\S\s]*?\-\->
    |
    <([\w\-\.]+)[^>]*?
    (?:
        \/>
        |
        >
        (?:
            [^<]
            |
            (?R)
        )*
        <\/\1>
    )
)

这将找到下一个格式正确的XML标记或注释,并且只有在其全部内容正确形成后才能找到它。 (此表达式已使用Notepad ++进行了测试,该记事本使用Boost C ++的正则表达式库,该库非常接近PCRE。)

运作方式如下:

  1. 第一块与注释匹配。首先必须这样做,以便它处理任何注释掉的代码,否则可能会导致挂断。
  2. 如果不匹配,它将寻找标签的开头。请注意,它使用括号来捕获名称。
  3. 该标签将以结束/>,从而完成标签,或者以结束>,在这种情况下,将通过检查标签的内容来继续进行操作。
  4. 它将继续解析,直到到达a为止<,这时它将递归回到表达式的开头,从而允许它处理注释或新标记。
  5. 它会继续循环,直到到达文本末尾或<无法解析的末尾为止。当然,不匹配将导致它重新开始该过程。否则,<大概是此迭代的结束标记的开始。在结束标记中使用反向引用<\/\1>,它将与当前迭代(深度)的开始标记匹配。只有一个捕获组,因此这场比赛很简单。尽管您可以根据需要修改捕获组以仅捕获特定的标签,但这使其与所使用标签的名称无关。
  6. 此时,它将退出当前递归,直至到达下一个级别或以匹配结束。

本示例通过使用字符组来解决空白或识别相关内容的问题,这些字符组仅对<或取反>,或者在注释的情况下,通过使用[\S\s],该字符组将匹配任何内容,包括回车符和换行符,即使是单行模式,一直持续到达到 -->。因此,它只是将一切视为有效,直到达到有意义的程度为止。

对于大多数目的,像这样的正则表达式并不是特别有用。它将验证XML的格式正确,但这仅是XML的全部工作,并且不考虑属性(尽管这很容易添加)。只是这么简单,因为它避免了诸如此类的现实问题以及标签名称的定义。实际使用它会使它更像野兽。通常,真正的XML解析器要好得多。这可能最适合于教授递归的工作原理。

长话短说:使用XML解析器进行实际工作,如果要使用正则表达式,可以使用它。


3
仅当输入格式正确时,此正则表达式才匹配的语句不正确。它不检查名称是否为有效的XML名称,不检查属性,不检查实体和字符引用,不处理CDATA或处理指令。当您说它已经过测试时,我非常怀疑它是否已经在类似于XML一致性测试套件的任何东西上进行了测试。这是我用过的所有用正则表达式处理XML的尝试的问题:它们使用少量输入,但不能使用任何合法传递给应用程序的XML。
迈克尔·凯

2
另外,还有一些格式不正确的输入,正则表达式不匹配。例如,在结束标记中的名称后不允许使用空格。这些故障大多数都可以轻松修复,但是一旦您修复了所有故障,您最终会发现完全无法使用。当然,真正的陷阱是,您不仅希望解析器为您提供是/否的答案,还希望它将信息传递给对它有用的应用程序。
迈克尔·凯

0

不要使用正则表达式解析XML / HTML,请使用适当的XML / HTML解析器和功能强大的 查询。

理论:

根据编译理论,不能使用基于有限状态机的正则表达式来解析XML / HTML 。由于XML / HTML的层次结构,您需要使用下推自动机并使用YACC之类的工具来处理LALR语法。

realLife©®™日常工具

您可以使用以下之一:

xmllint通常在默认情况下与libxml2xpath1 一起安装(检查包装程序以换行符分隔输出)

xmlstarlet可以编辑,选择,转换...默认情况下未安装,xpath1

通过perl的模块XML :: XPath,xpath1安装的xpath

西德尔 xpath3

saxon-lint我自己的项目,@ Michael Kay的Saxon-HE Java库xpath3的包装器

或者您可以使用高级语言和适当的库,我想到:

lxmlfrom lxml import etree

XML::LibXMLXML::XPathXML::Twig::XPathHTML::TreeBuilder::XPath

请检查此示例

DOMXpath请检查此示例


检查:将正则表达式与HTML标签一起使用

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.