PHP 与 Puppeteer/Selenium 协同:在无头浏览器环境下实现对复杂动态网页的数据抓取

各位同学,大家好!我是你们的“爬虫老司机”,今天不整虚的,咱们来聊聊一个在 PHP 开发圈子里既“性感”又让人“头疼”的话题——如何让 PHP 这位老将,去驾驭 Puppeteer 和 Selenium 这两位现代 Web 浏览器界的“变形金刚”

如果你对 PHP 的印象还停留在 file_get_contents 抓取静态网页的“石器时代”,那你今天算是来对地方了。现在的网页,那是相当的“作妖”,前端框架换得比衣服还快,AJAX 请求满天飞,动态加载像变魔术。你用 PHP 的老办法去抓,就像是拿着鱼叉去抓一条游得飞快的泥鳅——你连泥鳅的尾巴都摸不到,更别提红烧了。

所以,今天这场讲座的主题就是:PHP 与 Puppeteer/Selenium 协同:在无头浏览器环境下实现对复杂动态网页的数据抓取

准备好了吗?系好安全带,我们要起飞了。


第一章:为什么我们要折腾“无头浏览器”?

首先,咱们得搞清楚现状。

在 2010 年,抓取网页就像是在公园散步,网页是静态的 HTML,你往那一扔 file_get_contents,数据就到手了,简单、粗暴、有效。那时候的网页就像一本印好的书,虽然偶尔有个链接需要跳转,但整体结构是稳定的。

到了 2024 年呢?现在的网页简直就是个“装修工地”。

  • SPA(单页应用): 整个网站就是一个巨大的 JavaScript 文件,点一下按钮,页面不刷新,数据通过 AJAX 从后端吐给你。你的 PHP 爬虫抓下来的是骨架,肉还在服务器里没加载出来。
  • 无限滚动: 你往下拉,它就“哗啦啦”地加载数据,不把屏幕拉到底,你永远看不到最后 10 条数据。
  • 登录验证: 没点“登录”,你连个标题都看不清。
  • 动态渲染: 也就是那个让人头秃的 Canvas、SVG 动画,或者图表库(ECharts、Highcharts),它们得等 JavaScript 把画布填满,你才能抓到里面的数字。

这时候,传统的 HTTP 请求就像是一个只看封面不看书内容的读者,显然不靠谱。

这时候,PuppeteerSelenium 登场了。

  • Puppeteer:Google 出品,它是 Node.js 的亲儿子,能完美控制 Chrome 浏览器,速度快,对现代 Web 支持最好,简直是“老司机”的首选。
  • Selenium:老牌劲旅,Java/C# 的祖师爷,但 PHP 也有对应的 WebDriver 库(如 facebook/webdriver),它的兼容性极强,能跑在各种奇怪的浏览器上。

而我们的 PHP,作为后端的老大哥,它的强项是逻辑处理、数据存储和队列调度。所以,“PHP 负责调度,Puppeteer/Selenium 负责干活”,这种“父子兵”的配合模式才是王道。


第二章:PHP 搭配 Puppeteer —— 优雅的“空降兵”

首先我们来看看 Puppeteer。因为它是 Google 出的,对现代 Web 技术的支持那是没得说。但在 PHP 里直接用 Puppeteer 有点麻烦,因为 Puppeteer 是 Node.js 写的。

不过别怕,社区里有几个优秀的 PHP 包,最出名的就是 spatie/puppeteer。这个包就像是 PHP 和 Node.js 之间的翻译官。

2.1 基础架构:安装与启动

假设你用的是 Composer,安装这个包是必须的:

composer require spatie/puppeteer

接下来,咱们写个最简单的脚本,打开百度,看看它长啥样。

<?php

require __DIR__ . '/vendor/autoload.php';

use SpatiePuppeteerPuppeteer;

// 实例化 Puppeteer
$puppeteer = new Puppeteer();

// 启动一个 Chrome 浏览器实例
// 这里的 pathToChrome 是指你的 chrome.exe 的绝对路径
// 注意:没有图形界面的 Linux 服务器上,要加上 '--no-sandbox' 参数
$browser = $puppeteer->launch([
    'headless' => true, // 无头模式,不弹窗,安静地干活
    'args' => ['--no-sandbox', '--disable-setuid-sandbox'] // 服务器上必备
]);

