初めに
Headless Chromeを使って綺麗なホームページの画像を取りたかっただけなのに随分と遠回りした話です。
とりあえず環境から作ろう
version: '2'
services:
screenshot_web:
environment:
- PHP_APP_HOST=screenshot_app
build:
context: ./
dockerfile: docker/web/Dockerfile
volumes_from:
- screenshot_app
ports:
- "80:80"
links:
- screenshot_app
environment:
- FASTCGI_PASS=screenshot_app
command: /bin/sh -c "envsubst '$$FASTCGI_PASS' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
container_name: screenshot_web
screenshot_app:
build:
context: ./
dockerfile: docker/app/Dockerfile
expose:
- "9000"
volumes:
- .:/var/www/html
depends_on:
- screenshot_chrome
container_name: screenshot_app
env_file: .env
screenshot_chrome:
build:
context: ./
dockerfile: docker/chrome/Dockerfile
stdin_open: true
tty: true
container_name: screenshot_chrome
expose:
- "4444"
FROM selenium/standalone-chrome
screenshot_webとscreenshot_appの設定は割愛します。
ここまで終わったのならとりあえずDockerを起動。
スクリーンショットを取るための中身を作ります。
{
"require": {
"facebook/webdriver": "^1.6",
"aws/aws-sdk-php": "^3.70"
}
}
<?php
require_once '../vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
use Aws\S3\Enum\CannedAcl;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverDimension;
class Screenshot {
private $_defaultOrg = [
'--disable-accelerated-2d-canvas',
'--disable-accelerated-jpeg-decoding',
'--disable-gpu',
'--disable-gpu-sandbox',
'--disable-impl-side-painting',
'--disable-setuid-sandbox',
'--headless',
'--hide-scrollbars',
'--no-sandbox',
'--test-type=ui',
];
public $userAgent = '';
public $width = 0;
public $height = 0;
public $url = '';
const GET_HEIGHT_JS = "return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);";
const LOCAL_DIR = "/var/www/html/temp";
public function getCapture ($url)
{
$options = new ChromeOptions();
$width = $this->width;
$height = 500;
// headless
$options->addArguments(array_merge(
$this->_defaultOrg, [
"--window-size={$width},{$height}",
"--user-agent={$this->userAgent}",
]
));
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);
$driver = RemoteWebDriver::create(self::$_myDriver, $capabilities);
// 指定URLへ遷移 (Google)
$driver->get($url);
$contentsHeight = $driver->executeScript(self::GET_HEIGHT_JS);
$overflowFlg = $contentsHeight > $height;
$imgFrame = imagecreatetruecolor($width, $contentsHeight);
$scrollHeight = 0;
$count = 0;
while ($scrollHeight < $contentsHeight) {
$margin = 0;
$scrollHeight += $height;
if ($scrollHeight > $contentsHeight) {
$margin = $scrollHeight - $contentsHeight;
}
$tmpFile = self::LOCAL_DIR."/temp_{$count}.png";
$driver->takeScreenshot($tmpFile);
$driver->executeScript("window.scrollBy(0, {$height});");
$count++;
$src = imagecreatefrompng($tmpFile);
// copy
imagecopy($imgFrame, $src, 0, $scrollHeight - $height - $margin, 0, 0, $width, $height);
}
// save
imagepng($imgFrame, self::LOCAL_DIR."/temp.png");
$imgFrame = null;
$driver->quit();
return self::LOCAL_DIR."/{$tempFileName}.png";
}
}
Chromeはスクリーンショットを今描画してる分だけしか取れない模様。
それで、スクロールさせて、取った画像を一つづつ繋げて、一つの画像にするという荒技です。
よしこれで行ける!
…と思ったら、色んなところで問題が多発し始める。
slick.jsが埋め込まれてるページでChromeがコケる
必ずというわけでは無いが、感覚的には8割ぐらい。
一体何が起きてるんだ。
とりあえずGETパラメータ埋め込んでパラメータ渡されたらautoplay=falseにするようにする。
if(empty(parse_url($url, PHP_URL_QUERY))) {
$url .= "?debug=1";
} else {
$url .= "&debug=1";
}
こいつを埋め込んでリトライ。
…あれ?相変わらずだぞ?
もしかして、PHP側で画像データがいっぱいになって動かないとか?
と思い、gcを強制的に動かすようにしてsleepで少し休むよう設定。
sleep(0.1);
gc_collect_cycles();
うううむ…少しはマシに、なった、かな?
感覚的すぎてわからんけどほぼ同じな気もしてきた。
で、少し調べて見たらコケた時に上手くセッションが切られて無かった。
try catchで処理囲んで、必ずquitでChromeのセッションを切るようにする。
try {
/* スクリーンショット処理 */
} catch (Exception $e) {
$thrownExcption = $e;
}
try {
$driver->quit();
} catch (Exception $e) {
// もしここでコケたら前のエラーを優先してスローする。
if(null !== $thrownExcption) {
throw $thrownExcption;
} else {
throw $e;
}
}
if(null !== $thrownExcption) {
throw $thrownExcption;
}
相変わらず感覚的だけどマシになった気がする!
でもどちらも根本的な解決にならず。
Firefoxとかもあったから試してみたら普通に動いてくれた(こちらのコードは書きません)けど、<input type=checkbox>が全然描画されない+<video>タグを撮ってくれない(Chromeは撮ってた)から安定はしたけど逆戻り。
結局謎バグと戦うしかない羽目になったけど、
そもそもSelenium辺りは未だに資料が少ない。探しても見つからない途中。
shared memory辺りの話を聞いた。
…えっ、Headless Chromeってデフォルトの共有メモリーが64mbしか無いの?
共有メモリーを増やしたい!
とりあえず、手元でやる分には問題なく、やり方も簡単だった。
screenshot_chrome:
build:
context: ./
dockerfile: docker/chrome/Dockerfile
stdin_open: true
tty: true
container_name: screenshot_chrome
expose:
- "4444"
shm_size: 1g
shm_sizeって追加するだけでおk。
早速やってみると…やったコケない!
これで問題解決やん!と思ったのに、どうも今の環境ではこれが使えないらしい。
通常メモリーはバケモノ級に増やしてるのに…
Fargateが一体何だっていうんだってばよ。
Chromeに共有メモリー使わせないオプションがあった
--disable-dev-shm-usageがそれ。
でも、追加して動かしてみても結局変わらず。
サーバー環境じゃ無いからなのかもと思ってあげてみても駄目。
で、今のところは方法がなかったので、一番安定してたFireFoxでとりあえず動かすことに。
コード自体は変わらないけど、細かいオプションの付け方が別物だった。
そもそもChromeより資料が少ないから公式っぽいドキュメント読んでも関数と引数情報しか入ってない。
そんなんじゃなくて、何のデータ入れれば良いのか説明しやがれ!
と思ったのは数え切れないほどであった。
まあ、どちらにしろ、ほぼ打つ手がなくなった。
環境…変えるか <今ここ
正確には言語を変える。PHPからNode.jsの方に行く予定だけど、共有メモリー不足が元々の原因だから治る気がしないんだよな…という感じ。
これで駄目だったら本当に環境を変える予定。
Seleniumって元々は表示系のテスト自動化ツールだったはずなのにこんなにコケて良いのか?
私が悪いのだろうか…という気分がしなくもない今の現状でした。
一番怖いのは、これが現在進行中ということさ!