php-webdriverを使って指定した要素のキャプチャを撮る

投稿日:

朝は人間にとってゴールデンタイムと呼ばれているようにセミにとってもそれはゴールデンタイムのようです。

前回、php-webdriverを使ってフルスクリーンのキャプチャを撮る ことに成功したのですが、そこに至るまでにちょっと試してみたことがあって、それも無事解決したのでは?というお話を書きます。
※後述の課題点にあるようにすべて解決というわけではないです

要素のキャプチャ

それは何かというと、要素を指定した場合その部分だけのキャプチャを撮るというものです。
※この記事のサムネイル画像は、google検索結果画面の#nav > tbody部分のキャプチャです

そもそも、php-webdriverのwikiにはこんなサンプルが載っていて
Taking Full Screenshot and of an Element · facebook/php-webdriver Wiki
どうやら要素を指定すると、その要素のみキャプチャが撮れますよというサンプルのようなのですが、それを試しても前回書いたようにスクリーンキャプチャはブラウザによって撮る範囲が決まっていて、

  • 現在見えている範囲の要素は上手くキャプチャが撮れる
  • 見えていない範囲の要素のキャプチャは撮れない

という問題を抱えていたのです。
※ 今のところ全画面を撮るIEでは上手く撮れますが、、

実際に試してみたソースがこちらで、

サンプル

<?php

require_once realpath(__DIR__ . '/../vendor') . '/autoload.php';

use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\WebDriverBy;

/**
 * @link https://github.com/facebook/php-webdriver/wiki/taking-full-screenshot-and-of-an-element
 */
function TakeScreenshot($driver, $tempDirectoryPath, $element=null)
{
    // Change the Path to your own settings
    $screenshot = $tempDirectoryPath . time() . ".png";

    // Change the driver instance
    $driver->takeScreenshot($screenshot);
    if(!file_exists($screenshot)) {
        throw new Exception('Could not save screenshot');
    }

    if( ! (bool) $element) {
        return $screenshot;
    }

    $element_screenshot = $tempDirectoryPath . time() . ".png"; // Change the path here as well

    $element_width = $element->getSize()->getWidth();
    $element_height = $element->getSize()->getHeight();

    $element_src_x = $element->getLocation()->getX();
    $element_src_y = $element->getLocation()->getY();

    // Create image instances
    $src = imagecreatefrompng($screenshot);
    $dest = imagecreatetruecolor($element_width, $element_height);

    // Copy
    imagecopy($dest, $src, 0, 0, $element_src_x, $element_src_y, $element_width, $element_height);

    imagepng($dest, $element_screenshot);

    // unlink($screenshot); // unlink function might be restricted in mac os x.

    if( ! file_exists($element_screenshot)) {
        throw new Exception('Could not save element screenshot');
    }

    return $element_screenshot;
}

/**
 * selenium php-webdriver 指定した要素のキャプチャ サンプル
 */
function sample()
{
    // selenium
    $host = 'http://localhost:4444/wd/hub';

    if (getenv('CHROME_DRIVER_PATH') !== '') {
        putenv('webdriver.chrome.driver=' . getenv('CHROME_DRIVER_PATH'));
    }

    $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome());

    // 指定URLへ遷移 (Google)
    $driver->get('https://www.google.co.jp/');

    // 検索Box
    $findElement = $driver->findElement(WebDriverBy::name('q'));
    // 検索Boxにキーワードを入力して
    $findElement->sendKeys('お盆の予定');
    // 検索実行
    $findElement->submit();

    $tempDirectoryPath = realpath(__DIR__ . '/../capture') . '/';

    // 見えている範囲だとキャプチャは撮れる
    $element = $driver->findElement(WebDriverBy::cssSelector('#sfcnt'));
    TakeScreenshot($driver, $tempDirectoryPath, $element);

    // 見えていない範囲だとキャプチャは取れない
    $element2 = $driver->findElement(WebDriverBy::cssSelector('#fbar'));
    TakeScreenshot($driver, $tempDirectoryPath, $element2);

    // ブラウザを閉じる
    $driver->close();
}

sample();