// 创建一个新页面
$page = $browser->newPage();

// 访问网址
$page->navigate('https://www.baidu.com')->waitForNavigation();

// 截个图看看,验证一下是否成功
$page->screenshot(['path' => 'baidu.png']);

echo "截图已保存!";

// 关闭浏览器
$browser->close();

看到没?这代码写出来,PHP 就像是在云端控制了一台电脑,指挥它打开浏览器,输入网址,甚至截图。这感觉是不是很爽?

2.2 进阶操作:处理动态加载与点击

现在大部分网站都需要登录,或者需要点击“加载更多”。

假设有个网站 https://example.com/news,它有一个“加载更多”的按钮,点一下,下面就会多出 10 条新闻。传统的 CSS 选择器抓取是抓不到这 10 条的,因为它们压根就不存在 DOM 树里。

这时候,PHP 要指挥 Puppeteer 去点击。

<?php

use SpatiePuppeteerPuppeteer;

$puppeteer = new Puppeteer();
$browser = $puppeteer->launch([
    'headless' => true,
    'args' => ['--no-sandbox', '--disable-setuid-sandbox']
]);

$page = $browser->newPage();
$page->navigate('https://example.com/news');

// 等待“加载更多”按钮出现
// 这一步至关重要,防止脚本太快点不到按钮就往下跑了
$page->waitForSelector('.load-more-btn');

// 点击按钮
$page->click('.load-more-btn');

// 等待新内容加载完成
// 现在的网站通常有个加载动画或者等待几秒
$page->waitForSelector('.news-item', ['timeout' => 5000]);

// 现在的 DOM 树里应该有数据了,我们可以抓取了
$articles = $page->evaluate('document.querySelectorAll(".news-item").map(n => n.textContent)');

// $articles 是一个数组,包含了所有文本内容
print_r($articles);

$browser->close();

这里用到了一个神技 evaluate。它的作用是直接把 JavaScript 代码扔进浏览器里跑。这就是 PHP 和 Puppeteer 协同的精髓:PHP 搭台(写逻辑),JavaScript 唱戏(操作 DOM)

2.3 处理登录 Cookie

很多网站不登录就看不到内容。我们可以用 Puppeteer 自动登录,然后保存 Cookie,下次直接用 Cookie 访问。

// 1. 访问登录页
$page->navigate('https://example.com/login');
$page->type('#username', 'your_username');
$page->type('#password', 'your_password');
$page->click('button[type=submit]');

// 2. 等待跳转或登录成功
$page->waitForNavigation();

// 3. 导出 Cookie
$cookies = $page->getCookies();

// 4. 下次使用这些 Cookie 访问
// 获取浏览器实例
$browser = $puppeteer->launch([...]);
$page = $browser->newPage();

// 设置 Cookie
foreach ($cookies as $cookie) {
    $page->setCookie($cookie);
}

$page->navigate('https://example.com/dashboard');

这一招可以帮你绕过很多需要登录的动态数据。


第三章:PHP 搭配 Selenium —— 老当益壮的“老黄牛”

如果说 Puppeteer 是年轻力壮的田径冠军,那 Selenium 就是那个经验丰富、能跑马拉松、还能骑自行车的“老黄牛”。

Selenium 的核心是 WebDriver。PHP 里有 facebook/webdriver 这个库。

3.1 安装与配置

你需要下载对应浏览器的 WebDriver(比如 ChromeDriver),并配置环境变量,或者在代码里指定路径。

安装库:

composer require facebook/webdriver

3.2 Selenium 基础代码

Selenium 的风格和 Puppeteer 略有不同,它更像是在“遥控器”上操作。

<?php

require __DIR__ . '/vendor/autoload.php';

use FacebookWebDriverRemoteRemoteWebDriver;
use FacebookWebDriverWebDriverBy;
use FacebookWebDriverWebDriverExpectedCondition;

// 1. 连接到 Selenium Server (这里用的是默认的 4444 端口)
// 注意:你需要先启动 Selenium Server: java -jar selenium-server-standalone-4.x.x.jar
// 或者使用 Docker 启动 selenium/standalone-chrome
$host = 'http://localhost:4444';
$capabilities = [
    FacebookWebDriverRemoteDesiredCapabilities::CHROME
];
$driver = RemoteWebDriver::create($host, $capabilities);

