返回博客
指南
Mihnea-Octavian ManolacheLast updated on Mar 31, 20262 min read

使用 Pyppeteer 构建网页抓取工具的终极指南

使用 Pyppeteer 构建网页抓取工具的终极指南

Python 和 Web 自动化领域,Selenium 几乎一直是首选工具。至少直到现在都是如此。由于 Puppeteer 在 JavaScript 社区取得的成功,Python 开发者开始越来越关注它。 正是如此,Pyppeteer应运而生。但Pyppeteer究竟是什么?我们为何要选择它而非Selenium?它是否足够可靠,足以构建复杂的解决方案?对于这些问题以及更多疑问,我们将在今天的文章中一一解答。我的目标是,如果你通读本文,至少能掌握以下几点:

  • 对 Pyppeteer 的定义及其应用场景
  • 对 Pyppeteer 与 Selenium 之间差异的理解
  • 使用 Pyppeteer 实现的实际网页爬虫

所以请做好准备,因为今天我们将深入探讨并亲手编写代码!

Pyppeteer究竟是什么?如何使用它?

如果你正在阅读这篇文章,很可能你已经对网页脚本编程有了一定的了解。根据你偏好的编程语言不同,你可能已经听说过 Puppeteer 或 Selenium。但 Pyppeteer 确实是网页抓取领域的新秀。简而言之,Pyppeteer 与 Puppeteer 的相似度远高于 Selenium。

Puppeteer 是一个 Node.js 库,它通过 DevTools 协议来控制无头版 Chrome。而 Pyppeteer 则是 Puppeteer 的 Python 移植版。 与原版 Puppeteer 一样,Pyppeteer 是一个用 Python 编写的库,其核心功能是实现浏览器的自动化操作。换句话说,Pyppeteer 是 Puppeteer API 的 Python 实现,它让你能在 Python 环境中使用 Puppeteer 的各项功能。两者之间的主要区别在于所使用的编程语言。

您应了解的 Pyppeteer 术语

在继续之前,我认为我们应该先讨论一些 Pyppeteer 常用术语:

  • 无头模式(Headless):指在不显示图形用户界面(GUI)的情况下启动浏览器。换言之,它是在“后台”运行的,您无法在屏幕上看到它。通常用于在爬取过程中减少资源占用。
  • 带界面的(Headful):相反,“带界面的”浏览器是指带有图形用户界面(GUI)运行的浏览器。这是无头浏览器的对立面,通常用于测试、调试或手动与网页交互。
  • 浏览器上下文:这是浏览器中所有页面共享的状态。通常用于设置全局浏览器配置,例如 Cookie、HTTP 头部和地理位置。
  • DOM:文档对象模型(DOM)是用于 HTML 和 XML 文档的编程接口。它以树状结构表示网页的结构,其中的节点代表元素。Pyppeteer 允许您通过操作 DOM 与页面元素进行交互。
  • 元素:网页的基本构成单元。它们通过标签、属性和值来定义。

当然,内容远不止于此,您在后续学习中还会接触更多知识。但我希望您能先掌握这些基础概念,以便我们有一个扎实的开端。我相信,了解这些术语将有助于您更好地理解本文的核心内容。

为何要在数据抓取项目中使用 Pyppeteer?

我认为这个问题涉及两个方面。首先,为什么 Pyppeteer 总体上是网络爬取的理想选择;其次,为什么在 Selenium 之外还要使用 Pyppeteer。总体而言,Pyppeteer 的优势包括:

  • 执行 JavaScript:Pyppeteer 提供了 `page.evaluate()` 函数。它允许你在页面上下文中执行 JavaScript 代码。
  • 网络控制:Pyppeteer 提供了 `page.on()` 方法。这让你能够监听页面上发生的网络事件,例如请求和响应。
  • 跟踪与日志记录:Pyppeteer 支持跟踪浏览器活动并记录页面中的浏览器消息。这使得调试、追踪和理解网站运行状况变得十分便捷。

与 Selenium 相比,两者都用于自动化控制网页浏览器,因此非常相似。然而,Pyppeteer 相比 Selenium 具有一些关键的差异和优势:

  • 简洁性:Pyppeteer 的 API 比 Selenium 更简单且更一致,这使得初学者更容易上手。Pyppeteer API 基于 DevTools 协议构建,该协议与浏览器紧密相关,因此易于学习和使用。
  • 性能:由于基于 DevTools 协议构建,Pyppeteer 的运行速度可能快于 Selenium。该协议专为网页调试设计,其运行速度远快于 Selenium WebDriver。
  • 更强的网络控制:Pyppeteer 允许对浏览器的网络设置进行更精细的控制,例如请求拦截和请求/响应阻断。这使得测试和诊断网络相关问题变得更加容易。