こちらを試してみて頂くと分かるように、

  • 見えている範囲(ヘッダーらへんの #sfcnt)は撮れている
  • 見えていない範囲(フッターらへんの #fbar)は撮れていない

こんな残念な感じになってしまいます。

解決

で、これも結局画面の全範囲が特定出来ていないことが原因なので、全画面のキャプチャを元に、指定した要素の座標とサイズを使えばきちんと撮れるんじゃね?と思い立って書いたのが以下になります。

ソースはこちら。
shimabox/sample-phpwebdriver: Test with phpunit and phpwebdriver

SMB\PhpWebDriver\Modules\Elements\Spec.php
要素のスペック(セレクタの条件)を定義できます。

<?php

namespace SMB\PhpWebDriver\Modules\Elements;

/**
 * Spec
 */
class Spec
{
    /**
     * 等しい ===
     * @var string
     */
    const EQUAL = '===';

    /**
     * 等しくない !==
     * @var string
     */
    const NOT_EQUAL = '!==';

    /**
     * ~より大きい >
     * @var string
     */
    const GREATER_THAN = '>';

    /**
     * ~より小さい <
     * @var string
     */
    const LESS_THAN = '<';

    /**
     * ~以上 >=
     * @var string
     */
    const GREATER_THAN_OR_EQUAL = '>=';

    /**
     * ~以下 <=
     * @var string
     */
    const LESS_THAN_OR_EQUAL = '<=';

    /**
     * css selector
     * @var string
     */
    private $selector = '';

    /**
     * 条件
     * @var string
     */
    private $condition = '';

    /**
     * 期待する要素の出現数
     * @var int
     */
    private $expectedElementCount = 1;

    /**
     * コンストラクタ
     * @param string $selector
     * @param string $condition default '==='
     * @param int $expectedElementCount default 1
     */
    public function __construct($selector, $condition=self::EQUAL, $expectedElementCount = 1)
    {
        $this->selector = $selector;
        $this->condition = $condition;
        $this->expectedElementCount = $expectedElementCount;
    }

    /**
     * css selector ゲッター
     * @return string
     */
    public function getSelector()
    {
        return $this->selector;
    }

    /**
     * 条件 ゲッター
     * @return string
     */
    public function getCondition()
    {
        return $this->condition;
    }

    /**
     * 期待する要素の出現数
     * @return int
     */
    public function getExpectedElementCount()
    {
        return $this->expectedElementCount;
    }
}

SMB\PhpWebDriver\Modules\Elements\SpecPool.php
上記のスペックを知っています。

<?php

namespace SMB\PhpWebDriver\Modules\Elements;

use SMB\PhpWebDriver\Modules\Elements\Spec;

/**
 * SpecPool
 */
class SpecPool
{
    /**
     * Spec
     * @var array [\SMB\PhpWebDriver\Modules\Elements\Spec]
     */
    private $spec = array();

    /**
     * Spec 追加
     * @param \SMB\PhpWebDriver\Modules\Elements\Spec $spec
     * @return \SMB\PhpWebDriver\Modules\Elements\SpecPool
     */
    public function addSpec(Spec $spec)
    {
        $this->spec[] = $spec;
        return $this;
    }

    /**
     * Spec ゲッター
     * @return array [\SMB\PhpWebDriver\Modules\Elements\Spec]
     */
    public function getSpec()
    {
        return $this->spec;
    }

    /**
     * Spec clear
     */
    public function clearSpec()
    {
        $this->spec = [];
    }
}

SMB\PhpWebDriver\Modules\Screenshot.php
前回書いたクラスに以下のメソッドtakeElementを生やしました。

  • takeFullで一旦全画面のキャプチャを撮ります
  • 全画面キャプチャのリソースをもとに上記のSpecPoolを使って指定された要素のキャプチャを作成します
<?php

namespace SMB\PhpWebDriver\Modules;

use SMB\PhpWebDriver\Modules\Elements\SpecPool;
use SMB\PhpWebDriver\Modules\Elements\Spec;

use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\WebDriverBrowserType;

/**
 * Screenshot
 */
class Screenshot
{
    // 略

    /**
     * 指定された要素のキャプチャ
     * @param RemoteWebDriver $driver
     * @param string $filepath
     * @param string $filename Without extension
     * @param string $browser
     * @param SpecPool $specPool 取得したい要素のスペック
     * @param int $sleep Sleep for seconds
     * @return string  キャプチャ画像ファイルパス
     * @throws \Exception
     * @link https://github.com/facebook/php-webdriver/wiki/taking-full-screenshot-and-of-an-element
     */
    public function takeElement(RemoteWebDriver $driver, $filepath, $filename, $browser, SpecPool $specPool, $sleep=1)
    {
        // 一旦全画面のキャプチャを撮る
        $tmpFullScreenshot = $this->takeFull($driver, $filepath, $filename . '_tmp_' . time() . '.png', $browser, $sleep);
        // create image instances
        $src = imagecreatefrompng($tmpFullScreenshot);

        $elements = null;
        $specList = $specPool->getSpec();
        foreach ($specList as $specIndex => $spec) {
            $driver->wait()->until(
                function () use ($driver, $spec, &$elements) {
                    $elements = $driver->findElements(WebDriverBy::cssSelector($spec->getSelector()));

                    $count = count($elements);
                    $expected = $spec->getExpectedElementCount();

                    $conditon = $spec->getCondition();
                    switch ($conditon) {
                        case Spec::EQUAL :
                            return $count === $expected;
                        case Spec::NOT_EQUAL :
                            return $count !== $expected;
                        case Spec::GREATER_THAN :
                            return $count > $expected;
                        case Spec::LESS_THAN :
                            return $count < $expected;
                        case Spec::GREATER_THAN_OR_EQUAL :
                            return $count >= $expected;
                        case Spec::LESS_THAN_OR_EQUAL :
                            return $count <= $expected;
                    }
                }
            );

            foreach ($elements as $index => $element) {
                // 指定された要素のサイズ
                $elementWidth = $element->getSize()->getWidth();
                $elementHeight = $element->getSize()->getHeight();

                // 指定された要素が存在する座標
                $elementSrcX = $element->getLocation()->getX();
                $elementSrcY = $element->getLocation()->getY();

                $dest = imagecreatetruecolor($elementWidth, $elementHeight);
                $captureFile = $filepath . $filename . '_' . $specIndex . '_' . $index . '.png';

                $this->toPatchTheImage($captureFile, $dest, $src, 0, 0, $elementSrcX, $elementSrcY, $elementWidth, $elementHeight);

                $this->destroyImage($dest);

                $this->throwExceptionIfNotExistsFile($captureFile, 'Could not save element screenshot');
            }
        }

        $this->destroyImage($src);
        $this->deleteImageFile($tmpFullScreenshot);
    }

    // 略
}

そして以下がこのクラスを使うサンプル用ソースになります。

sample_6_element_screenshot.php

<?php

require_once realpath(__DIR__ . '/../vendor') . '/autoload.php';

use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\Chrome;
use Facebook\WebDriver\Firefox;

use SMB\PhpWebDriver\Modules\Screenshot;
use SMB\PhpWebDriver\Modules\Elements\Spec;
use SMB\PhpWebDriver\Modules\Elements\SpecPool;

/**
 * selenium php-webdriver 指定した要素のキャプチャ サンプル
 * @param string $browser chrome or firefox or ie
 * @param array $size ['w' => xxx, 'h' => xxx]
 * @param string overrideUA true : override Useragent
 */
function sample_6($browser, array $size=[], $overrideUA = '')
{
    // selenium
    $host = 'http://localhost:4444/wd/hub';

    switch ($browser) {
        case WebDriverBrowserType::CHROME :
            $cap = DesiredCapabilities::chrome();

            if ($overrideUA !== '') {
                $options = new Chrome\ChromeOptions();
                $options->addArguments(['--user-agent=' . $overrideUA]);

                $cap->setCapability(Chrome\ChromeOptions::CAPABILITY, $options);
            }

            if (getenv('CHROME_DRIVER_PATH') !== '') {
                putenv('webdriver.chrome.driver=' . getenv('CHROME_DRIVER_PATH'));
            }

            $driver = RemoteWebDriver::create($host, $cap);

            break;
        case WebDriverBrowserType::FIREFOX :
            $cap = DesiredCapabilities::firefox();

            if ($overrideUA !== '') {
                $profile = new Firefox\FirefoxProfile();
                $profile->setPreference('general.useragent.override', $overrideUA);

                $cap->setCapability(Firefox\FirefoxDriver::PROFILE, $profile);
            }

            if (getenv('FIREFOX_DRIVER_PATH') !== '') {
                putenv('webdriver.gecko.driver=' . getenv('FIREFOX_DRIVER_PATH'));
            }

            $driver = RemoteWebDriver::create($host, $cap);

            break;
        case WebDriverBrowserType::IE :
            if (getenv('IE_DRIVER_PATH') !== '') {
                putenv('webdriver.ie.driver=' . getenv('IE_DRIVER_PATH'));
            }
            $driver = RemoteWebDriver::create($host, DesiredCapabilities::internetExplorer());
            break;
    }

    // 画面サイズをMAXにする場合
    // $driver->manage()->window()->maximize();

    // 画面サイズの指定あり
    if (isset($size['w']) && isset($size['h'])) {
        $dimension = new WebDriverDimension($size['w'], $size['h']);
        $driver->manage()->window()->setSize($dimension);
    }

    // 指定URLへ遷移 (Google)
    $driver->get('https://www.google.co.jp/');

    // 検索Box
    $findElement = $driver->findElement(WebDriverBy::name('q'));
    // 検索Boxにキーワードを入力して
    $findElement->sendKeys('お盆の予定');
    // 検索実行
    $findElement->submit();

    // pc と sp で指定要素を変える
    $selector = $overrideUA === '' ? '.rc' : '#rso > div > div.mnr-c';
    $selector2 = $overrideUA === '' ? '.brs_col' : 'a._bCp';

    // 要素のセレクターを定義して
    $spec = new Spec($selector, Spec::GREATER_THAN_OR_EQUAL, 10);
    $spec2 = new Spec($selector2, Spec::GREATER_THAN, 1);

    // SpecPoolに突っ込む
    $specPool = (new SpecPool())
                ->addSpec($spec)
                ->addSpec($spec2);

    // キャプチャ (ファイル名は拡張子無し / pngになります)
    $fileName = $overrideUA === '' ? __METHOD__ . "_{$browser}" : __METHOD__ . "_sp_{$browser}";
    $captureDirectoryPath = realpath(__DIR__ . '/../capture') . '/';

    $screenshot = new Screenshot();
    $screenshot->takeElement($driver, $captureDirectoryPath, $fileName, $browser, $specPool);

    // ブラウザを閉じる
    $driver->close();
}

// iPhone6のサイズ
$size4iPhone6 = ['w' => 375, 'h' => 667];

// iOS10のUA
$ua4iOS = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1';

/**
 |------------------------------------------------------------------------------
 | 有効にしたいドライバーの値を true にしてください
 |------------------------------------------------------------------------------
 */

// chrome
if (getenv('ENABLED_CHROME_DRIVER') === 'true') {
    sample_6(WebDriverBrowserType::CHROME);
    sample_6(WebDriverBrowserType::CHROME, $size4iPhone6, $ua4iOS);
}

// firefox
if (getenv('ENABLED_FIREFOX_DRIVER') === 'true') {
    sample_6(WebDriverBrowserType::FIREFOX);
    sample_6(WebDriverBrowserType::FIREFOX, $size4iPhone6, $ua4iOS);
}

// ie
if (getenv('IE_DRIVER_PATH') !== '') {
    sample_6(WebDriverBrowserType::IE);
}

使い方

一応使い方は、

// pc と sp で指定要素を変える
$selector = $overrideUA === '' ? '.rc' : '#rso > div > div.mnr-c';
$selector2 = $overrideUA === '' ? '.brs_col' : 'a._bCp';

// 要素のセレクターを定義して
$spec = new Spec($selector, Spec::GREATER_THAN_OR_EQUAL, 10);
$spec2 = new Spec($selector2, Spec::GREATER_THAN, 1);

// SpecPoolに突っ込む
$specPool = (new SpecPool())
            ->addSpec($spec)
            ->addSpec($spec2);

$screenshot = new Screenshot();
$screenshot->takeElement($driver, $captureDirectoryPath, $fileName, $browser, $specPool);

こんな感じで、Specに要素のセレクタとその要素の出現回数を指定してSpecPoolに突っ込んでtakeElementに渡すだけ。
というものになります。

ですので、最初のサンプルで撮れなかった要素#fbar

$spec = new Spec('#fbar', Spec::EQUAL, 1);
$specPool = (new SpecPool())->addSpec($spec);

$screenshot = new Screenshot();
$screenshot->takeElement($driver, $captureDirectoryPath, $fileName, $browser, $specPool);

このように定義すれば上手くキャプチャが撮れます。

注意点

例えば指定したセレクタをミスって、存在しない要素だったり指定したセレクタのサイズが取れない場合はエラーになってしまいます。
あと、*とかの無茶なセレクタではなく、節操のあるセレクタを指定してください。

課題

いまのところ、無限スクロールとかページ内のiframeとかには対応できていません。
そもそも、特定の要素のみキャプチャを撮ることの需要があるのかわかりませんが。。

まとめ

特定要素のキャプチャは撮れる。

上記内容で動作しないケースがあったら

こちらの記事を参考にして頂けると解決するかもしれません。
php-webdriverのラッパーライブラリ”screru”のバージョンをあげた – Shimabox Blog

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください