在stackoverflow上,提问者使用正则表达式从HTML中获取一些信息的每个问题似乎都不可避免地会有一个“答案”,即不使用正则表达式来解析HTML。
为什么不?我知道那里有quote-unquote“真正的”HTML解析器,例如Beautiful Soup,我相信它们功能强大且有用,但是如果您只是做一些简单,快速或肮脏的事情,那么为什么当一些正则表达式就可以正常工作时,还要麻烦使用如此复杂的东西吗?
此外,对于正则表达式,是否有一些我不了解的基本知识使它们成为一般解析的糟糕选择?
在stackoverflow上,提问者使用正则表达式从HTML中获取一些信息的每个问题似乎都不可避免地会有一个“答案”,即不使用正则表达式来解析HTML。
为什么不?我知道那里有quote-unquote“真正的”HTML解析器,例如Beautiful Soup,我相信它们功能强大且有用,但是如果您只是做一些简单,快速或肮脏的事情,那么为什么当一些正则表达式就可以正常工作时,还要麻烦使用如此复杂的东西吗?
此外,对于正则表达式,是否有一些我不了解的基本知识使它们成为一般解析的糟糕选择?
对于 quick´n´dirty regexp 就可以了。但要知道的基本知识是,不可能构造一个能够正确解析 HTML 的正则表达式。
原因是正则表达式不能处理任意嵌套的表达式。请参阅可以使用正则表达式匹配嵌套模式吗?
(来自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 数据执行的任何操作,正则表达式只是未来令人心痛的秘诀。
两个快速的原因:
关于一般解析的正则表达式的适用性:它们不适合。您是否见过解析大多数语言所需的各种正则表达式?
就解析而言,正则表达式在“词法分析”(lexer)阶段很有用,在这个阶段,输入被分解为标记。它在实际的“构建解析树”阶段不太有用。
对于 HTML 解析器,我希望它只接受格式良好的 HTML,并且需要正则表达式无法执行的功能(它们不能“计数”并确保给定数量的打开元素由相同数量平衡关闭元素)。
因为有很多方法可以“搞砸”浏览器以相当自由的方式处理 HTML,但是要重现浏览器的自由行为以使用正则表达式覆盖所有情况需要相当多的努力,所以你的正则表达式将不可避免地在某些特殊情况下失败情况下,这可能会在您的系统中引入严重的安全漏洞。
问题是大多数提出与 HTML 和正则表达式有关的问题的用户都会这样做,因为他们找不到自己的有效正则表达式。然后必须考虑使用 DOM 或 SAX 解析器或类似的东西是否会更容易。它们经过优化和构造,用于处理类似 XML 的文档结构。
当然,有些问题可以用正则表达式轻松解决。但重点在于轻松。
如果您只想找到所有看起来对正则表达式没问题的 URL http://.../
。但是,如果您想查找具有类“mylink”的 a-Element 中的所有 URL,您最好使用适当的解析器。
正则表达式不是为处理嵌套标签结构而设计的,而且处理所有可能的实际 HTML 可能出现的边缘情况充其量是复杂的(在最坏的情况下是不可能的)。
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]*? )
)
)
)
>
此表达式从 HTML 元素中检索属性。它支持:
(?:\<\!\-\-(?:(?!\-\-\>)\r\n?|\n|.)*?-\-\>)|(?:<(\S+)\s+(?=.*>)|(?<=[=\s])\G)(?:((?:(?!\s|=).)*)\s*?=\s*?[\"']?((?:(?<=\")(?:(?<=\\)\"|[^\"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!\"|')(?:(?!\/>|>|\s).)+))[\"']?\s*)
检查出来。它与“gisx”标志一起工作得更好,就像在演示中一样。
“这取决于”。由于这里给出的所有原因,正则表达式确实不能也不能以真正的准确性解析 HTML。但是,如果错误的后果(例如不处理嵌套标签)很小,并且如果正则表达式在您的环境中非常方便(例如当您破解 Perl 时),请继续。
假设您正在解析链接到您网站的网页——或许您通过 Google 链接搜索找到了它们——并且您想要一种快速的方法来大致了解链接周围的上下文。您正在尝试运行一个可能会提醒您链接垃圾邮件的小报告,类似的东西。
在这种情况下,错误解析一些文件不会有什么大不了的。除了您之外,没有人会看到错误,如果您非常幸运,将很少有足够的错误可以单独跟进。
我想我是说这是一个权衡。有时,如果准确性不重要,那么实现或使用正确的解析器(尽管可能很简单)可能不值得麻烦。
小心你的假设。例如,如果您尝试解析将公开显示的内容,我可以想到一些正则表达式快捷方式可能适得其反的方法。
在某些情况下,使用正则表达式从 HTML 解析某些信息是正确的方法——这在很大程度上取决于具体情况。
上面的共识是,总的来说这是一个坏主意。但是,如果 HTML 结构是已知的(并且不太可能更改),那么它仍然是一种有效的方法。
请记住,虽然 HTML 本身不规则,但您正在查看的页面部分可能是规则的。
例如,<form>
标签嵌套是错误的;如果网页工作正常,那么使用正则表达式来获取 a<form>
是完全合理的。
我最近只使用 Selenium 和正则表达式进行了一些网页抓取。我侥幸成功了,因为我想要的数据放在<form>
, 并以简单的表格格式放置(所以我什至可以依靠<table>
,<tr>
并且<td>
是非嵌套的——这实际上是非常不寻常的)。在某种程度上,正则表达式甚至几乎是必要的,因为我需要访问的一些结构是由注释分隔的。(Beautiful Soup 可以给你评论,但使用 Beautiful Soup 很难抓取<!-- BEGIN -->
和<!-- END -->
阻止。)
但是,如果我不得不担心嵌套表,我的方法根本行不通!我将不得不依靠美丽的汤。然而,即便如此,有时您也可以使用正则表达式来获取所需的块,然后从那里向下钻取。
实际上,在 PHP 中使用正则表达式进行 HTML 解析是完全可能的。您只需要向后解析整个字符串,每次使用不贪婪的说明符从那里strrpos
查找并重复正则表达式即可克服嵌套标签。<
在大型事物上并不花哨而且速度非常慢,但我将它用于我自己的网站模板编辑器。我实际上并没有解析 HTML,而是我为查询数据库条目以显示数据表而制作的一些自定义标签(我的<#if()>
标签可以通过这种方式突出显示特殊条目)。我不准备在这里和那里只使用几个自创标签(其中包含非常非 XML 数据)的 XML 解析器。
所以,即使这个问题已经死了,它仍然出现在谷歌搜索中。我读了它并认为“接受挑战”并完成了我的简单代码的修复,而无需替换所有内容。决定向任何寻找类似原因的人提供不同的意见。最后一个答案也是 4 小时前发布的,所以这仍然是一个热门话题。
我也为此尝试了一个正则表达式。它主要用于查找与下一个 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[ ... ]]>
<div .../>
<input checked>
<div style='...'>
<div style="...">
<a title='John\'s Story'>
<a href = '...'>
不触发格式错误的标签也很好,比如当你忘记 a <
or时>
。
如果您的正则表达式支持重复命名捕获,那么您就是黄金,但 Pythonre
不支持(我知道正则表达式支持,但我需要使用 vanilla Python)。这是你得到的:
content
- 直到下一个标签的所有内容。你可以忽略这个。markup
- 包含所有内容的整个标签。comment
- 如果是评论,评论内容。cdata
- 如果是<![CDATA[...]]>
,则为 CDATA 内容。close_tag
- 如果是关闭标签 ( </div>
),则为标签名称。tag
- 如果是开放标签 ( <div>
),则为标签名称。attributes
- 标签内的所有属性。如果您没有得到重复的组,请使用它来获取所有属性。attribute
- 重复,每个属性。attribute_name
- 重复,每个属性名称。attribute_value
- 重复,每个属性值。如果被引用,这包括引号。is_self_closing
- 这是/
如果它是一个自闭合标签,否则什么都没有。_q
-_v
忽略这些;它们在内部用于反向引用。如果您的正则表达式引擎不支持重复的命名捕获,则有一个部分被调用,您可以使用它来获取每个属性。只需在组上运行该正则表达式attributes
即可获取每个attribute
,attribute_name
并attribute_value
退出它。
对于像 HTML 这样的语言,正则表达式还不够强大。当然,有一些示例可以使用正则表达式。但一般来说不适合解析。
你,知道……有很多你做不到的心态,我认为围栏两边的每个人都是对的和错的。你可以做到,但它比仅仅针对它运行一个正则表达式需要更多的处理。以这个(我在一个小时内写的)为例。它假定 HTML 是完全有效的,但根据您用于应用上述正则表达式的语言,您可以对 HTML 进行一些修复以确保它会成功。例如,删除不应该存在的结束标签:</img>
例如。然后,将结束的单个 HTML 正斜杠添加到缺少它们的元素等。
例如,我将在编写一个库的上下文中使用它,该库允许我执行类似于 JavaScript 的 HTML 元素检索[x].getElementsByTagName()
。我只是拼接了我在正则表达式的 DEFINE 部分中编写的功能,并使用它来逐步进入元素树,一次一个。
那么,这将是验证 HTML 的最终 100% 答案吗?不,但这是一个开始,只要多做一点工作,就可以完成。但是,尝试在一个正则表达式执行中执行它是不切实际的,也不高效。