gsouf/chromium

此包已被弃用且不再维护。作者建议使用 chrome-php/chrome 包。

使用 PHP 从 PHP 脚本中检测无头 chrome/chromium 实例

v1.11.0 2024-03-17 22:51 UTC

README

Chrome PHP

Latest Stable Version License

此库允许您从 PHP 中开始使用 chrome/chromium 的无头模式。

可以同步和异步使用!

特性

  • 从 PHP 打开 Chrome 或 Chromium 浏览器
  • 创建页面并导航到页面
  • 截图
  • 在页面上评估 JavaScript
  • 制作 PDF
  • 模拟鼠标
  • 模拟键盘
  • 始终对 IDE 友好

快乐的浏览!

要求

需要 PHP 7.4-8.3 和 Chrome/Chromium 65+ 可执行文件。

请注意,此库仅在 Linux 上进行了测试,但与 macOS 和 Windows 兼容。

安装

此库可以使用 Composer 安装,并在 Packagist 上的 chrome-php/chrome 下可用。

$ composer require chrome-php/chrome

用法

它使用简单易懂的 API 来启动 Chrome,打开页面,截图,爬取网站...以及您作为人类可以做的几乎所有事情。

use HeadlessChromium\BrowserFactory;

$browserFactory = new BrowserFactory();

// starts headless Chrome
$browser = $browserFactory->createBrowser();

try {
    // creates a new page and navigate to an URL
    $page = $browser->createPage();
    $page->navigate('http://example.com')->waitForNavigation();

    // get page title
    $pageTitle = $page->evaluate('document.title')->getReturnValue();

    // screenshot - Say "Cheese"! 😄
    $page->screenshot()->saveToFile('/foo/bar.png');

    // pdf
    $page->pdf(['printBackground' => false])->saveToFile('/foo/bar.pdf');
} finally {
    // bye
    $browser->close();
}

使用不同的 Chrome 可执行文件

启动时,工厂会查找环境变量 "CHROME_PATH" 以用作 Chrome 可执行文件。如果未找到变量,它将尝试根据您的操作系统猜测正确的可执行文件路径,或者使用 "chrome" 作为默认值。

您还可以在创建新对象时显式设置任何可执行文件。例如 "chromium-browser"

use HeadlessChromium\BrowserFactory;

// replace default 'chrome' with 'chromium-browser'
$browserFactory = new BrowserFactory('chromium-browser');

调试

以下示例禁用了无头模式以简化调试

use HeadlessChromium\BrowserFactory;

$browserFactory = new BrowserFactory();

$browser = $browserFactory->createBrowser([
    'headless' => false, // disable headless mode
]);

其他调试选项

[
    'connectionDelay' => 0.8,            // add 0.8 second of delay between each instruction sent to Chrome,
    'debugLogger'     => 'php://stdout', // will enable verbose mode
]

关于 debugLogger:这可以是任何资源字符串、资源或实现 Psr\Log 的对象(例如 monologapix/log)。

API

浏览器工厂

直接在 createBrowser 方法中设置的选项仅用于单个浏览器创建。默认选项将被忽略。

use HeadlessChromium\BrowserFactory;

$browserFactory = new BrowserFactory();
$browser = $browserFactory->createBrowser([
    'windowSize'   => [1920, 1000],
    'enableImages' => false,
]);

// this browser will be created without any options
$browser2 = $browserFactory->createBrowser();

使用 setOptionsaddOptions 方法设置的选项将持久存在。

$browserFactory->setOptions([
    'windowSize' => [1920, 1000],
]);

// both browser will have the same 'windowSize' option
$browser1 = $browserFactory->createBrowser();
$browser2 = $browserFactory->createBrowser();

$browserFactory->addOptions(['enableImages' => false]);

// this browser will have both the 'windowSize' and 'enableImages' options
$browser3 = $browserFactory->createBrowser();

$browserFactory->addOptions(['enableImages' => true]);

// this browser will have the previous 'windowSize', but 'enableImages' will be true
$browser4 = $browserFactory->createBrowser();

可用选项

以下是浏览器工厂的可用选项

