各位同学,大家好!我是你们的“爬虫老司机”,今天不整虚的,咱们来聊聊一个在 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 请求就像是一个只看封面不看书内容的读者,显然不靠谱。
这时候,Puppeteer 和 Selenium 登场了。
- 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 也是可以的)。
架构设计:
- Worker (控制器): 一个 PHP 进程,运行在后台,不断从队列里拿任务。
- Browser Pool (浏览器池): Worker 启动时,生成 N 个 Puppeteer/Selenium 实例,存放在数组里。
- Task (任务): 一条数据,包含 URL、选择器等。
- 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,那就是绝配。
- Job 类: 把抓取任务封装成 Laravel Job。
- Worker 命令:
php artisan queue:work。
在你的 Job 类的 handle() 方法里,实例化 BrowserPool,执行抓取。这样,你的爬虫就可以在后台默默运行,不需要你一直开着终端。
第五章:实战演练 —— 爬取“无限滚动”的电商评论
这是最折磨人的场景之一。淘宝、京东、亚马逊,你往下拉,评论区哗啦啦地出来。
如果你只写一次脚本,那你只能抓到第一页的数据。
核心思路:
- PHP 发起请求,进入页面。
- 模拟滚动: 使用 JavaScript 改变页面的
scrollTop属性。 - 等待: 等待新内容加载(通常会有 Loading 遮罩层消失,或者新元素出现)。
- 判断: 滚动了 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),脚本崩了。
解药:
- 坚决不要频繁
launch和close。 - 使用上面的“浏览器池”模式,复用浏览器实例。
- 如果必须频繁关闭,一定要设置
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) 这种代码是最糟糕的。因为网络环境不同,有的网站快,有的慢。
解药:
- 强制等待: 坚决少用。
- 显式等待: 使用
waitForSelector或waitForNavigation。告诉 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,就是你手中的钻井平台。
记住几个核心点:
- 不要写死: 网页会变,代码也要变。
- 不要乱跑: 模拟人的行为,控制频率,带上代理。
- 不要硬等: 用
waitFor代替sleep。 - 复用资源: 管理好你的浏览器实例,别让内存爆了。
好了,今天的讲座就到这里。大家回去记得自己动手敲代码,爬取几个大网站的数据练练手。如果遇到什么问题,记得多看文档,多看报错,多看 Chrome DevTools。
愿你们的爬虫,稳如老狗,快如闪电!下课!