// 2. 访问页面
$driver->get('https://www.google.com');

// 3. 等待元素出现
// Selenium 的等待机制比 Puppeteer 稍微繁琐一点,但功能强大
$driver->wait(30, 1000)->until(
    WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::name('q'))
);

// 4. 输入并搜索
$driver->findElement(WebDriverBy::name('q'))->sendKeys('PHP爬虫');
$driver->findElement(WebDriverBy::name('q'))->submit();

// 5. 等待搜索结果加载
$driver->wait(20, 1000)->until(
    WebDriverExpectedCondition::titleContains('PHP爬虫 - Google 搜索')
);

// 6. 获取页面源码
$html = $driver->getPageSource();

// 7. 也可以用 XPath 抓取数据
$element = $driver->findElement(WebDriverBy::xpath('//h3[@class="LC20lb MBeuO DKV0Md"]'));
echo $element->getText();

// 8. 关闭
$driver->quit();

Selenium 的优势在于它的稳定性。比如处理一些非常古老的网站,或者不支持某些高级 Chrome API 的网站,Selenium 的兼容性往往更好。


第四章:协同作战 —— 构建高并发爬虫系统

光会用还不行,真正的挑战在于高并发稳定性。你不能在脚本里写死 launch -> navigate -> close,那样你的服务器内存会像吹气球一样爆炸,CPU 也会被吃光。

我们需要建立一个资源池(Pool)。

4.1 策略:复用浏览器实例

每次打开和关闭浏览器都要几秒钟,这是巨大的浪费。我们需要 PHP 启动几个浏览器进程,把它们“养”起来,任务来了就分派给它们,任务干完了就回收利用。

这里我们结合 Laravel 的队列来演示(当然,纯 PHP 也是可以的)。

架构设计:

  1. Worker (控制器): 一个 PHP 进程,运行在后台,不断从队列里拿任务。
  2. Browser Pool (浏览器池): Worker 启动时,生成 N 个 Puppeteer/Selenium 实例,存放在数组里。
  3. Task (任务): 一条数据,包含 URL、选择器等。
  4. Loop (循环): 拿到任务 -> 指派给空闲浏览器 -> 执行抓取 -> 返回结果 -> 把浏览器放回池中。

代码示例(简化版逻辑):

<?php

class BrowserPool
{
    protected $puppeteer;
    protected $pool = []; // 存放浏览器实例
    protected $maxSize = 5; // 池子大小,根据你的服务器内存定

    public function __construct()
    {
        $this->puppeteer = new Puppeteer();
    }

    // 获取一个浏览器实例
    public function acquire()
    {
        // 如果池子满了,就等待(这里简化为同步等待,生产环境可以用信号量)
        while (count($this->pool) >= $this->maxSize) {
            sleep(1);
        }

        // 如果池子有空的,拿出来
        if (!empty($this->pool)) {
            return array_shift($this->pool);
        }

        // 池子空了,新建一个
        return $this->puppeteer->launch([
            'headless' => true,
            'args' => ['--no-sandbox', '--disable-setuid-sandbox']
        ]);
    }

    // 归还浏览器实例
    public function release($browser)
    {
        $this->pool[] = $browser;
    }

    // 执行抓取任务
    public function scrape($url, $selector)
    {
        $browser = $this->acquire();
        try {
            $page = $browser->newPage();
            $page->navigate($url)->waitForNavigation();

            // 这里执行具体的抓取逻辑
            $data = $page->evaluate("document.querySelector('$selector').textContent");

            return $data;
        } catch (Exception $e) {
            // 出错也要归还
            return false;
        } finally {
            // 关闭页面,保持浏览器实例存活
            // $page->close(); // 如果你想重用浏览器,这一步千万别写,或者只关闭 page
            $this->release($browser);
        }
    }
}

4.2 队列系统的配合

上面的 BrowserPool 如果配合 Laravel Queue,那就是绝配。

  1. Job 类: 把抓取任务封装成 Laravel Job。
  2. Worker 命令: php artisan queue:work

