集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。加入到数百名开发者中,将你的采集器发布到市场,从而给你带来被动收入吧!
Puppeteer 是一款功能强大的工具,可通过浏览器自动化进行测试和网页抓取。虽然它是一个 JavaScript 库,但它的流行促使开发者社区为其他语言创建非官方端口。其中之一就是 Puppeteer PHP 库,它允许 PHP 用户无需切换到 JavaScript 即可获得 Puppeteer 的好处。
在本指南中,您将了解 PHP 版 Puppeteer 的基础知识,然后了解更复杂的浏览器交互。您将了解:
Puppeteer是一款深受开发人员喜爱的 JavaScript 无头浏览器库,因为它拥有直观而丰富的 API。它简化了跨平台浏览器自动化,支持测试和网页抓取活动。
Puppeteer 有一个非官方的 PHP 端口,名为PuPHPeteer。截至 2023 年 1 月 1 日,原始项目不再维护。不过,有几个最新的分支,zoonru/puphpeteer是最受欢迎的一个。
让我们开始使用 Puppeteer for PHP 的第一步。您将构建一个自动脚本,从此无限滚动演示中提取数据:
无限滚动页面
当您向下滚动时,此网页会通过 AJAX 动态加载新产品。与其交互需要能够执行 JavaScript 的浏览器自动化工具,例如 Puppeteer。
按照以下步骤构建针对此动态内容页面的数据抓取工具!
开始之前,请确保您的机器上安装了PHP 8+、Composer和Node.js。按照三个链接获取设置说明。
现在,您已经拥有初始化 PHP Composer 项目所需的一切。创建一个php-puppeteer-project文件夹并将其输入到终端中:
mkdir php-puppeteer-project
cd php-puppeteer-project
运行init命令在文件夹内创建一个新的 Composer 项目。按照向导并根据需要回答问题。默认答案将执行以下操作:
composer init
php-puppeteer-project现在包含 Composer 项目。
使用以下命令将zoon/puphpeteer添加到项目依赖项中:
composer require zoon/puphpeteer
该命令可能由于以下错误而失败:
- clue/socket-raw[v1.2.0, ..., v1.6.0] require ext-sockets * -> it is missing from your system. Install or enable PHP's sockets extension.
在这种情况下,您需要安装并启用ext-sockets PHP extension。然后重新启动上述composer require命令。
接下来,安装github:zoonru/puphpeteernpm 包:
npm install github:zoonru/puphpeteer
这将需要一段时间,所以请耐心等待。
在您最喜欢的 PHP IDE(例如带有 PHP 扩展的 Visual Studio Code)中加载项目文件夹。在文件夹中创建一个scraper.php文件/src并导入 Puppeteer PHP 包:
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
// scraping logic...
您可以在项目的根文件夹中使用此命令运行上述 PHP Puppeteer 脚本:
php src/scraper.php
您的 PHP 设置已准备就绪!
首先,初始化一个 Puppeteer 实例并启动它以打开可控制的 Chromium 窗口:
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
笔记 默认情况下,PHP Puppeteer 以无头模式启动 Chromium。如果您想要在浏览器中跟踪脚本在目标页面上的操作,请将headless选项设置为。这对于调试特别有用!false
现在可以初始化一个新页面以及goto()访问目标页面的方法:
$page = $browser->newPage();
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
然后,使用该content()方法检索页面的 HTML 源代码。使用以下命令在终端中打印它echo:
$html = $page->content();
echo $html;
不要忘记在脚本末尾使用此行来释放浏览器资源:
$browser->close();
此时应包含以下内容scraper.php:
scraper.php
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
// open a new Chromium browser window
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
// open a new page in the browser
$page = $browser->newPage();
// visit the target page
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
// retrieve the source HTML code of the page and
// print it
$html = $page->content();
echo $html;
// release the browser resources
$browser->close();
在 headed 模式下运行脚本。Puppeteer PHP 库将打开一个 Chromium 窗口并访问 Infinite Scrolling 演示页面:
无限滚动演示 PHP 脚本还将在终端中打印以下 HTML:
<html class="h-full"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Learn to scrape infinite scrolling pages"><title>Scraping Infinite Scrolling Pages (Ajax) | ScrapingClub</title>
<link rel="icon" href="/static/img/icon.611132651e39.png" type="image/png">
<!-- Omitted for brevity... -->
好了!这就是目标页面的 HTML 代码。在下一步中,您将了解如何从中提取数据。
Puppeteer 解析页面的 HTML 内容并提供从中提取数据的 API。
假设您的 PHP 抓取脚本的目标是收集页面上每个产品元素的名称和价格。您需要执行以下操作:
PuPHPeteer 支持XPath表达式和CSS 选择器,这是从 DOM 获取元素的两种最流行的节点选择策略。CSS 选择器易于使用且直观,而 XPath 表达式更灵活但更复杂。
如需完整比较,请阅读我们的CSS 选择器与 XPath 指南。
为了简单起见,我们来选择 CSS 选择器。要定义正确的选择器,您需要查看产品节点的 HTML 代码。因此,在浏览器中打开目标网站并使用 DevTools 检查它:
检查元素 点击全屏打开图片 展开 HTML 代码并注意每个产品:
是一个具有类的元素post。 包含 中的名称和 中的价格。 由于该页面包含许多产品,因此$products为抓取的数据初始化一个数组:
$products = [];
接下来,使用该querySelectorAll()方法并选择 HTML 产品节点。这将在页面上应用 CSS 选择器:
$product_elements = $page->querySelectorAll('.post');
迭代它们并应用数据提取逻辑。检索感兴趣的数据,创建新对象,并使用它们来填充$products:
foreach ($product_elements as $product_element) {
// select the name and price elements
$name_element = $product_element->querySelector('h4');
$price_element = $product_element->querySelector('h5');
// retrieve the data of interest
$name = $name_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
$price = $price_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
// create a new product object and add it to the list
$product = ['name' => $name, 'price' => $price];
$products[] = $product;
}
要使用 Puppeteer PHP 包从节点提取数据,您必须编写自定义的JsFunction。将其传递给evaluate()方法以将其应用于给定的节点。
使用以下方式记录所有抓取的产品:
print_r($products);
您的 PHP Puppeteerscraper.php脚本现在将包含:
scraper.php
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
use Nesk\Rialto\Data\JsFunction;
// open a new Chromium browser window
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
// open a new page in the browser
$page = $browser->newPage();
// visit the target page
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
// where to store the scraped data
$products = [];
// select all product nodes on the page
$product_elements = $page->querySelectorAll('.post');
// iterate over the product elements and
// apply the scraping logic
foreach ($product_elements as $product_element) {
// select the name and price elements
$name_element = $product_element->querySelector('h4');
$price_element = $product_element->querySelector('h5');
// retrieve the data of interest
$name = $name_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
$price = $price_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
// create a new product object and add it to the list
$product = ['name' => $name, 'price' => $price];
$products[] = $product;
}
// print the scraped data
print_r($products);
// release the browser resources
$browser->close();
执行 PHP 脚本,它将在终端中产生以下输出:
Array
(
[0] => Array
(
[name] => Short Dress
[price] => $24.99
)
// omitted for brevity...
[9] => Array
(
[name] => Fitted Dress
[price] => $34.99
)
)
太棒了!PHP 解析逻辑按预期工作。剩下的就是将收集的数据导出为更好的格式,例如 CSV。
PHP 标准 API 提供了将抓取的数据导出到输出 CSV 文件所需的一切。使用fopen()创建products.csv文件,然后用它填充文件fputcsv()。此命令将产品对象数组转换为 CSV 记录并将其附加到 CSV 文件中。
// open the output CSV file
$csvFilePath = 'products.csv';
$csvFile = fopen($csvFilePath, 'w');
// write the header row
$header = ['name', 'price'];
fputcsv($csvFile, $header);
// add each product to the CSV file
foreach ($products as $product) {
fputcsv($csvFile, $product);
}
// close the CSV file
fclose($csvFile);
将上述逻辑整合到中scraper.php,你将获得:
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
use Nesk\Rialto\Data\JsFunction;
// open a new Chromium browser window
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
// open a new page in the browser
$page = $browser->newPage();
// visit the target page
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
// where to store the scraped data
$products = [];
// select all product nodes on the page
$product_elements = $page->querySelectorAll('.post');
// iterate over the product elements and
// apply the scraping logic
foreach ($product_elements as $product_element) {
// select the name and price elements
$name_element = $product_element->querySelector('h4');
$price_element = $product_element->querySelector('h5');
// retrieve the data of interest
$name = $name_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
$price = $price_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
// create a new product object and add it to the list
$product = ['name' => $name, 'price' => $price];
$products[] = $product;
}
// open the output CSV file
$csvFilePath = 'products.csv';
$csvFile = fopen($csvFilePath, 'w');
// write the header row
$header = ['name', 'price'];
fputcsv($csvFile, $header);
// add each product to the CSV file
foreach ($products as $product) {
fputcsv($csvFile, $product);
}
// close the CSV file
fclose($csvFile);
// release the browser resources
$browser->close();
启动 Puppeteer PHP 抓取脚本:
php src/scraper.php
脚本执行结束后,products.csv项目根文件夹中会出现一个文件,打开它,你会看到以下记录:
产品 CSV 文件
太棒了!现在您已经了解了在 PHP 中使用 Puppeteer 的基础知识。
如您所见,当前输出仅包含 10 条记录。这是因为页面的初始视图仅包含少量产品,并且依赖无限滚动来加载更多产品。
在下一部分中,您将学习如何处理无限滚动并从网站上的所有产品中提取数据。
PuPHPeteer 可以模拟许多 Web 交互,包括等待、点击等。您需要它们像人类用户一样与动态内容网页进行交互。模仿人类行为还将帮助您的脚本避开反机器人。
Puppeteer PHP 库支持的交互包括:
是时候学习如何从无限滚动演示页面抓取所有产品数据,然后模拟其他流行的交互了!
目标页面在第一次加载后仅包含十种产品,当用户到达视口末尾时会加载更多产品。
Puppeteer 没有自带滚动方法。您需要自定义 JavaScript 脚本来模拟滚动交互并加载新产品。
此 JavaScript 代码片段告诉浏览器以每次 0.5 秒的间隔向下滚动页面十次:
const scrolls = 10
let scrollCount = 0
// scroll down and then wait for 0.5s
const scrollInterval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
scrollCount++
if (scrollCount === scrolls) {
clearInterval(scrollInterval)
}
}, 500)
将上述脚本存储在一个多行字符串变量中。然后,使用它初始化 aJSFunction并将其提供给evalutate()的方法$page:
scraper.php
$scrolling_script = <<<EOD
const scrolls = 10
let scrollCount = 0
// scroll down and then wait for 0.5s
const scrollInterval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
scrollCount++
if (scrollCount === numScrolls) {
clearInterval(scrollInterval)
}
}, 500)
EOD;
$scrolling_js_function = (new JsFunction())->body($scrolling_script);
$page->evaluate($scrolling_js_function);
笔记 将该指令放在evaluate()节点选择逻辑之前,以确保 DOM 包含所有产品。
Puppeteer 现在将指示 Chromium 向下滚动页面。但是,检索新产品并呈现它们需要时间。等待滚动和数据加载操作以指令结束sleep()。停止脚本执行 10 秒:
sleep(10);
完整代码如下:
scraper.php
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
use Nesk\Rialto\Data\JsFunction;
// open a new Chromium browser window
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
// open a new page in the browser
$page = $browser->newPage();
// visit the target page
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
// JS script to simulate the infinite scrolling interaction
$scrolling_script = <<<EOD
const scrolls = 10
let scrollCount = 0
// scroll down and then wait for 0.5s
const scrollInterval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
scrollCount++
if (scrollCount === numScrolls) {
clearInterval(scrollInterval)
}
}, 500)
EOD;
// execute the JS script on the page
$scrolling_js_function = (new JsFunction())->body($scrolling_script);
$page->evaluate($scrolling_js_function);
// wait 10 seconds for the product nodes to load
sleep(10);
// where to store the scraped data
$products = [];
// select all product nodes on the page
$product_elements = $page->querySelectorAll('.post');
// iterate over the product elements and
// apply the scraping logic
foreach ($product_elements as $product_element) {
// select the name and price elements
$name_element = $product_element->querySelector('h4');
$price_element = $product_element->querySelector('h5');
// retrieve the data of interest
$name = $name_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
$price = $price_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
// create a new product object and add it to the list
$product = ['name' => $name, 'price' => $price];
$products[] = $product;
}
// open the output CSV file
$csvFilePath = 'products.csv';
$csvFile = fopen($csvFilePath, 'w');
// write the header row
$header = ['name', 'price'];
fputcsv($csvFile, $header);
// add each product to the CSV file
foreach ($products as $product) {
fputcsv($csvFile, $product);
}
// close the CSV file
fclose($csvFile);
// release the browser resources
$browser->close();
执行脚本来验证它是否检索了全部 60 种产品:
php src/scraper.php
该products.csv文件将包含超过 10 条记录:
任务完成!您刚刚使用 Puppeteer PHP 包从页面抓取了所有产品。
目前,PHP Puppeteer 脚本依赖于硬等待来让所有新产品出现在页面上。然而,这种方法是无效的,原因有二:
它会给您的抓取逻辑带来不稳定性,使其容易受到网络或浏览器速度变慢的影响。
它会在固定的秒数内停止执行,从而减慢脚本的速度。 您应该选择智能等待,例如等待特定元素出现在 DOM 上。这是构建强大、一致、可靠的浏览器自动化抓取工具的最佳实践。
PuPHPeteer 提供了waitForSelector()等待节点出现在页面上的功能。使用它最多可以等待 10 秒钟,直到第 60 个产品出现:
$page->waitForSelector('.post:nth-child(60)', ['timeout' => 10000]);
用此行替换该sleep()指令。脚本现在将等待滚动触发的 AJAX 调用后呈现产品节点。
最终的抓取逻辑如下:
<?php
require_once ('vendor/autoload.php');
use Nesk\Puphpeteer\Puppeteer;
use Nesk\Rialto\Data\JsFunction;
// open a new Chromium browser window
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'headless' => true, // set to false while developing locally
]);
// open a new page in the browser
$page = $browser->newPage();
// visit the target page
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/');
// JS script to simulate the infinite scrolling interaction
$scrolling_script = <<<EOD
const scrolls = 10
let scrollCount = 0
// scroll down and then wait for 0.5s
const scrollInterval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
scrollCount++
if (scrollCount === numScrolls) {
clearInterval(scrollInterval)
}
}, 500)
EOD;
// execute the JS script on the page
$scrolling_js_function = (new JsFunction())->body($scrolling_script);
$page->evaluate($scrolling_js_function);
// wait up to 10 seconds for the 60th product to be on the page
$page->waitForSelector('.post:nth-child(60)', ['timeout' => 10000]);
// where to store the scraped data
$products = [];
// select all product nodes on the page
$product_elements = $page->querySelectorAll('.post');
// iterate over the product elements and
// apply the scraping logic
foreach ($product_elements as $product_element) {
// select the name and price elements
$name_element = $product_element->querySelector('h4');
$price_element = $product_element->querySelector('h5');
// retrieve the data of interest
$name = $name_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
$price = $price_element->evaluate(JsFunction::createWithParameters(['node'])->body('return node.innerText;'));
// create a new product object and add it to the list
$product = ['name' => $name, 'price' => $price];
$products[] = $product;
}
// open the output CSV file
$csvFilePath = 'products.csv';
$csvFile = fopen($csvFilePath, 'w');
// write the header row
$header = ['name', 'price'];
fputcsv($csvFile, $header);
// add each product to the CSV file
foreach ($products as $product) {
fputcsv($csvFile, $product);
}
// close the CSV file
fclose($csvFile);
// release the browser resources
$browser->close();
运行它。您将以更快的速度获得与以前相同的结果。这是因为脚本现在只会等待适当的时间,从而减少了空闲时间。
默认情况下,该goto()方法等待页面load在浏览器中触发事件。要更改该行为,您可以将特殊选项数组传递给goto():
$page->goto('https://scrapingclub.com/exercise/list_infinite_scroll/', ['waitUntil' => 'load']);
可能的值包括waitUntil:
如果您想等待页面导航到新 URL 或重新加载,请使用该waitForNavigation()方法。当您的交互逻辑触发页面更改时,它很有用:
$page->waitForNavigation();
笔记 waitForNavigation()接受具有与之前相同属性的可选选项数组waitUntil。
问题是现代网页非常动态,因此很难判断页面何时完全加载。要处理更复杂的情况,请使用该waitForSelector()方法。它接受具有以下属性的可选数组:
$element->click();
此函数指示 Chromium 点击指定节点。浏览器将发送鼠标点击事件并调用onclick()回调。
当click()调用触发页面更改时(如下面的代码片段所示),您必须等待新页面加载。然后,在新的 DOM 结构上编写一些解析逻辑:
// select a product element and click it
$product_element = $page->querySelector('.post');
$product_element->click();
// wait for the new page to load
$page->waitForNavigation();
// you are now on the detail product page...
// new scraping logic...
// $page->querySelectorAll(...);
抓取文本数据并不是从网站获取有用信息的唯一方法。特定页面或 DOM 元素的屏幕截图通常很有用,例如,用于竞争对手研究或测试目的。
PHP Puppeteer 包含screenshot()截取当前视口屏幕截图的方法:
// take a screenshot of the current viewport
$page->screenshot(['path' => 'screenshot.png']);
它会screenshot.png在你的项目的根文件夹中生成一个文件。
screenshot()类似地,您可以在单个元素上调用该方法:
scraper.php
$product_elements[0]->screenshot(['path' => 'product.png']);
它将生成一个product.png包含所选元素的屏幕截图的文件。
干得好!现在您已成为 PHP 版 Puppeteer 中用户交互的大师了。
使用 PHP 中的 Puppeteer 进行抓取时避免被阻止 被反机器人解决方案拦截是使用 Puppeteer 进行网页抓取的最大挑战。保护系统能够判断传入的请求是由人类用户还是机器人(例如您的脚本)发出的。
为了避免阻塞,您必须使请求对目标服务器来说看起来更自然。实现该目标的两种有用技术是:
让我们从用户代理开始。通过 Chromium 标志自定义 PHP Puppeteer 中的用户代理--user-agent:
$custom_user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'args' => ['--user-agent=$custom_user_agent'],
// other options...
]);
在我们的网页抓取用户代理指南中了解有关此方法的更多信息。
设置代理遵循类似的模式,并取决于标志。从Free Proxy List--proxy-server等网站获取免费代理的 URL ,然后将其传递给 Chrmoium,如下所示:
$proxy_url = '233.67.4.11:6879';
$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
'args' => ['--proxy-server=$proxy_url'],
// other options...
]);
笔记 当您阅读本指南时,所选的代理服务器将不再有效。这是因为免费代理占用大量数据、寿命短且不可靠。请仅将它们用于学习目的,切勿将其用于生产。
结论 在本教程中,您学习了在 PHP 中控制 Chromium 的基础知识。您探索了 Puppeteer 的基础知识,然后深入研究了更高级的技术。您已成为 PHP 浏览器自动化专家!
集蜂云(beeize.com)是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。平台提供了海量任务调度、三方应用集成、数据存储、监控告警、运行日志查看等功能,能够提供稳定的数据采集环境。平台提供丰富的采集模板,简单配置就可以直接运行,快来试一下吧。