选项名称 默认值 描述
connectionDelay 0 调试目的之间应用的每个操作的延迟
customFlags 传递给命令行的标志数组。例如:['--option1', '--option2=someValue']
debugLogger 一个字符串(例如 "php://stdout"),或资源,或实现 Psr\Log 的 PSR-3 日志记录实例以打印调试消息
disableNotifications 禁用浏览器通知
enableImages 切换加载图片
envVariables 传递给进程的环境变量数组(例如 DISPLAY 变量)
headers 自定义 HTTP 标头数组
headless 启用或禁用无头模式
ignoreCertificateErrors 设置 Chrome 忽略 SSL 错误
keepAlive 设置为 true 以在脚本终止时保持 Chrome 实例活动状态
noSandbox 启用非沙盒模式,适用于在Docker容器中运行
noProxyServer 不使用代理服务器,始终建立直接连接。覆盖其他代理设置。
proxyBypassList 指定要绕过代理设置并使用直接连接的主机列表
proxyServer 要使用的代理服务器。用法:127.0.0.1:8080(使用凭据进行授权无效)
sendSyncDefaultTimeout 5000 发送同步消息的默认超时时间(毫秒)
startupTimeout 30 等待Chrome启动的最大时间(秒)
userAgent 用于整个浏览器的用户代理(请参阅页面API以获取替代方案)
userDataDir Chrome用户数据目录(默认:生成一个新的临时空目录)
userCrashDumpsDir crashpad应将转储存储在该目录中(将自动启用崩溃报告器)
windowSize 窗口大小。用法:$width, $height - 参见Page::setViewport
excludedSwitches 要从中删除的Chrome标志数组(例如 --enable-automation)

持久浏览器

此示例显示了如何为多个脚本共享单个Chrome实例。

首次启动脚本时,我们使用浏览器工厂来启动Chrome,之后我们将连接到该浏览器的uri保存到文件系统。

下一次调用脚本时,将从这个文件中读取uri以连接到Chrome实例,而不是创建一个新的实例。如果Chrome已关闭或崩溃,将再次启动新实例。

use \HeadlessChromium\BrowserFactory;
use \HeadlessChromium\Exception\BrowserConnectionFailed;

// path to the file to store websocket's uri
$socket = \file_get_contents('/tmp/chrome-php-demo-socket');

try {
    $browser = BrowserFactory::connectToBrowser($socket);
} catch (BrowserConnectionFailed $e) {
    // The browser was probably closed, start it again
    $factory = new BrowserFactory();
    $browser = $factory->createBrowser([
        'keepAlive' => true,
    ]);

    // save the uri to be able to connect again to browser
    \file_put_contents($socketFile, $browser->getSocketUri(), LOCK_EX);
}

浏览器API

创建一个新的页面(选项卡)

$page = $browser->createPage();

获取打开的页面(选项卡)

$pages = $browser->getPages();

关闭浏览器

$browser->close();

设置在浏览器创建的每个页面导航之前要评估的脚本

$browser->setPagePreScript('// Simulate navigator permissions;
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
    parameters.name === 'notifications' ?
        Promise.resolve({ state: Notification.permission }) :
        originalQuery(parameters)
);');

页面API

导航到URL

// navigate
$navigation = $page->navigate('http://example.com');

// wait for the page to be loaded
$navigation->waitForNavigation();

当使用$navigation->waitForNavigation()时,您将等待30秒,直到页面事件“loaded”被触发。您可以更改超时或监听的事件

use HeadlessChromium\Page;

// wait 10secs for the event "DOMContentLoaded" to be triggered
$navigation->waitForNavigation(Page::DOM_CONTENT_LOADED, 10000);

可用事件(按触发顺序排列)

  • Page::DOM_CONTENT_LOADED:dom已完全加载
  • Page::FIRST_CONTENTFUL_PAINT:当第一个非白色内容元素在屏幕上绘制时触发
  • Page::FIRST_IMAGE_PAINT:当第一个图像在屏幕上绘制时触发
  • Page::FIRST_MEANINGFUL_PAINT:当页面的主要内容对用户可见时触发
  • Page::FIRST_PAINT:当屏幕上的任何像素被绘制时触发,包括浏览器的默认背景颜色
  • Page::INIT:DevTools协议的连接初始化
  • Page::INTERACTIVE_TIME:脚本已加载完成,并且主线程不再被渲染或其他任务阻塞
  • Page::LOAD:(默认)页面和所有资源都已加载
  • Page::NETWORK_IDLE:页面已加载,至少500ms没有发生网络活动

当您想要等待页面导航时,可能会出现两个主要问题。首先,页面加载时间太长,其次,您等待加载的页面已被替换。好消息是,您可以使用古老的try-catch来处理这些问题

