返回博客
指南
Mihai MaximLast updated on Mar 31, 20262 min read

像专家一样解析 HTML:用 Python 和正则表达式精通网页抓取

像专家一样解析 HTML:用 Python 和正则表达式精通网页抓取

近几十年来,互联网上的数据量持续增长。人们出于各种目的利用这些数据,从个人兴趣到商业研究,不一而足。

然而,如果这些数据未以XML或JSON等格式呈现,软件应用程序往往难以甚至无法读取。此时,网络爬虫技术便派上了用场。

网络爬虫是指从互联网上收集并处理原始数据的过程。这些数据经过解析后,可用于多种用途,例如价格情报、市场调研、训练AI模型、情感分析、品牌审计以及SEO审计。

网页抓取的关键环节之一是解析HTML。这可以通过多种工具实现,例如Python的BeautifulSoup、NodeJS的Cheerio以及Ruby的Nokogiri。

正则表达式(regex)是一串定义搜索模式的字符序列。

本文将探讨如何使用正则表达式和 Python 解析 HTML 文档,同时还将讨论网络爬虫过程中可能遇到的挑战及相应的替代解决方案。

读完本文后,您将对这一主题以及可用的各种工具和技术有全面的了解。

基础正则表达式解析

大多数通用编程语言都支持正则表达式。您可以在多种编程语言中使用正则表达式,包括 Python、C、C++、Java、Rust、OCaml 和 JavaScript。

以下是从 <title> 标签中提取值的正则表达式示例:

<title>(.*?)</title>

看起来很吓人,对吧?请记住,这只是个开始。我们很快就会深入探索。

本文使用 Python 3.11.1 进行演示。现在我们将这条规则转化为代码。创建一个名为 main.py 的文件,并粘贴以下代码片段:

import re

html = "<html><head><title>Scraping</title></head></html>"

title_search = re.search("<title>(.*?)</title>", html)

title = title_search.group(1)

print(title)

你可以通过执行命令 `python main.py` 来运行这段代码。你将看到输出结果为单词“Scraping”。

在这个示例中,我们使用 `re` 模块来处理正则表达式。`re.search()` 函数用于在字符串中搜索特定的模式。第一个参数是正则表达式模式,第二个参数是我们要搜索的字符串。

本例中的正则表达式模式为 "<title>(.*?)</title>"。它由几个部分组成:

  • <title>:这是一个字面字符串,将精确匹配字符“<title>”。
  • (.*?): 这是一个捕获组,由圆括号表示。字符 . 匹配任意单个字符(换行符除外),量词 * 表示匹配前一个字符 0 次或多次。此外,? 使 * 变为非贪婪模式,这意味着一旦找到结束标签,匹配就会立即停止。
  • </title>:这也是一个字面字符串,将精确匹配字符 "</title>"。

如果找到匹配项,re.search() 函数将返回一个匹配对象,而 group(1) 方法用于提取第一个捕获组匹配的文本,即 title 标签的开始和结束标签之间的文本。

该文本将被赋值给变量 title,输出结果为 "Scraping"。

高级正则表达式解析

仅从单个 HTML 标签中提取数据并没有太大用处。这虽能让你初步了解正则表达式的功能,但无法应用于实际场景。

让我们查看 PyPI 网站(Python 软件包索引)。其首页展示了四项统计数据:项目数量、发布版本数量、文件数量以及用户数量。

我们需要提取项目数量。为此,我们可以使用以下正则表达式:

([0-9,]+) projects

该正则表达式将匹配任何以一个或多个数字开头(可选逗号分隔),并以单词“projects”结尾的字符串。其工作原理如下:

  • ([0-9,]+):这是一个捕获组,由圆括号表示;方括号 [0-9,] 匹配 0 到 9 之间的任意数字以及字符 `,`;量词 `+` 表示匹配前面的字符 1 个或多个。
  • projects:这是一个字面字符串,将精确匹配“projects”。

现在是时候测试这条规则了。请用以下代码片段更新 `main.py`:

import urllib.request

import re

response = urllib.request.urlopen("https://pypi.org/")

html = response.read().decode("utf-8")

matches = re.search("([0-9,]+) projects", html)

projects = matches.group(1)

print(projects)

我们使用 urllib 库中的 urlopen 方法向 pypi.org 网站发送 GET 请求。我们将响应内容读取到 html 变量中。对 HTML 内容应用正则表达式规则,并打印第一个匹配的捕获组。

使用 `python main.py` 命令运行代码并查看输出:它将显示该网站上的项目数量。

提取链接

既然我们已经有一个简单的爬虫可以获取网站的 HTML 文档,让我们稍微修改一下代码。

我们可以使用以下正则表达式提取所有链接:

href=[\'"]?([^\'" >]+)

该正则表达式由几个部分组成:

  • href=:这是一个字面字符串,将精确匹配字符 "href="。
  • [\'"]?: 方括号 [] 匹配其中的任意单个字符,在此情况下即 ' 或 ";量词 ? 表示匹配前面的字符零次或一次,这意味着 href 值可以由 " 或 ' 包围,也可以不包围。
  • ([^\'" >]+):这是一个捕获组,由圆括号表示;方括号内的 ^ 表示否定,将匹配除 '、"、> 或空格以外的任何字符;+ 量词表示匹配前一个字符 1 个或多个,这意味着该组将捕获一个或多个符合该模式的字符。

提取图片

还有一件事,我们几乎就写完了正则表达式规则:我们需要提取图片。让我们使用这条规则:

<img.*?src="(.*?)"

这个正则表达式由几个部分组成:

  • <img:这是一个字面字符串,将精确匹配字符“<img”。
  • .*?: .* 匹配任意字符(除换行符外)0 次或多次,而 ? 量词表示尽可能少地匹配前面的字符;这用于匹配 <img> 标签中 src 属性前出现的任意字符,并允许该模式匹配任何 <img> 标签,无论其拥有多少个属性。
  • src=":这是一个字面字符串,将精确匹配字符 "src="。
  • (.*?): 这是一个捕获组,由圆括号表示;.*? 匹配任意字符(除换行符外)0 次或多次,而 ? 量词表示尽可能少地匹配前面的字符;该组捕获 <img> 标签的 src 值。
  • ": 这是一个字面字符串,它将精确匹配字符 "。

让我们来测试一下。将之前的代码片段替换为以下内容:

import urllib.request

import re

response = urllib.request.urlopen("https://pypi.org/")

html = response.read().decode("utf-8")

images = re.findall('<img.*?src="(.*?)"', html)

print(*images, sep = "\n")

该代码的输出将显示一个列表,其中包含 PyPI 页面上的所有图片链接。

局限性

使用正则表达式进行网页抓取虽是提取网站数据的强大工具,但也有其局限性。使用正则表达式进行网页抓取的主要问题之一是,当 HTML 结构发生变化时,它可能会失败。

例如,请看以下代码示例,我们试图使用正则表达式从 h2 标签中提取文本:

<html>

   <head>

       <title>Example Title</title>

   </head>

   <body>

       <h1>Page Title</h1>

       <p>This is a paragraph under the title</p>

       <h2>First Subtitle</h2>

       <p>First paragraph under the subtitle</p>

       <h2>Second Subtitle</p>

   </body>

</html>

比较第一个 <h2> 标签与第二个。您可能会注意到第二个 <h2> 未正确闭合,且代码中使用了 </p> 而不是 </h2>。让我们将代码片段更新为以下内容:

import re

html = "<html><head><title>Example Title</title></head><body><h1>Page Title</h1><p>This is a paragraph under the title</p><h2>First Subtitle</h2><p>First paragraph under the subtitle</p><h2>Second Subtitle</p></body></html>"

headingTags = re.findall("<h2>(.*?)</h2>", html)

print(*headingTags, sep = "\n")

运行代码并查看输出结果:

First Subtitle

第二个标题标签中的文本缺失了。这是因为正则表达式规则无法匹配未闭合的标题标签。

解决此问题的一个方法是使用 BeautifulSoup 这样的库,它允许你遍历和搜索 HTML 树结构,而不是依赖正则表达式。使用 BeautifulSoup,你可以像这样提取网页的标题:

from bs4 import BeautifulSoup

html = "<html><head><title>Example Title</title></head><body><h1>Page Title</h1><p>This is a paragraph under the title</p><h2>First Subtitle</h2><p>First paragraph under the subtitle</p><h2>Second Subtitle</p></body></html>"

soup = BeautifulSoup(html, 'html.parser')

for headingTag in soup.findAll('h2'):

   print(headingTag.text)

BeautifulSoup 能够处理格式不规范的标签,输出结果如下所示:

First Subtitle

Second Subtitle

这种方法对 HTML 结构的变化更具鲁棒性,因为它不依赖于 HTML 代码中的特定模式。如果您想进一步了解 BeautifulSoup,这篇文章是绝佳的阅读材料。

另一种解决方案是使用 WebScrapingAPI 等网络爬虫 API,它抽象化了网络爬取的复杂性,让你无需担心底层 HTML 结构,即可轻松提取所需数据。

借助 WebScrapingAPI,您只需调用一个简单的 API 接口即可从任何网站提取数据,它会自动处理 HTML 结构的变化。

结语

使用正则表达式进行数据解析,是提取网站数据的强大工具。

在本文中,我们探讨了正则表达式的基础知识、如何使用它们解析 HTML,以及使用过程中可能遇到的挑战。我们还了解到了 BeautifulSoup 等库如何作为替代方案。

您已掌握了如何使用正则表达式从网页中提取数据,以及如何通过使用 BeautifulSoup 这样的更健壮的库来提高代码的可靠性。

网页抓取可能是一项耗时的任务,但借助合适的工具,它可以变得简单高效。如果您正在寻找一种能为您节省时间和精力的网页抓取解决方案,不妨试试 WebScrapingAPI。

我们提供为期 14 天的免费试用,您可以借此测试我们的服务,并亲身体验使用网页抓取 API 的优势。

关于作者
Mihai Maxim, 全栈开发工程师 @ WebScrapingAPI
Mihai Maxim全栈开发工程师

米海·马克西姆(Mihai Maxim)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。