当然,这也涉及个人选择。以我为例。日常工作中,我使用 JavaScript 编程,对 Pyppeteer 非常熟悉。但另一方面,我最喜欢的编程语言是 Python。因此,如果我要用自己偏好的语言基于已知技术构建一个爬虫,我可能会选择 Pyppeteer。

言归正传,我认为本文的“理论讲解”部分已经讲完了。现在是时候开始实际编码了。

如何使用 Pyppeteer 创建网页爬虫

在开始编码之前,请允许我向大家介绍 Pyppeteer 官方文档。 我主张在遇到困难时,应优先查阅官方文档。这比在社区(如 Stackoverflow)提问更重要。我通常发现,只要先阅读文档,大多数问题都能找到答案。所以请把这当作我的一番善意建议:每当遇到困难时,先查阅文档,再搜索答案,只有在万不得已时才提问。

#1:搭建环境

首先,作为 Python 开发者,你应该对虚拟环境很熟悉。因此,我们需要做的第一件事就是为项目创建一个虚拟环境。以下是我通常使用的命令序列:

# Create a new directory and navigate into it

~ » mkdir py_project && cd py_project

# Create the virtual environment

~ » python3 -m venv env

# Activate the virtual environment

~ » source env/bin/activate


关于虚拟环境,现在你已经准备就绪。接下来是安装 Pyppeteer。既然终端已经打开,只需输入:

# Install the package using pip

~ » python3 -m pip install pyppeteer

# Open the project in your IDE

~ » code .

#2:创建一个简单的 Pyppeteer 爬虫

上一条命令会打开 Visual Studio Code 或你常用的 IDE。既然已经进入“开发环境”,我们就来创建一个新的 `.py` 文件来存放代码。我将文件命名为 `scraper.py`。请注意,Pyppeteer 原生支持异步执行。因此,让我们将 `asyncio` 和 `pyppeteer` 都导入到文件中:

import asyncio
from pyppeteer import launch

完成这一步后,我们可以继续编写更复杂的代码。总体而言,我并不是函数式编程的坚定拥护者。但我认为将代码拆分成小块有助于更好地学习。因此,让我们将代码封装在一个函数中:

async def scrape(url):

   browser = await launch()

   page = await browser.newPage()

   await page.goto(url)

   content = await page.content()

   await browser.close()

  

   return content

该函数以 URL 作为输入,使用 Pyppeteer 启动一个无头浏览器。随后,它会导航至指定的 URL,获取页面内容,并关闭浏览器。其返回值正是从页面中收集到的 HTML 内容。你可以使用这个函数抓取几乎任何网站。要使用该函数,你需要在 `asyncio` 事件循环中调用它,如下所示:

async def main():

   content = await scrape('https://www.example.com')

   print(content)

loop = asyncio.get_event_loop()

loop.run_until_complete(main())

#3:添加更多功能

到目前为止,我们已经拥有了一个可运行的爬虫。但这基本上就是全部了。如果你想使用 Pyppeteer 构建一个更高级的网页爬虫,就必须为 id 添加更多功能。剧透预警:我们将深入探索面向对象编程的世界。但首先,让我们明确我们的目标。我们希望爬虫能够实现什么功能?

  • 使用自定义参数初始化浏览器
  • 导航并从网页中提取内容
  • 向输入框中输入文本
  • 提取单个元素的值
  • 从多个元素中提取值

3.1. 自定义选项

那么,我们先创建一个新的 `Scraper` 类,稍后再添加其方法:

class Scraper:

   def __init__(self, launch_options: dict) -> None:

   	self.options = launch_options['options']

 	self.viewPort = launch_options['viewPort'] if 'viewPort' in launch_options else None

pass

我们为 Scraper 使用的唯一参数是一个 `launch_options` 字典。如您所见,它内部包含两个键。其中一个键定义 Pyppeteer 的启动选项。第二个键要么是 `None`,要么是一个包含 `viewPort` 的 `width` 和 `height` 的字典。后者用于方法。

3.2. 导航至页面

如果你查看之前使用的函数,会发现它既涵盖了导航功能,也包含从特定 URL 提取原始数据的功能。我们只需稍作调整,将其转换为 Scraper 的方法即可:

async def goto(self, url: str) -> None:

       self.browser = await launch(options=self.options)

       self.page = await self.browser.newPage()

       await self.page.setViewport(self.viewPort) if self.viewPort != None else print('[i] Using default viewport')

       await self.page.goto(url)

该方法非常简单。首先,它会使用我们之前设定的自定义选项启动一个新浏览器。随后创建一个新页面,如果 `launch_options` 字典中包含 `viewPort`,则设置页面的视口;否则,它会输出一条简单提示。最后但同样重要的是,它将我们带到目标页面。

3.3. 从页面中提取原始数据

同样,该方法位于初始的 `scraper` 函数中。我们只需等待 `page.content()` 加载并返回其值:

async def get_full_content(self) -> str:

       content = await self.page.content()

       return content

3.4. 向输入框写入文本

若要使用 Pyppeteer 向输入框写入内容,你需要完成两步操作。首先,定位该元素;其次,向其中添加值。幸运的是,Pyppeteer 提供了实现这两项操作的方法:

async def type_value(self, selector: str, value: str) -> None:

       element = await self.page.querySelector(selector)

       await element.type(value)

3.5. 从页面提取值

请记住,我们需要能够从单个元素中提取值,也能从多个元素中提取值。虽然可以用一个方法同时处理这两种情况,但我通常喜欢将它们分开。因此,目前我将再添加两个方法:

async def extract_one(self, selector) -> str:

       element = await self.page.querySelector(selector)

       text = await element.getProperty("textContent")

       return await text.jsonValue()

这里,我们使用 `querySelector` 方法定位元素。随后获取 `textContent` 并返回其 `jsonValue()`。另一方面,当需要选择多个元素时,我们将使用 `querySelector`:

async def extract_many(self, selector) -> list:

       result = []

       elements = await self.page.querySelectorAll(selector)

       for element in elements:

           text = await element.getProperty("textContent")

           result.append(await text.jsonValue())

       return result

该方法的工作原理与 `extract_one` 类似,唯一的区别在于返回值。这次我们返回的是所选元素内所有文本组成的列表。我想,至此,我们已实现了所有目标。

#4:实现隐身

在网页抓取中,隐蔽性可理解为不被察觉的能力。当然,构建一个完全无法被检测到的抓取器需要大量工作。例如,Web Scraping API 的隐身模式由一个专门团队维护。他们为此付出的努力使得我们的抓取器在每次请求时都拥有独特的指纹。

但本教程的总体目标是引导大家走上正确的道路。而对于基于 Pyppeteer 的完整网页爬虫而言,正确的道路意味着需要为其添加一些隐蔽功能。 幸运的是,正如 Node.js 中有 `puppeteer-extra-plugin-stealth` 一样,Python 也有相应的包。它的名字直截了当,就叫 `pyppeteer-stealth`。要将其添加到你的项目中,首先使用 pip 安装:

~ » python3 -m pip install pyppeteer_stealth

然后将其导入项目,并仅需添加一行代码:

async def goto(self, url: str) -> None:

       self.browser = await launch(options=self.options)

       self.page = await self.browser.newPage()

	  # Make it stealthy

       await stealth(self.page)  

       await self.page.setViewport(self.viewPort) if self.viewPort != None else print('[i] Using default viewport')

       await self.page.goto(url)

以下是运行爬虫的方法。我在代码中添加了一些注释,以说明每个步骤的作用:

async def main():

   # Define the launch options dictionary

   launch_options = {

       'options': {

           'headless': False,

           'autoClose': True

       },

       'viewPort': {

           'width': 1600,

           'height': 900

       }

   }

   # Initialize a new scraper

   scraper = Scraper(launch_options)

   # Navigae to your target

   await scraper.goto('https://russmaxdesign.github.io/accessible-forms/accessible-name-input01.html')

   # Type `This is me` inside the input field

   await scraper.type_value(

       '#fish',

       'This is me')

   # Scrape the entire page

   content = await scraper.get_full_content()

   print(content)

   # Scrape one single element

   el = await scraper.extract_one('body > div:nth-child(14) > ul')

   print(el)

   # Scrape multiple elements

   els = await scraper.extract_many('p')

   print(els)

loop = asyncio.get_event_loop()

loop.run_until_complete(main())

结论

Pyppeteer 是一款出色的网页抓取工具。它将 Puppeteer 的完整 API 移植到了 Python 平台,使 Python 开发者无需学习 JavaScript 即可使用这项技术。此外,虽然我认为它无法完全替代 Selenium,但无疑是一个非常不错的替代方案。

希望今天的文章能为你的学习之路增添价值。既然我喜欢挑战大家的极限,那就请你在此基础上更进一步。我们共同构建的爬虫是一个非常好的起点,同时也引出了编程中的关键概念:面向对象编程(OOP)。因此,我挑战你为 `Scraper` 类添加更多方法,让它变得真正出色。

关于作者
Mihnea-Octavian Manolache, 全栈开发工程师 @ WebScrapingAPI
Mihnea-Octavian Manolache全栈开发工程师

Mihnea-Octavian Manolache 是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

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

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