使用 lxml 和 Python 进行 Web 抓取的简介

2025-05-28

使用 lxml 和 Python 进行 Web 抓取的简介

你为什么要费心学习如何进行网页爬虫呢?如果你的工作不需要你学习它,那么让我给你一些动力。如果你想创建一个网站,从亚马逊、沃尔玛和其他一些在线商店精选最便宜的商品,该怎么办?很多这样的在线商店并没有提供使用 API 轻松访问其信息的方法。在没有 API 的情况下,你唯一的选择就是创建一个网页爬虫,它可以自动从这些网站提取信息,并以易于使用的方式提供给你。

以下是 JSON 格式的典型 API 响应示例。这是来自 Reddit 的响应:

JSON 格式的典型 API 响应

有很多 Python 库可以帮助你进行网页数据抓取。例如lxmlBeautifulSoup以及一个功能齐全的框架Scrapy。大多数教程都讨论了 BeautifulSoup 和 Scrapy,所以我决定在这篇文章中介绍 lxml 。我将教你 XPath 的基础知识以及如何使用它们从 HTML 文档中提取数据。我将通过几个不同的示例,帮助你快速掌握 lxml 和 XPath 的使用方法。

如果您是游戏玩家,您一定已经知道(并且很可能喜欢)这个网站。我们将尝试从Steam中提取数据。更具体地说,我们将从“热门新发行”信息中进行选择。我将把它分成两部分。在这一部分中,我们将创建一个 Python 脚本,它可以提取游戏名称、游戏价格、与每个游戏相关的不同标签以及目标平台。在第二部分中,我们将把这个脚本转换为基于 Flask 的 API,然后将其托管在 Heroku 上。

Steam 热门新品

步骤 1:探索 Steam

首先,打开 Steam 上的“热门新发行”页面,向下滚动直到看到“热门新发行”标签。这时,我通常会打开 Chrome 开发者工具,查看哪些 HTML 标签包含所需的数据。我经常使用元素检查器工具(开发者工具左上角的按钮)。它允许您只需单击一下即可查看页面上特定元素背后的 HTML 标记。概括地说,网页上的所有内容都封装在 HTML 标签中,并且标签通常是嵌套的。您需要确定需要从哪些标签中提取数据,然后就可以开始了。在我们的例子中,如果我们仔细查看,就会发现每个单独的列表项都封装在一个锚点 ( a) 标签中。

锚标签本身封装在 中,div其 ID 为tab_newreleases_content。我之所以提到 ID,是因为此页面上有两个标签页。第二个标签页是标准的“新品发布”标签页,我们不想从该标签页中提取信息。因此,我们将首先提取“热门新品发布”标签页,然后再从此标签页中提取所需信息。

第 2 步:开始编写 Python 脚本

现在是创建一个新的 Python 文件并开始编写脚本的最佳时机。我将创建一个scrape.py文件。现在让我们导入所需的库。第一个是requests库,第二个也是lxml.html库。

import requests
import lxml.html
Enter fullscreen mode Exit fullscreen mode

如果您尚未requests安装,则可以通过在终端中运行以下命令轻松安装它:

$ pip install requests
Enter fullscreen mode Exit fullscreen mode

请求库将帮助我们在 Python 中打开网页。我们lxml也可以用来打开 HTML 页面,但它并不适用于所有网页,所以为了安全起见,我打算使用requests

现在让我们使用请求打开网页并将该响应传递给lxml.html.fromstring

html = requests.get('https://store.steampowered.com/explore/new/')
doc = lxml.html.fromstring(html.content)
Enter fullscreen mode Exit fullscreen mode

这为我们提供了一个HtmlElement类型的对象。该对象具有xpath可用于查询 HTML 文档的方法。这为我们提供了一种从 HTML 文档中提取信息的结构化方法。

步骤 3:启动 Python 解释器

现在保存此文件并打开终端。从文件中复制代码scrape.py并将其粘贴到 Python 解释器会话中。

Python终端

我们这样做是为了能够快速测试我们的 XPath,而无需不断地编辑、保存和执行我们的scrape.py文件。

让我们尝试编写一个 XPath 来提取包含“热门新品”标签的 div。我会在后面解释代码:

new_releases = doc.xpath('//div[@id="tab_newreleases_content"]')[0]
Enter fullscreen mode Exit fullscreen mode

divs此语句将返回HTML 页面中所有 id 为 的 列表tab_newreleases_content。现在,因为我们知道页面上只有一个 div 具有此 id,所以我们可以从列表中取出第一个元素([0]),这就是我们需要的div。让我们分解一下 ,xpath并尝试理解它:

  • //双斜杠表示lxml我们要搜索 HTML 文档中所有符合我们需求/过滤器的标签。另一个选项是使用/单斜杠。单斜杠仅返回符合我们需求/过滤器的直接子标签/节点
  • div告诉我们在 HTML 页面中lxml搜索divs
  • [@id="tab_newreleases_content"]告诉lxml我们我们只对那些divs具有 id 的tab_newreleases_content

太棒了!我们得到了所需的div。现在让我们回到 Chrome 并检查哪个标签包含发行版的标题。

步骤 4:提取标题和价格

从 Steam 版本中提取标题

标题包含在一个类名为 的 div 中tab_item_name。现在我们已经提取了“热门新品”标签页,可以对该标签页运行进一步的 XPath 查询了。在我们之前运行代码的同一个 Python 控制台中写下以下代码:

titles = new_releases.xpath('.//div[@class="tab_item_name"]/text()')
Enter fullscreen mode Exit fullscreen mode

这将返回“热门新作”标签页中所有游戏的标题。预期输出如下:

Steam 终端发布标题

让我们稍微分解一下这个 XPath,因为它与上一个有点不同。

  • .new_releases告诉 lxml 我们只对标签的子标签感兴趣
  • [@class="tab_item_name"]divs与我们之前基于进行过滤的方式非常相似id。唯一的区别是,这里我们根据类名进行过滤
  • /text()告诉 lxml 我们想要刚刚提取的标签中包含的文本。在本例中,它返回 div 中包含的带有tab_item_name类名的标题

现在我们需要提取游戏的价格。我们可以通过运行以下代码轻松完成:

prices = new_releases.xpath('.//div[@class="discount_final_price"]/text()')
Enter fullscreen mode Exit fullscreen mode

我觉得这段代码不需要解释,因为它和标题提取代码非常相似。我们唯一的改动就是类名的改变。

从蒸汽中提取价格

步骤5:提取标签

现在我们需要提取与标题相关的标签。以下是 HTML 标记:

HTML 标记

在 Python 终端中写下以下代码来提取标签:

tags = new_releases.xpath('.//div[@class="tab_item_top_tags"]')
total_tags = []
for tag in tags:
    total_tags.append(tag.text_content())
Enter fullscreen mode Exit fullscreen mode

我们在这里做的是提取divs包含游戏标签的内容。然后循环遍历提取出的标签列表,并使用text_content()方法从这些标签中提取文本。text_content()返回 HTML 标签中包含的文本(不包含 HTML 标记)。

注意:我们也可以使用列表推导式来缩短代码。我这样写,是为了即使不懂列表推导式的人也能理解。无论如何,以下是替代代码:

tags = [tag.text_content() for tag in new_releases.xpath('.//div[@class="tab_item_top_tags"]')]
Enter fullscreen mode Exit fullscreen mode

让我们将列表中的标签分开,以便每个标签都是一个单独的元素:

tags = [tag.split(', ') for tag in tags]
Enter fullscreen mode Exit fullscreen mode

步骤 6:提取平台

现在剩下的就是提取与每个标题相关的平台。以下是 HTML 标记:

HTML 标记

这里的主要区别在于,平台名称并非包含在特定标签内的文本中。它们被列为类名。有些游戏只关联一个平台,例如:

<span class="platform_img win"></span>
Enter fullscreen mode Exit fullscreen mode

有些游戏有 5 个与之相关的平台,例如:

<span class="platform_img win"></span>
<span class="platform_img mac"></span>
<span class="platform_img linux"></span>
<span class="platform_img hmd_separator"></span>
<span title="HTC Vive" class="platform_img htcvive"></span>
<span title="Oculus Rift" class="platform_img oculusrift"></span>
Enter fullscreen mode Exit fullscreen mode