use HeadlessChromium\Exception\OperationTimedOut;
use HeadlessChromium\Exception\NavigationExpired;

try {
    $navigation->waitForNavigation()
} catch (OperationTimedOut $e) {
    // too long to load
} catch (NavigationExpired $e) {
    // An other page was loaded
}

在页面上评估脚本

一旦页面完成导航,您就可以在这个页面上评估任意脚本

// navigate
$navigation = $page->navigate('http://example.com');

// wait for the page to be loaded
$navigation->waitForNavigation();

// evaluate script in the browser
$evaluation = $page->evaluate('document.documentElement.innerHTML');

// wait for the value to return and get it
$value = $evaluation->getReturnValue();

有时您评估的脚本会点击链接或提交表单,在这种情况下,页面将重新加载,并且您将想要等待新页面的重新加载。

您可以通过使用$page->evaluate('some js that will reload the page')->waitForPageReload()来实现这一点。一个示例可以在form-submit.php中找到

调用函数

这是evaluate的替代方案,它允许在页面上下文中使用给定的参数调用给定的函数

$evaluation = $page->callFunction(
    "function(a, b) {\n    window.foo = a + b;\n}",
    [1, 2]
);

$value = $evaluation->getReturnValue();

添加脚本标签

如果您想向页面添加jQuery(或其他任何东西),这很有用

$page->addScriptTag([
    'content' => file_get_contents('path/to/jquery.js')
])->waitForResponse();

$page->evaluate('$(".my.element").html()');

您还可以使用URL来填充src属性

$page->addScriptTag([
    'url' => 'https://code.jqueryjs.cn/jquery-3.3.1.min.js'
])->waitForResponse();

$page->evaluate('$(".my.element").html()');

设置页面HTML

您可以使用setHtml方法手动将HTML注入到页面。

// Basic
$page->setHtml('<p>text</p>');

// Specific timeout & event
$page->setHtml('<p>text</p>', 10000, Page::NETWORK_IDLE);

当页面的HTML更新时,我们将等待页面卸载。您可以通过两个可选参数指定等待时间以及等待哪个事件。默认为3000毫秒和“load”事件。

请注意,此方法不会追加到当前页面的HTML,而是完全替换它。

获取页面HTML

您可以使用getHtml方法将页面HTML作为字符串获取。

$html = $page->getHtml();

在页面导航时添加脚本

$page->addPreScript('// Simulate navigator permissions;
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
    parameters.name === 'notifications' ?
        Promise.resolve({ state: Notification.permission }) :
        originalQuery(parameters)
);');

如果您的脚本需要在运行前DOM完全填充,可以使用“onLoad”选项。

$page->addPreScript($script, ['onLoad' => true]);

设置视口大小

此功能允许更改当前页面视口(模拟)的大小,而不会影响浏览器中所有页面的尺寸(另请参阅BrowserFactory::createBrowser"windowSize"选项)。

$width = 600;
$height = 300;
$page->setViewport($width, $height)
    ->await(); // wait for the operation to complete

制作截图

// navigate
$navigation = $page->navigate('http://example.com');

// wait for the page to be loaded
$navigation->waitForNavigation();

// take a screenshot
$screenshot = $page->screenshot([
    'format'  => 'jpeg',  // default to 'png' - possible values: 'png', 'jpeg', 'webp'
    'quality' => 80,      // only when format is 'jpeg' or 'webp' - default 100
    'optimizeForSpeed' => true // default to 'false' - Optimize image encoding for speed, not for resulting size
]);

// save the screenshot
$screenshot->saveToFile('/some/place/file.jpg');

截图页面上的某个区域

您可以使用“clip”选项选择页面上的截图区域。

use HeadlessChromium\Clip;

// navigate
$navigation = $page->navigate('http://example.com');

// wait for the page to be loaded
$navigation->waitForNavigation();

// create a rectangle by specifying to left corner coordinates + width and height
$x = 10;
$y = 10;
$width = 100;
$height = 100;
$clip = new Clip($x, $y, $width, $height);

// take the screenshot (in memory binaries)
$screenshot = $page->screenshot([
    'clip'  => $clip,
]);

// save the screenshot
$screenshot->saveToFile('/some/place/file.jpg');

全页截图

您也可以使用$page->getFullPageClip和属性captureBeyondViewport = true来对全页布局(而不仅仅是视口)进行截图。

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

