返回博客
指南
米赫内亚-奥克塔维安·马诺拉切2023年2月28日阅读时长:11分钟

使用 Pyppeteer 构建网络抓取器终极指南

使用 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 相关语境中常用的几个术语:

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

当然,这其中还有更多内容,你会在学习过程中逐渐掌握。但我希望你能先对这些概念有个大致的了解,这样我们才能有一个扎实的开端。我相信,掌握这些术语将有助于你更好地理解本文的核心内容。

为什么要在网页抓取项目中使用 Pyppeteer?

我认为这个问题有两个方面。首先,为什么Pyppeteer总体上是网络爬虫的理想选择。其次,为什么选择Pyppeteer而非Selenium。总体而言,Pyppeteer的一些优势包括:

  • 评估 JavaScript:Pyppeteer 提供了一个 `page.evaluate()` 函数。它允许您在页面上下文中执行 JavaScript 代码。
  • 网络控制:Pyppeteer 提供了 `page.on()` 方法。该方法允许您监听页面上发生的网络事件,例如请求和响应。
  • 跟踪与日志记录:Pyppeteer 允许您跟踪浏览器的活动,并记录页面中的浏览器消息。这使得调试、追踪和理解网站的运行情况变得轻而易举。

与 Selenium 相比,两者非常相似,因为它们都用于自动化控制网页浏览器。不过,Pyppeteer 相比 Selenium 仍存在一些关键差异和优势:

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

当然,这还涉及一个选择问题。以我为例。平时我主要用 JavaScript 编写代码,对 Puppeteer 也相当熟悉。但另一方面,我最喜欢的编程语言是 Python。因此,如果我要用自己偏好的语言开发一个基于已知技术的爬虫工具,我可能会选择 Pyppeteer。

话虽如此,我认为本文的“理论”部分已经讲完了。现在是时候开始实际编码了。

如何使用 Pyppeteer 创建网页抓取工具

在开始编写代码之前,让我先向大家介绍一下Pyppeteer 的官方文档。 我一直提倡,每当遇到困难时,首先查阅官方文档。这比直接在社区(比如 Stackoverflow)提问要好。我通常发现,只要先阅读文档,大多数问题的答案都能找到。所以,请把这当作我的一番善意建议。无论何时遇到困难,请先查阅文档,然后搜索答案,只有在万不得已的情况下才提问。

#1:配置环境

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

# 创建一个新目录并进入该目录

~ » mkdir py_project && cd py_project 

# 创建虚拟环境

~ » python3 -m venv env 

# 激活虚拟环境

~ » source env/bin/activate

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

# 使用 pip 安装该包

~ » python3 -m pip install pyppeteer

# 在您的 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
并排显示的代码编辑器窗格中,展示了一个能启动浏览器并加载页面的 Puppeteer 脚本

该函数以 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] 使用默认视口')

       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()

	  # 设置为隐身模式

       await stealth(self.page)  

       await self.page.setViewport(self.viewPort) if self.viewPort != None else print('[i] 使用默认视口')

       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 是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

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

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