chrome-php/chrome

从PHP中监测无头chrome/chromium实例

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

README

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,打开页面,截图,爬取网站...几乎您可以像人类一样用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的LoggerInterface的对象(例如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();

可用选项

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

持久浏览器

此示例展示了如何为多个脚本共享单个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更新时,我们将等待页面卸载。您可以通过两个可选参数指定等待时间和等待哪个事件。默认为3000ms和"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->getFullPageClipcaptureBeyondViewport = 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:URL地址
  • pageNumber:当前页码
  • totalPages:文档总页数

Save downloads:保存下载

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

// 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

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

键别名

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();

获取 Cookie

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) 许可。