在你的 Job 类的 handle() 方法里,实例化 BrowserPool,执行抓取。这样,你的爬虫就可以在后台默默运行,不需要你一直开着终端。


第五章:实战演练 —— 爬取“无限滚动”的电商评论

这是最折磨人的场景之一。淘宝、京东、亚马逊,你往下拉,评论区哗啦啦地出来。

如果你只写一次脚本,那你只能抓到第一页的数据。

核心思路:

  1. PHP 发起请求,进入页面。
  2. 模拟滚动: 使用 JavaScript 改变页面的 scrollTop 属性。
  3. 等待: 等待新内容加载(通常会有 Loading 遮罩层消失,或者新元素出现)。
  4. 判断: 滚动了 5 次,或者高度变化很小了,认为到底了。
  5. 抓取: 这时候的 DOM 树已经满了,随便抓。

代码实现:

public function scrapeInfiniteScroll($url)
{
    $browser = $this->puppeteer->launch(['headless' => true]);
    $page = $browser->newPage();

    $page->navigate($url);

    // 等待评论区出现
    $page->waitForSelector('.review-content');

    $lastHeight = 0;
    $scrollCount = 0;
    $maxScroll = 5; // 最多滚5次,防止死循环
    $allData = [];

    while ($scrollCount < $maxScroll) {
        // 1. 执行滚动到底部
        $page->evaluate('window.scrollTo(0, document.body.scrollHeight)');

        // 2. 等待一下,让 AJAX 把数据吐出来
        sleep(1); // 这里的 sleep 可以换成更智能的 wait
        // $page->waitForSelector('.review-item', ['visible' => true]);

        // 3. 获取新的高度
        $newHeight = $page->evaluate('document.body.scrollHeight');

        // 4. 判断高度有没有变化,如果没有变化,说明到底了
        if ($newHeight == $lastHeight) {
            break;
        }

        $lastHeight = $newHeight;
        $scrollCount++;

        // 每3次抓取一次数据,避免抓太慢
        if ($scrollCount % 3 == 0) {
            $items = $page->evaluate('document.querySelectorAll(".review-item").map(item => ({
                text: item.querySelector(".review-text").textContent,
                star: item.querySelector(".star").getAttribute("data-value")
            }))');
            $allData = array_merge($allData, $items);
        }
    }

    $browser->close();
    return $allData;
}

这段代码展示了PHP 与 JS 的深度协作。PHP 控制着节奏(循环、计数),JS 在浏览器里负责改变视图。这就是所谓的“双核驱动”。


第六章:避坑指南 —— 资深专家的血泪经验

写爬虫不是请客吃饭,遇到坑是常态。作为“过来人”,我总结了一些大家容易忽略的坑。

1. 阿米巴虫(内存泄漏)

这是 PHP + Puppeteer 最常见的问题。如果你在代码里写了 launch() -> navigate() -> close(),这在脚本结束前是没问题的。但在并发环境下,或者不关闭浏览器的情况下,内存会一直涨。

症状: 爬了 1000 条数据,内存从 100MB 涨到了 2GB,最后直接 OOM(Out Of Memory),脚本崩了。

解药:

  • 坚决不要频繁 launchclose
  • 使用上面的“浏览器池”模式,复用浏览器实例。
  • 如果必须频繁关闭,一定要设置 puppeteer -> end()
  • 定期重启 PHP-FPM 进程。

2. IP 被封

你写了个好爬虫,每天抓 10 万条数据。网站一看:“好家伙,这 IP 一分钟抓了一千次”。结果:封 IP。

解药:

  • 代理 IP 池: 必须要搞。买一批 HTTP 代理,或者用免费的(不推荐,不稳定)。
  • 随机化: 在请求头里加 User-Agent,并且随机切换。不要每次都发 Mozilla/5.0 (Windows NT 10.0...),换个 iPhone 的 UA 试试。
  • 限速: 在 Worker 和 URL 之间加个延时(比如每个 URL 间隔 2 秒)。

3. 等待策略的玄学

sleep(5) 这种代码是最糟糕的。因为网络环境不同,有的网站快,有的慢。

