PHPPHP 浏览器自动化
MeteorCat对于一些要求比较高的数据采集流程, 会采用模拟正常浏览器处理方式来避免被风控, 这种技术就是 浏览器模拟/浏览器渲染型爬虫
核心是用真实浏览器内核执行 JS、渲染页面, 完全模拟真人访问行为来绕过风控
比较老牌的就是 Python 的 Selenium 处理, 但如果采用 PHP 做底层采集框架(接口服务)就感觉没必要引入额外的开发语言
而 PHP 之中第三方也是具有这方面组件库, 也就是这篇文章的主角 php-webdriver
利用 php-webdriver 可以实现模拟玩家浏览器行为来采集数据, 并且一步到位来暴露自身 API 服务接口功能
注: 这里采用 PHP 的 Composer 来做包管理功能
环境安装
首先需要知道模拟玩家浏览器行为是需要用到底层浏览器驱动的, 目前底层浏览器驱动有以下方案:
这部分比较常用的是 ChromeDriver 驱动, 需要在 LINUX 系统用命令行安装:
1 2 3 4 5 6 7 8
| sudo apt install -y chromium-chromedriver # 有的发行版是 chromium-driver
# 安装完成之后确认版本是否完成 chromedriver --version
# 查看安装路径, 一般都在 /usr/bin/chromedriver # 这个路径比较重要后面需要记住 whereis chromedriver
|
之后在自己依赖的 composer.json 项目之中引入 php-webdriver 依赖:
1 2
| # 声明项目依赖 php-webdriver composer require php-webdriver/webdriver
|
这样前置准备就处理好, 之后就是实现唤起浏览器的采集功能, 这部分需要参照官方文档来开发
功能实现
这部分可以先模拟人类点击 bing.com 网址并且跳转浏览, 首先这里先定义个 getChromeDriver 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| use Facebook\WebDriver\Chrome\ChromeDriver; use Facebook\WebDriver\Chrome\ChromeDriverService; use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver;
function getChromeDriver(): RemoteWebDriver { $desiredCapabilities = DesiredCapabilities::chrome();
$chromeOptions = new ChromeOptions(); $chromeOptions->addArguments([ '--headless=new', # 无界面模式 '--no-sandbox', # 解决沙箱权限问题 '--disable-gpu',# 禁用 GPU '--start-maximized', # 默认启动最大化 '--disable-blink-features=AutomationControlled', # 隐藏 navigator.webdriver 避免风控 # 模拟正常访问 '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', '--lang=zh-CN', '--ignore-certificate-errors' ]); $chromeOptions->setExperimentalOption('excludeSwitches', ['enable-automation']); $chromeOptions->setExperimentalOption('useAutomationExtension', false); $desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);
$chromeServicePort = 9515; $chromeService = new ChromeDriverService( "/usr/bin/chromedriver", $chromeServicePort, ["--port={$chromeServicePort}"] );
return ChromeDriver::start( $desiredCapabilities, $chromeService ); }
|
具体的配置项参照(需要工具访问): https://sites.google.com/chromium.org/driver/capabilities
这里罗列比较重要配置用于提供给 ChromeOptions 做参考
-
--headless=new: 2023之前的老版本采用 --headless 用于声明采用简化版 Chrome 内核处理
-
--no-sandbox: 关闭安全沙箱机制, 有的时候服务器环境中权限可能导致操作受限, 避免分配给 www-data 等系统用户导致响应错误
-
--disable-gpu: 服务器一般都不会专门配置显卡, 所以需要关闭显卡硬件渲染
-
窗口尺寸配置, 这个不是必须
--screen-info={1024x768}: 界面运行模式的窗口设置, 这里设置为 1024x768 窗口化处理
--window-size=1024,768: 无界面运行模式的窗口设置, 模拟成 1024x768 窗口避免被风控检测
--start-maximized: 设置窗口最大化
-
--user-agent=Mozilla/5.0...: 设置访问的请求客户端代理信息
-
--lang=zh-CN: 设置默认本地语言
-
--ignore-certificate-errors: 忽略 https 证书异常问题
-
--disable-blink-features=AutomationControlled: 访问浏览器不设置 navigator.webdriver 来说明自己是模拟启动
这里其实还有个代理访问的问题, 一般有些数据是基于外网来读取, 要额外配置 Socks5 代理访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
function getChromeDriver(): RemoteWebDriver { $desiredCapabilities = DesiredCapabilities::chrome();
$chromeOptions = new ChromeOptions(); $chromeOptions->addArguments([ '--proxy-server=socks5://127.0.0.1:1080', # Socks5 代理地址 '--proxy-bypass-list=localhost;127.0.0.1;192.168.*;10.*' # 跳过内网相关地址 ]);
}
|
这里的主要设置就是 --proxy-server 和 --proxy-bypass-list, 之后就是模拟正常人浏览网页处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| $driver = getChromeDriver(); try {
$driver->get('https://cn.bing.com/');
$driver->manage()->window()->maximize();
$driver->wait(10)->until( WebDriverExpectedCondition::presenceOfElementLocated( WebDriverBy::id('sb_form_q') ) );
$searchBox = $driver->findElement(WebDriverBy::id('sb_form_q')); $searchBox->sendKeys('PHP 模拟测试'); sleep(3);
$searchBox->sendKeys([WebDriverKeys::CONTROL, "a"]);
$searchBox->sendKeys(WebDriverKeys::BACKSPACE);
$searchBox->sendKeys("Selenium 模拟输入和按键");
$searchBox->click(); $searchBox->sendKeys(WebDriverKeys::ENTER);
$driver->wait(5)->until( WebDriverExpectedCondition::presenceOfElementLocated( WebDriverBy::id('b_content') ) );
sleep(5); } catch (\Exception $exception) { echo $exception->getMessage().PHP_EOL; } finally { $driver->quit(); }
|
注意: 写死 sleep 定时停止太过于机械化, 建议采用 sleep(mt_rand(2,4)) 来随机停止执行
这样运行下测试看看是否能够自动化启动, 如果想要看到具体流程可以先把 --headless=new 注释掉看看行为是否正常.
数据采集
现在行为模拟已经完成, 之后就是机械化的提取页面信息, 依靠 PHP 生态来保存成 JSON 文件或者写入数据库
我这边抓取凤凰网卫视的财经新闻信息(https://www.ifeng.com/)并将新闻保存到本地 JSON 文件
这里因为我集成的是 codeigniter4 框架, 需要创建自己的 Spark 命令
创建 Spark 命令: https://codeigniter.org.cn/user_guide/cli/cli_commands.html
所以这部分拉取数据功能都是写入下 App\Commands 命名空间之下, 具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| <?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use Facebook\WebDriver\Chrome\ChromeDriver; use Facebook\WebDriver\Chrome\ChromeDriverService; use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverExpectedCondition;
class LoadIFengNews extends BaseCommand {
protected $group = 'News'; protected $name = 'news:iFeng'; protected $description = 'Load iFeng News to json file.';
function getChromeDriver(): RemoteWebDriver { $desiredCapabilities = DesiredCapabilities::chrome();
$chromeOptions = new ChromeOptions(); $chromeOptions->addArguments([ '--headless=new', # 无界面模式 '--no-sandbox', # 解决沙箱权限问题 '--disable-gpu',# 禁用 GPU '--start-maximized', # 默认启动最大化 '--disable-blink-features=AutomationControlled', # 隐藏 navigator.webdriver 避免风控
# 模拟正常访问 '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', '--lang=zh-CN', '--ignore-certificate-errors' ]);
$chromeOptions->setExperimentalOption('excludeSwitches', ['enable-automation']); $chromeOptions->setExperimentalOption('useAutomationExtension', false); $desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);
$chromeServicePort = 9515; $chromeService = new ChromeDriverService( "/usr/bin/chromedriver", $chromeServicePort, ["--port={$chromeServicePort}"] );
return ChromeDriver::start( $desiredCapabilities, $chromeService ); }
public function run(array $params): void { $driver = $this->getChromeDriver();
try {
$driver->get("https://news.ifeng.com");
$driver->manage()->window()->maximize();
$driver->wait(10)->until( WebDriverExpectedCondition::presenceOfElementLocated( WebDriverBy::className('news-stream-basic-news-list') ) );
$elements = $driver->findElements(WebDriverBy::className('news-stream-basic-news-list')); CLI::write(CLI::color('news node total = ' . count($elements), 'green'));
$rows = [];
foreach ($elements as $id => $element) { $items = $element->findElements(WebDriverBy::className( 'news-stream-newsStream-news-item-has-image' )); CLI::write(CLI::color("news node-{$id} total = " . count($items), 'green'));
foreach ($items as $item) { $active = $item->findElement(WebDriverBy::className('news-stream-newsStream-news-item-title')); if (empty($active)) continue; $news = $active->findElement(WebDriverBy::tagName('a')); if (empty($news)) continue;
$title = $news->getAttribute('title'); $url = $news->getAttribute('href'); CLI::write(CLI::color("{$url} - {$title}", 'green'));
$rows[] = [ 'url' => $url, 'title' => $title, ]; } }
if (!empty($rows)) file_put_contents(WRITEPATH . 'uploads/news.json', json_encode($rows, JSON_UNESCAPED_UNICODE)); } catch (\Exception $e) { $this->logger->error($e->getMessage()); CLI::write(CLI::color("{$e->getMessage()}", 'red')); } finally { if ($driver instanceof ChromeDriver) { $driver->quit(); } }
} }
|
这里执行 php -f spark news:iFeng 就会开始模拟人类打开页面复制内容, 最后保存在 writable/uploads/news.json 之中
这就是依靠 PHP 实现无头浏览器采集数据的流程, 不过在目前主流的前后端分离的情况下都是直接拦截接口获取数据;
只有风控比较严格的情况才会需要用到模拟人类采集, 一般集中于购物网站的历史价格采集或者抢购物品的监控下单等.