// wait for the page to be loaded
$navigation->waitForNavigation();

$screenshot = $page->screenshot([
    'captureBeyondViewport' => true,
    'clip' => $page->getFullPageClip(),
    'format' => 'jpeg', // default to 'png' - possible values: 'png', 'jpeg', 'webp'
]);

// save the screenshot
$screenshot->saveToFile('/some/place/file.jpg');

打印为PDF

// navigate
$navigation = $page->navigate('http://example.com');

// wait for the page to be loaded
$navigation->waitForNavigation();

$options = [
    'landscape'           => true,             // default to false
    'printBackground'     => true,             // default to false
    'displayHeaderFooter' => true,             // default to false
    'preferCSSPageSize'   => true,             // default to false (reads parameters directly from @page)
    'marginTop'           => 0.0,              // defaults to ~0.4 (must be a float, value in inches)
    'marginBottom'        => 1.4,              // defaults to ~0.4 (must be a float, value in inches)
    'marginLeft'          => 5.0,              // defaults to ~0.4 (must be a float, value in inches)
    'marginRight'         => 1.0,              // defaults to ~0.4 (must be a float, value in inches)
    'paperWidth'          => 6.0,              // defaults to 8.5 (must be a float, value in inches)
    'paperHeight'         => 6.0,              // defaults to 11.0 (must be a float, value in inches)
    'headerTemplate'      => '<div>foo</div>', // see details above
    'footerTemplate'      => '<div>foo</div>', // see details above
    'scale'               => 1.2,              // defaults to 1.0 (must be a float)
];

// print as pdf (in memory binaries)
$pdf = $page->pdf($options);

// save the pdf
$pdf->saveToFile('/some/place/file.pdf');

// or directly output pdf without saving
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename=filename.pdf');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');

echo base64_decode($pdf->getBase64());

选项headerTemplatefooterTemplate

应有效HTML标记,并使用以下类将打印值注入其中

  • date:格式化的打印日期
  • title:文档标题
  • url:文档位置
  • pageNumber:当前页码
  • totalPages:文档中的总页数

保存下载

您可以设置保存下载文件的路径。

// After creating a page.
$page->setDownloadPath('/path/to/save/downloaded/files');

鼠标API

鼠标API依赖于页面实例,允许您控制鼠标的移动、点击和滚动。

