UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

PHPでYouTube APIを並列実行する

nazoです。

弊社は業務内容上、Google API、特にYouTube APIを叩くことが多いのですが、PHPクライアントであるgoogle-api-php-clientは、非同期処理を書くのは簡単ではありません。

しかし、近日リリース予定である2.0では、通信部分がGuzzleになり、そこそこ簡単に非同期処理を書けるようになりました。(最近までGuzzleのバージョンが5でしたが、6でも動かせるパッチがmergeされたようです。2016/01/20現在ではv2.0.0-RC4を利用します)

ここでは最新のgoogle-api-php-clientを使用し、YouTube APIを並列実行する方法について解説します。

試してみる

composergoogle-api-php-clientの2.0.0-RC4とGuzzleを入れた上で、以下のスクリプトを実行します。(CLI用です)

GoogleのクライアントID等は各自で取得してください。ここでは省略します。

% composer init
% composer require guzzlehttp/guzzle
% composer require google/apiclient:2.0.0-RC4
<?php

require 'vendor/autoload.php';

/**
 * 認証処理をした上で、 Google_Service_YouTube を返す
 *
 * @return Google_Service_YouTube
 */
function getYouTubeService()
{
    $clientId = 'YOUR-API-KEY';
    $clientSecret = 'YOUR-API-SECRET';

    $client = new Google_Client();
    $client->setClientId($clientId);
    $client->setClientSecret($clientSecret);
    $client->setRedirectUri('urn:ietf:wg:oauth:2.0:oob');
    $client->setScopes('https://www.googleapis.com/auth/youtube.readonly');

    $url = $client->createAuthUrl();
    echo '以下のURLを開いてください。' . PHP_EOL;
    echo $url . PHP_EOL;
    echo 'コードを入力してください:';
    $code = fgets(STDIN);
    $client->authenticate($code);

    return new \Google_Service_YouTube($client);
}

/**
 * 同期でチャンネル情報を取得する
 *
 * @param array $channelIds チャンネルID文字列のリスト
 * @param Google_Service_YouTube $service getYouTubeService() で取得したもの
 * @return float 実行時間(秒)
 */
function runSync(array $channelIds, \Google_Service_YouTube $service)
{
    $service->getClient()->setDefer(false);     // API実行時に実行結果オブジェクトを返す

    $time = microtime(true);
    foreach ($channelIds as $channelId) {
        $channelInfo = $service->channels->listChannels('id,snippet', ['id' => $channelId]);
        echo $channelInfo['items'][0]['snippet']['title'] . PHP_EOL;
    }
    return microtime(true) - $time;
}

/**
 * 非同期でチャンネル情報を取得する
 *
 * @param array $channelIds チャンネルID文字列のリスト
 * @param Google_Service_YouTube $service getYouTubeService() で取得したもの
 * @return float 実行時間(秒)
 */
function runAsync(array $channelIds, \Google_Service_YouTube $service)
{
    $service->getClient()->setDefer(true);      // API実行時に`GuzzleHttp\Psr7\Request`を返す

    $time = microtime(true);
    $promisesGenerator = function () use ($service, $channelIds) {
        foreach ($channelIds as $channelId) {
            $request = $service->channels->listChannels('id,snippet', ['id' => $channelId]);
            $promise = $service->getClient()->authorize()->sendAsync($request);
            // API呼び出し成功時の処理
            $promise->then(function ($response) use ($request) {
                $channelInfo = \Google_Http_REST::decodeHttpResponse($response, $request);      // GoogleAPIの結果オブジェクトを取得
                echo $channelInfo['items'][0]['snippet']['title'] . PHP_EOL;
            });
            yield $promise;
        }
    };
    $promise = \GuzzleHttp\Promise\each_limit(
        $promisesGenerator(),
        5   // 5並列で実行
    );
    $promise->wait();

    return microtime(true) - $time;
}

$service = getYouTubeService();

$channelIds = [
    'UCr-QcqNToYablI-jU2VPVSw',
    'UCg4nOl7_gtStrLwF0_xoV0A',
    'UCFTVNLC7ysej-sD5lkLqNGA',
    'UCgMPP6RRjktV7krOfyUewqw',
    'UCZf__ehlCEBPop-_sldpBUQ',
    'UCHhXSfCzQYAAFkpdxr7QsaA',
    'UCFBjsYvwX7kWUjQoW7GcJ5A',
    'UCfCY70zRsvnnKzQ39mBq0rw',
    'UCpOjLndjOqMoffA-fr8cbKA',
    'UCO06KZjWOe6b1tXrgzzakZA',
];

echo '=== 同期版 ===' . PHP_EOL;

printf('=== %f sec ===' . PHP_EOL, runSync($channelIds, $service));

echo '=== 非同期版 ===' . PHP_EOL;

printf('=== %f sec ===' . PHP_EOL, runAsync($channelIds, $service));

大体、以下のような結果になると思います。(具体的な実行結果は皆さんで確認してみてください)

% php google.php
以下のURLを開いてください。
https://accounts.google.com/o/oauth2/auth?〜
コードを入力してください:[ブラウザで表示されたコードを入力]
=== 同期版 ===
くまみき/Kumamiki
SeikinTV
木下ゆうか Yuka Kinoshita
はじめしゃちょー(hajime)
HikakinTV
AAAjoken
瀬戸弘司 / Koji Seto
sasakiasahi
水溜りボンド
ぽこにゃん
=== 1.304957 sec ===
=== 非同期版 ===
木下ゆうか Yuka Kinoshita
SeikinTV
HikakinTV
くまみき/Kumamiki
はじめしゃちょー(hajime)
AAAjoken
瀬戸弘司 / Koji Seto
sasakiasahi
水溜りボンド
ぽこにゃん
=== 0.549455 sec ===

通信処理なので具体的な実行時間は大きく変わることがありますが、大体、同期版に比べて非同期版のほうが倍近く高速になっているのを確認することができると思います。また、同期版は何度行っても表示されるチャンネル名の順序が同一ですが、非同期版は実行する度に順序が変わっているのが確認できると思います。(並列数を制限しているのと、並列とは言え呼び出しを開始する順序は決まっているので、大きく順序が変わることは少ないです)

概要

Google_Clientに対し、$client->setDefer(true);を呼ぶと、APIの実行時にいきなり結果を返すのではなくRequestオブジェクトを返すのは1系も2系も同じなのですが、1系では独自の Google_Http_Request であったのに対し、2系ではGuzzleのRequestオブジェクト( GuzzleHttp\Psr7\Request )を返すようになります。 このオブジェクトに対し、通常のGuzzleで行える非同期処理の手法を使うことにより、非同期でAPIが呼べるようになります。

まとめ

というわけで、google-api-php-clientというより、Guzzleの使い方の説明になってしまいましたが、このように簡単に非同期処理を書くことができます。

google-api-php-client自体には、1系と2系で基本的な書き方の違いはほとんどないので、特殊な使い方をしていなければ、何も修正しなくても1系から2系に差し替えることができると思います。詳細な変更点についてはUPGRADING.mdを参照していただければと思います。2.0の正式リリースが楽しみですね。

もちろん、YouTube API以外のGoogle APIでも利用できますので、大量のAPIを高速に呼び出したい場合は利用を検討してみてください。ちなみに aws-sdk-phpも似たようなことが可能です

UUUMでは、YouTuberYouTuberをサポートするバティはもちろん、YouTube APIを駆使してYouTuberをサポートするエンジニアを積極採用中です。詳しくは以下をご覧下さい。