Python:如何将多个正则表达式匹配结果存储在元组中?

5

我正在尝试使用正则表达式制作一个简单的基于Python的HTML解析器。我的问题在于如何使我的正则表达式搜索查询找到所有可能的匹配项,然后将它们存储在一个元组中。

假设我有一个页面,其中包含存储在变量HTMLtext中的以下内容:

<ul>
<li class="active"><b><a href="/blog/home">Back to the index</a></b></li>
<li><b><a href="/blog/about">About Me!</a></b></li>
<li><b><a href="/blog/music">Audio Production</a></b></li>
<li><b><a href="/blog/photos">Gallery</a></b></li>
<li><b><a href="/blog/stuff">Misc</a></b></li>
<li><b><a href="/blog/contact">Shoot me an email</a></b></li>
</ul>

我希望对这段文本进行正则表达式搜索,并返回一个包含每个链接最后一个URL目录的元组。因此,我想要返回类似于以下内容:
pages = ["home", "about", "music", "photos", "stuff", "contact"]

到目前为止,我能使用正则表达式搜索一个结果:

pages = [re.compile('<a href="/blog/(.*)">').search(HTMLtext).group(1)]

运行这个表达式会使pages = ['home']

我该如何让正则表达式搜索整个文本,将匹配的文本附加到此元组中?

(注:我知道我可能不应该使用正则表达式来解析HTML。但是我仍然想知道如何做到这一点。)

5个回答

2
使用 re 模块的 findall 函数:
pages = re.findall('<a href="/blog/([^"]*)">',HTMLtext)
print(pages)

输出:

['home', 'about', 'music', 'photos', 'stuff', 'contact']

@tchrist 您是正确的。我没有在模式本身中查找。OP编写的方式 .* 消耗了所有符号直到行尾,然后回溯以匹配以下的 ",这会减慢解析速度。我会在我的答案中更正这个模式。 - ovgolovin
这只有在HTML中有换行符,且每行只有一个此类链接时才有效—这是很少见的。请参阅我的答案以获取修复方法。是的,我喜欢你的修复方案:否定字符类不仅更高效,而且更正确,比最小匹配要好。 - tchrist

2
您的模式不能适用于所有输入,包括您的输入。.* 将会太过贪婪(严格来说,它会找到一个最大匹配),导致它成为第一个 href 和最后一个相应的关闭标签。解决这个问题的两种最简单方法是使用最小匹配,或者使用反向字符类。
# minimal match approach
pages = re.findall(r'<a\s+href="/blog/(.+?)">', 
                   full_html_text, re.I + re.S)

# negated charclass approach
pages = re.findall(r'<a\s+href="/blog/([^"]+)">',
                   full_html_text, re.I)

必要警告

对于简单且约束良好的文本,正则表达式是完全可以胜任的;毕竟在编辑HTML时我们使用正则表达式进行搜索和替换!但是,如果你对输入的内容了解得越少,它就变得越加复杂,例如:

  • 如果在<ahref之间有其他字段,比如<a title="foo" href="bar">
  • 大小写问题,例如<A HREF='foo'>
  • 空格问题
  • 使用不同类型的引号,例如href='/foo/bar'而不是href="/foo/bar"
  • 嵌入HTML注释

这并不是所有需要考虑的问题,还有其他问题也需要考虑。因此,在HTML中使用正则表达式是可能的,但它是否可行取决于太多其他因素,无法判断。

然而,从你展示的小例子来看,对于你自己的情况,它看起来是完全可以的。你只需改进模式并调用正确的方法即可。


据我所读,否定字符类比非贪婪量词更快(因为它避免了很多回溯步骤)。 - ovgolovin
@ovgolovin 您是100%正确的,否定字符类更快。还存在正确性问题。通常,像 A.*?B 这样的模式实际上并不会阻止 .*? 部分中出现 B; 为此,您必须包含一个前瞻否定,例如 A(?:(?!B).)*B。如果您编写 A.*?BC,则可能会发生这种情况,因为为了使 C 为真,它可能必须在 .*? 中包括 B。简单地说,这样的字符串是 "AxxxBxxxBC" - tchrist
@tchrist 谢谢您提供这个优雅的解决方案(以及有用的警告)。我正在学习正则表达式,所以关于贪婪/非贪婪模式的讨论对我很有帮助。 - hao_maike
1
如果你正在学习正则表达式,你需要养成使用原始字符串的习惯,比如 r'...',以避免双反斜杠。你可以看看我的其他正则表达式答案。虽然大多数(但不是全部)都是用 Perl 写的,但通常这并不重要,因为模式可以直接转换成 Python,没有任何问题。对于涉及到Unicode 属性的更复杂的例子,比如 \p{Greek} 或者 \p{Dash},你需要在 Python 2 和 3 中都使用 Matthew Barnett 的 regex 库。 - tchrist

1

使用findall代替search

>>> pages = re.compile('<a href="/blog/(.*)">').findall(HTMLtext)
>>> pages
['home', 'about', 'music', 'photos', 'stuff', 'contact']

@mr_schlomo,除非您的HTML中实际上有换行符,并且每行只有一个此类链接,否则这不起作用。还有其他问题,请参见我的答案的强制警告。 - tchrist

1

要查找所有结果,请使用 findall()。此外,您只需要编译re一次,然后就可以重复使用它。

href_re = re.compile('<a href="/blog/(.*)">')  # Compile the regexp once

pages = href_re.findall(HTMLtext)  # Find all matches - ["home", "about",

这在大多数HTML页面上不起作用,因为您假定换行符停止贪婪的.*,并且每行只有一个链接。 - tchrist
@tchrist 我认为没有人真正研究过这个模式。他们只是回答了问题(关于findall)。我不认为忽视这样的错误是好的,但这就是事实(除了实际问题之外,没有人关心其他任何事情)。你能够注意到并指出模式中的错误是非常好的。 - ovgolovin
1
@ovgolovin,这些东西对我来说已经是信手拈来了。你可以说我是正则表达式的本地人,因为这些数百个答案应该能证明这一点。 :) 顺便说一句,对于Python正则表达式,我推荐使用Matthew Barnett的替代regex模块;它比re模块处理Unicode要好得多,并且还有很多其他很酷的功能。 - tchrist

1

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