$page->mouse()
    ->move(10, 20)                             // Moves mouse to position x=10; y=20
    ->click()                                  // left-click on position set above
    ->move(100, 200, ['steps' => 5])           // move mouse to x=100; y=200 in 5 equal steps
    ->click(['button' => Mouse::BUTTON_RIGHT]; // right-click on position set above

// given the last click was on a link, the next step will wait
// for the page to load after the link was clicked
$page->waitForReload();

您可以通过模拟鼠标滚轮在页面、框架或元素中上下滚动。

$page->mouse()
    ->scrollDown(100) // scroll down 100px
    ->scrollUp(50);   // scroll up 50px

查找元素

find方法将使用querySelector搜索元素并将光标移动到它上面的随机位置。

try {
    $page->mouse()->find('#a')->click(); // find and click at an element with id "a"

    $page->mouse()->find('.a', 10); // find the 10th or last element with class "a"
} catch (ElementNotFoundException $exception) {
    // element not found
}

此方法将尝试向右和向下滚动,将元素带到可见屏幕。如果元素位于内部可滚动部分中,首先尝试将鼠标移动到该部分内部。

键盘API

键盘API依赖于页面实例,允许您像真实用户一样输入。

$page->keyboard()
    ->typeRawKey('Tab') // type a raw key, such as Tab
    ->typeText('bar');  // type the text "bar"

为了模仿真实用户,您可能需要使用setKeyInterval方法在每个按键之间添加延迟。

$page->keyboard()->setKeyInterval(10); // sets a delay of 10 milliseconds between keystrokes

键组合

可以使用presstyperelease方法发送键组合,如ctrl + v

// ctrl + a to select all text
$page->keyboard()
    ->press('control') // key names are case insensitive and trimmed
        ->type('a')    // press and release
    ->release('Control');

// ctrl + c to copy and ctrl + v to paste it twice
$page->keyboard()
    ->press('Ctrl') // alias for Control
        ->type('c')
        ->type('V') // upper and lower cases should behave the same way
    ->release();    // release all

您可以连续按同一个键几次,这相当于用户按住键。但是,释放事件每个键只会发送一次。

键别名

别名
Control ControlCtrlCtr
Alt AltAltGrAlt Gr
Meta MetaCommandCmd
Shift Shift

Cookie API

您可以设置和获取页面的cookie。

设置Cookie

use HeadlessChromium\Cookies\Cookie;

$page = $browser->createPage();

// example 1: set cookies for a given domain

$page->setCookies([
    Cookie::create('name', 'value', [
        'domain' => 'example.com',
        'expires' => time() + 3600 // expires in 1 hour
    ])
])->await();


// example 2: set cookies for the current page

$page->navigate('http://example.com')->waitForNavigation();

$page->setCookies([
    Cookie::create('name', 'value', ['expires'])
])->await();

获取Cookies

use HeadlessChromium\Cookies\Cookie;

$page = $browser->createPage();

// example 1: get all cookies for the browser

$cookies = $page->getAllCookies();

// example 2: get cookies for the current page

$page->navigate('http://example.com')->waitForNavigation();
$cookies = $page->getCookies();

// filter cookies with name == 'foo'
$cookiesFoo = $cookies->filterBy('name', 'foo');

// find first cookie with name == 'bar'
$cookieBar = $cookies->findOneBy('name', 'bar');
if ($cookieBar) {
    // do something
}

设置用户代理

您可以按页面设置用户代理。

$page->setUserAgent('my user-agent');

有关在整个浏览器中设置用户代理的信息,请参阅BrowserFactory选项userAgent

高级用法

该库包含用于隐藏所有通信逻辑的工具,但您可以使用内部使用的工具直接与Chrome调试协议通信。

示例

use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Communication\Message;

// Chrome devtools URI
$webSocketUri = 'ws://127.0.0.1:9222/devtools/browser/xxx';

// create a connection
$connection = new Connection($webSocketUri);
$connection->connect();

// send method "Target.activateTarget"
$responseReader = $connection->sendMessage(new Message('Target.activateTarget', ['targetId' => 'xxx']));

// wait up to 1000ms for a response
$response = $responseReader->waitForResponse(1000);

创建会话并向目标发送消息

// given a target id
$targetId = 'yyy';

// create a session for this target (attachToTarget)
$session = $connection->createSession($targetId);

// send message to this target (Target.sendMessageToTarget)
$response = $session->sendMessageSync(new Message('Page.reload'));

调试

您可以在每次操作之前设置延迟以简化调试。

  $connection->setConnectionDelay(500); // wait for 500ms between each operation to ease debugging

浏览器(独立版)

use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Browser;

// Chrome devtools URI
$webSocketUri = 'ws://127.0.0.1:9222/devtools/browser/xxx';

// create connection given a WebSocket URI
$connection = new Connection($webSocketUri);
$connection->connect();

// create browser
$browser = new Browser($connection);

与DOM交互

使用CSS选择器在页面上查找一个元素

$page = $browser->createPage();
$page->navigate('http://example.com')->waitForNavigation();

$elem = $page->dom()->querySelector('#index_email');

使用CSS选择器在另一个元素内部查找所有元素

$elem = $page->dom()->querySelector('#index_email');
$elem->querySelectorAll('a.link');

通过XPath选择器查找页面上的所有元素

$page = $browser->createPage();
$page->navigate('http://example.com')->waitForNavigation();

$elem = $page->dom()->search('//div/*/a');

通过CSS选择器等待元素

$page = $browser->createPage();
$page->navigate('http://example.com')->waitForNavigation();

$page->waitUntilContainsElement('div[data-name=\"el\"]');

如果将字符串传递给Page::waitUntilContainsElement,则Page::waitForElement会为您创建一个CSSSelector实例。要使用其他选择器,您可以传递所需Selector的实例。

通过XPath选择器等待元素

use HeadlessChromium\Dom\Selector\XPathSelector;

$page = $browser->createPage();
$page->navigate('http://example.com')->waitForNavigation();

$page->waitUntilContainsElement(new XPathSelector('//div[contains(text(), "content")]'));

您可以向元素发送文本或点击它

$elem->click();
$elem->sendKeys('Sample text');

您可以从输入上传文件

$elem->sendFile('/path/to/file');

您可以获取元素的文本或属性

$text = $elem->getText();
$attr = $elem->getAttribute('class');

贡献

有关贡献详情,请参阅CONTRIBUTING.md

许可证

本项目采用MIT许可证(MIT)