解药:

  • 强制等待: 坚决少用。
  • 显式等待: 使用 waitForSelectorwaitForNavigation。告诉 Puppeteer:“我等你,直到这个按钮出现了,或者加载条消失了,我就干活”。
  • 元素可见性: 有时候元素加载出来了但被 display: none 遮住了,waitForSelector 是不行的,要用 waitForSelector(..., ['visible' => true])

4. CAPTCHA(验证码)与 2FA

当你抓取量稍微大一点,网站就会开始折腾你。弹出滑块验证码,或者让你手机收验证码。

解药:

  • 人工打码平台: 比如打码狗、超级鹰。在 PHP 里写个接口,把验证码图片传过去,后台把解密后的 token 返回给你,你带着 token 继续跑。
  • 指纹识别: 这就是黑科技了。模拟浏览器的指纹,像 Canvas 指纹、WebGL 指纹、插件指纹。这个比较复杂,涉及到了反反爬虫的领域。

第七章:进阶话题 —— Puppeteer 与 Selenium 的选择

回到最初的问题,到底选谁?

  • 选 Puppeteer,如果:

    • 你抓取的网站是现代 Web,基于 React、Vue、Angular。
    • 你追求速度和性能,Chrome 是你的朋友。
    • 你不想搞复杂的 Java 环境,只想用 PHP 和 Node.js 搭配。
    • 优势: 速度快,API 现代化,调试方便。
  • 选 Selenium,如果:

    • 你需要兼容一些非常老旧的 IE 或 Safari 浏览器(虽然现在很少见了)。
    • 你的团队更熟悉 Java/Python 的 Selenium 生态,想复用代码。
    • 你需要处理复杂的 Selenium 交互(如拖拽、多窗口切换,虽然 Puppeteer 也能做,但 Selenium 文档更全)。
    • 优势: 跨平台广,生态成熟。

我的建议: 绝大多数 PHP 爬虫项目,用 Puppeteer 的 PHP 封装足矣。它轻量、现代。


第八章:调试技巧 —— 看到你的浏览器在动

在写代码时,不要一开始就开无头模式。无头模式就像蒙着眼睛开车,出了问题你都不知道发生了什么。

如何调试?

headless 设为 false

$browser = $puppeteer->launch([
    'headless' => false, // 弹出一个真实的 Chrome 窗口
    'args' => ['--start-maximized']
]);

然后,你就可以像人类一样操作浏览器了。你可以把断点打在 PHP 代码里(如果使用 XDebug),当 PHP 运行到断点时,浏览器会暂停,你可以右键检查元素,看看那个 document.querySelector 到底能不能选到。

这是排查抓取失败最快的方法。


第九章:未来的路 —— AI 与爬虫的结合

最后,聊聊趋势。

现在的数据抓取,不再是简单的 HTML 解析,而是结构化数据提取。以前我们用正则表达式,现在用 AI。

比如,你抓下来一段商品评论:“这家衣服质量很好,就是有点贵,发货太慢了”。

用正则,你可能得写一堆 .*?
用 AI,你可以把这段文字喂给一个 LLM(大语言模型),让它直接返回 JSON:{"quality": "好", "price": "贵", "speed": "慢"}

这就是 PHP + Puppeteer + AI 的终极形态。PHP 负责去网页上“薅羊毛”,AI 负责把“羊毛”洗干净,整理成你想要的数据。


总结

各位同学,今天我们聊了很多。从 PHP 的落寞讲到现代 Web 的复杂,从 Puppeteer 的优雅讲到 Selenium 的稳健,从基础的抓取讲到高并发池的构建,再到反爬虫的博弈。

数据是新时代的石油,而 PHP 加上 Puppeteer/Selenium,就是你手中的钻井平台。

记住几个核心点:

  1. 不要写死: 网页会变,代码也要变。
  2. 不要乱跑: 模拟人的行为,控制频率,带上代理。
  3. 不要硬等:waitFor 代替 sleep
  4. 复用资源: 管理好你的浏览器实例,别让内存爆了。

好了,今天的讲座就到这里。大家回去记得自己动手敲代码,爬取几个大网站的数据练练手。如果遇到什么问题,记得多看文档,多看报错,多看 Chrome DevTools。

愿你们的爬虫,稳如老狗,快如闪电!下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注