我们可以看到,这些文件spans都包含平台类型作为类名。它们之间唯一的共同点spans是都包含platform_img类。首先,我们将提取divs包含tab_item_details类的 ,然后提取spans包含platform_img类的 ,最后从中提取第二个类名spans。代码如下:

platforms_div = new_releases.xpath('.//div[@class="tab_item_details"]')
total_platforms = []

for game in platforms_div:
    temp = game.xpath('.//span[contains(@class, "platform_img")]')
    platforms = [t.get('class').split(' ')[-1] for t in temp]
    if 'hmd_separator' in platforms:
        platforms.remove('hmd_separator')
    total_platforms.append(platforms)
Enter fullscreen mode Exit fullscreen mode

在第 1 行中,我们首先提取。第 5 行tab_item_details div的 XPath略有不同。这里我们使用 ,而不是简单地使用。原因是 会返回仅包含与其关联的类的条目。如果包含其他类,则不会返回。而会过滤所有包含 的类。无论它是唯一的类,还是有其他与该标签关联的类,都无关紧要。[contains(@class, "platform_img")][@class="platform_img"][@class="platform_img"]spansplatform_imgspans[contains(@class, "platform_img")]spansplatform_img

第 6 行,我们使用列表推导来缩减代码大小。该.get()方法允许我们提取标签的属性。这里我们使用它来提取class的属性span。该方法返回一个字符串.get()。对于第一个游戏,返回的字符串是 ,platform_img win因此我们根据逗号和空格将该字符串拆分,然后将拆分后的字符串的最后一部分(即实际的平台名称)存储在列表中。

在第 7-8 行中,我们将从hmd_separator列表中删除(如果存在)。这是因为hmd_separator不是一个平台。它只是一个垂直分隔符,用于将实际平台与 VR/AR 硬件区分开。

步骤7:结论

这是我们目前拥有的代码:

import requests
import lxml.html

html = requests.get('https://store.steampowered.com/explore/new/')
doc = lxml.html.fromstring(html.content)

new_releases = doc.xpath('//div[@id="tab_newreleases_content"]')[0]

titles = new_releases.xpath('.//div[@class="tab_item_name"]/text()')
prices = new_releases.xpath('.//div[@class="discount_final_price"]/text()')

tags = [tag.text_content() for tag in new_releases.xpath('.//div[@class="tab_item_top_tags"]')]
tags = [tag.split(', ') for tag in tags]

platforms_div = new_releases.xpath('.//div[@class="tab_item_details"]')
total_platforms = []

for game in platforms_div:
    temp = game.xpath('.//span[contains(@class, "platform_img")]')
    platforms = [t.get('class').split(' ')[-1] for t in temp]
    if 'hmd_separator' in platforms:
        platforms.remove('hmd_separator')
    total_platforms.append(platforms)
Enter fullscreen mode Exit fullscreen mode

现在我们只需要返回 JSON 格式的响应,这样我们就可以轻松地将其转换为基于 Flask 的 API。代码如下:

output = []
for info in zip(titles,prices, tags, total_platforms):
    resp = {}
    resp['title'] = info[0]
    resp['price'] = info[1]
    resp['tags'] = info[2]
    resp['platforms'] = info[3]
    output.append(resp)
Enter fullscreen mode Exit fullscreen mode

这段代码不言自明。我们使用该zip函数并行循环遍历所有这些列表。然后,我们为每个游戏创建一个字典,并将标题、价格、标签和平台作为该字典中的单独键。最后,我们将该字典附加到输出列表中。

在以后的文章中,我们将研究如何将其转换为基于 Flask 的 API 并将其托管在 Heroku 上。

本文由Python Tips的Yasoob撰写。希望大家喜欢本教程。如果您想阅读更多类似的教程,请访问Python Tips 。我定期在该博客上撰写 Python 技巧、窍门和教程。如果您有兴趣学习中级 Python,请点击此处查看我的开源书籍

祝你有美好的一天!

声明一下:我们是一家伐木公司@ Timber。如果您能试用一下我们的产品(真的很棒!),我们会非常高兴,但我们的宣传也仅限于此。

文章来源:https://dev.to/timber/an-intro-to-web-scraping-with-lxml-and-python-32hl
PREV
不要错过 CSS 变量
NEXT
流式 HTML 的怪异而晦涩的艺术