2019 年 Google 發一則 公告 表示即將停用 AdSense 手機 App 。前天又來發信通知,索性就開始研究 API ,想把手機版報表功能移植到 LINE Notify 定時通知。 Ref: [PHP] LINE Notify 應用於行政流程的方法(範例)

一開始看 API 文件還以為是 AdSense Host API 這個,結果串好看回應表示我不是 Host 帳號XD 一查才知道 Host 帳號是指 Blogger 或 痞客邦 這樣使用子網域的內容平台申請者,可以透過這種類型帳號幫旗下客戶申請廣告聯播網功能。Ref: Google API: Customer is not an AdSense Host

執行結果如下圖:

AdSense LINE Notify

前置準備

  1. 一組完整權限的 Google AdSense 帳號
  2. 一個網站伺服器與可以使用的網域
  3. 看得懂文件與改點 PHP 程式的能力

申請 Google APIs 與使用憑證相關資料

開始前要先到 Google Cloud Platform 中啟用 AdSense Management API

確認啟用後再到「憑證」建立一組 OAuth 用戶端 ID,類型為網路應用程式。申請過程中會需要驗證網域的就先去驗證,然後把「已授權的 JavaScript 來源」填入含 HTTP 協議的網址,例如: https://www.mxp.tw ,而「已授權的重新導向 URI」則填入授權後跳轉回去的應用程式路徑,例如:https://www.mxp.tw/adsense/callback.php

申請 OAuth 這一步是建立一個規範角色,確保參與 OAuth 協議過程中都能遵守申請的設定,完成授權。所以會需要指定授權的網域以及回傳接收資料的位置!針對 OAuth 協議的細節可以參考這份文件:Using OAuth 2.0 to Access Google APIs

申請完這步驟後會得到一組「OAuth 2.0 用戶端編號與密碼」,這些都不用記,把這組資訊下載 JSON 檔案到電腦中先暫存著。

程式設計邏輯

官方範例其實已經夠詳細,稍微 Google 一下就找到 AdSense Management API sample for PHP 。 這專案寫了很多範例,但都不是本篇要的,僅是提供串接參考。

除非環境的限制,例如 PHP 版本過低或硬是不想用 Google 那一大包的函式庫,不然絕對是用他的那包方便!(想用 cURL 自己串的可以看這邊:Google oAuth 2.0 PHP Curl Boilerplate

首先要安裝 Google APIs Client Library for PHP 這包客戶端工具,非常推薦使用 Composer 套件安裝方式,裝好後只需要在程式中引用一行即可。

require_once __DIR__ . '/vendor/autoload.php';

使用 Google APIs 客戶端工具分為兩大方向:

  1. Google Client OAuth 2.0 交握取得授權
  2. 拿著取得的授權開始使用 API 服務

Google Client OAuth 2.0 交握取得授權

所以取得授權的程式如下:

$client = new Google_Client();
$client->addScope('https://www.googleapis.com/auth/adsense.readonly');
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->setAuthConfig('/path/to/client_secrets.json');

AuthConfig 就是剛剛下載的客戶端編號等相關設定 JSON 檔案路徑。

然後輸出組合出前往授權的網址:

echo '<a class="login" href="' . $client->createAuthUrl() . '">綁定</a>';

打開瀏覽器點擊此連結登入 Google 帳號後會到 Google 授權服務的頁面,一系列確認完成授權後即轉往先前設定的那個服務頁面接收授權 code

if (isset($_GET['code'])) {
    $client->authenticate($_GET['code']);
    // Note that "getAccessToken" actually retrieves both the access and refresh
    // tokens, assuming both are available.
    $access_token  = $client->getAccessToken();
    $refresh_token = $client->getRefreshToken();

    file_put_contents(TOKEN_FILENAME, json_encode(array(
        'access_token'  => $access_token,
        'refresh_token' => $refresh_token,
        'expires_time'  => intval($access_token['expires_in']) + intval($access_token['created']) - 30,
    )));
}

Google_Client 物件收到 GET 方法取得的 code 後完成授權,即可取得接下來需要的授權碼。範例中僅是使用 PHP Session 方法記錄,我則是改成寫入一個檔案裡暫存,用來之後離線還可以請求。

如果有離線使用的需求,關鍵是要設定這兩行 $client->setAccessType('offline');$client->setApprovalPrompt('force');。然後會取得一個 refresh_token 更新 Access Token 的 Token,之後當 Access Token 過期時可以拿這個 refresh_token 去換過一組新的。換 Access Token 的方法直接寫在下方完整程式碼的章節。

使用取得的授權開始使用 API 服務

將授權過的客戶端物件帶入 AdSense 的方法來呼叫。

$service       = new Google_Service_AdSense($client);

然後就能用這個 Google_Service_AdSense 物件開始回傳報表囉!

$t     = time();
$FMD   = date('Y-m-01', $t);
$TODAY = date('Y-m-d', $t);
$account_id = 'pub-你的 AdSense 編號'; 
$result     = $service->accounts_reports->generate(
    $account_id,
    $FMD,
    $TODAY,
    array(
        'metric'               => array("EARNINGS", "PAGE_VIEWS", "PAGE_VIEWS_RPM", "MATCHED_AD_REQUESTS", "CLICKS", "COST_PER_CLICK", "AD_REQUESTS_COVERAGE"),
        'dimension'            => array('DOMAIN_NAME', 'DATE'),
        'useTimezoneReporting' => true,
    )
);

這個方法是「取得網站月初到今日的每日營收相關報表」。指標和維度的參數可以參考連結文件說明。

取得每日的資訊後,要做什麼統計資訊就完全看個人了!我則是參考 App 裡常用的「今日累計」與「本月累計」。各別網站的查看「涵蓋率」也是能抓出廣告放送狀態是否正常。

程式組裝

上述都算是片段的程式解析,並非完全部分,完整的程式邏輯還有些設計,如下:

<?php
require_once '../vendor/autoload.php';
define('TOKEN_FILENAME', '/PATH/TO/YOUR/TOKENFILE.dat', true);

$client = new Google_Client();
$client->addScope('https://www.googleapis.com/auth/adsense.readonly');
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->setAuthConfig('/PATH/TO/YOUR/client_secrets.json');
$auth          = "";
$access_token  = "";
$refresh_token = "";
$service       = new Google_Service_AdSense($client);

function gapp_refresh_token($client, $auth) {
    $params = array(
        "client_id"     => $client->getClientId(),
        "client_secret" => $client->getClientSecret(),
        "refresh_token" => $auth['refresh_token'],
        "grant_type"    => "refresh_token",
    );
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_URL, 'https://accounts.google.com/o/oauth2/token');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $r = curl_exec($ch);
    curl_close($ch);
    $g_res                                = json_decode($r, true);
    $access_token                         = $g_res['access_token'];
    $expires_in                           = $g_res['expires_in'];
    $scope                                = $g_res['scope'];
    $auth['access_token']['access_token'] = $access_token;
    $auth['access_token']['created']      = time();
    $auth['expires_time']                 = intval($expires_in) + time() - 30;
    file_put_contents(TOKEN_FILENAME, json_encode($auth));
    $client->setAccessToken($auth['access_token']);
    return $client;
}

if (isset($_GET['code'])) {
    $client->authenticate($_GET['code']);
    $access_token  = $client->getAccessToken();
    $refresh_token = $client->getRefreshToken();

    file_put_contents(TOKEN_FILENAME, json_encode(array(
        'access_token'  => $access_token,
        'refresh_token' => $refresh_token,
        'expires_time'  => intval($access_token['expires_in']) + intval($access_token['created']) - 30,
    )));

}

if (file_exists(TOKEN_FILENAME) && filesize(TOKEN_FILENAME) > 0) {
    $auth = json_decode(file_get_contents(TOKEN_FILENAME), true);
    //還沒過期
    if (time() < $auth['expires_time']) {
        $client->setAccessToken($auth['access_token']);
        $access_token = $client->getAccessToken();
    } else {
        //過期了去刷過
        if (isset($auth['refresh_token']) && !empty($auth['refresh_token'])) {
            $client = gapp_refresh_token($client, $auth);
        } else {
            echo '<a class="login" href="' . $client->createAuthUrl() . '">綁定</a>';
            exit;
        }
    }
} else {
    echo '<a class="login" href="' . $client->createAuthUrl() . '">綁定</a>';
    exit;
}
//到這邊就是授權狀態中,可以開始請求

$t          = time();
$FMD        = date('Y-m-01', $t);
$TODAY      = date('Y-m-d', $t);
$account_id = 'pub-YOUR_PUB_ID';
$result     = $service->accounts_reports->generate(
    $account_id,
    $FMD,
    $TODAY,
    array(
        'metric'               => array("EARNINGS", "PAGE_VIEWS", "PAGE_VIEWS_RPM", "MATCHED_AD_REQUESTS", "CLICKS", "COST_PER_CLICK", "AD_REQUESTS_COVERAGE"),
        'dimension'            => array('DOMAIN_NAME', 'DATE'),
        'useTimezoneReporting' => true,
    )
);

$rows = $result['rows'];

$domain = array();
foreach ($rows as $key => $row) {
    if (!isset($domain[$row[0]])) {
        if (in_array($row[0], array('www.mxp.tw'))) {
            $domain[$row[0]]   = array();
            $domain[$row[0]][] = array('DATE' => $row[1], 'EARNINGS' => $row[2], 'PAGE_VIEWS' => $row[3], 'PAGE_VIEWS_RPM' => $row[4], 'MATCHED_AD_REQUESTS' => $row[5], 'CLICKS' => $row[6], 'COST_PER_CLICK' => $row[7], 'AD_REQUESTS_COVERAGE' => $row[8]);
        }
    } else {
        if (in_array($row[0], array('www.mxp.tw'))) {
            $domain[$row[0]][] = array('DATE' => $row[1], 'EARNINGS' => $row[2], 'PAGE_VIEWS' => $row[3], 'PAGE_VIEWS_RPM' => $row[4], 'MATCHED_AD_REQUESTS' => $row[5], 'CLICKS' => $row[6], 'COST_PER_CLICK' => $row[7], 'AD_REQUESTS_COVERAGE' => $row[8]);
        }
    }
}

$total = array();
foreach ($domain as $key => $value) {
    $total[$key] = array();
    $days        = count($value);
    //累計收入
    $total[$key]['T_EARNINGS']   = 0;
    $total[$key]['AVG_EARNINGS'] = 0;
    //累計瀏覽數
    $total[$key]['T_PAGE_VIEWS']   = 0;
    $total[$key]['AVG_PAGE_VIEWS'] = 0;
    //累計廣告曝光量
    $total[$key]['T_MATCHED_AD_REQUESTS']   = 0;
    $total[$key]['AVG_MATCHED_AD_REQUESTS'] = 0;
    //累計點擊數
    $total[$key]['T_CLICKS']   = 0;
    $total[$key]['AVG_CLICKS'] = 0;
    //累計點擊數
    $total[$key]['T_COST_PER_CLICK']   = 0;
    $total[$key]['AVG_COST_PER_CLICK'] = 0;
    //累計涵蓋率
    $total[$key]['T_AD_REQUESTS_COVERAGE']   = 0;
    $total[$key]['AVG_AD_REQUESTS_COVERAGE'] = 0;
    //累計千次瀏覽收益
    $total[$key]['T_PAGE_VIEWS_RPM']   = 0;
    $total[$key]['AVG_PAGE_VIEWS_RPM'] = 0;
    foreach ($value as $index => $data) {
        $total[$key]['T_EARNINGS'] += floatval($data['EARNINGS']);
        $total[$key]['T_PAGE_VIEWS'] += intval($data['PAGE_VIEWS']);
        $total[$key]['T_CLICKS'] += intval($data['CLICKS']);
        $total[$key]['T_MATCHED_AD_REQUESTS'] += intval($data['MATCHED_AD_REQUESTS']);
        $total[$key]['T_COST_PER_CLICK'] += floatval($data['COST_PER_CLICK']);
        $total[$key]['T_AD_REQUESTS_COVERAGE'] += floatval($data['AD_REQUESTS_COVERAGE']);
        $total[$key]['T_PAGE_VIEWS_RPM'] += floatval($data['PAGE_VIEWS_RPM']);
        if ($index == ($days - 1)) {
            $total[$key]['TODAY'] = $data;
        }
    }
    //平均收入
    $total[$key]['AVG_EARNINGS'] = $total[$key]['T_EARNINGS'] / $days;
    //平均瀏覽數
    $total[$key]['AVG_PAGE_VIEWS'] = $total[$key]['T_PAGE_VIEWS'] / $days;
    //平均點擊數
    $total[$key]['AVG_CLICKS'] = $total[$key]['T_CLICKS'] / $days;
    //平均曝光量
    $total[$key]['AVG_MATCHED_AD_REQUESTS'] = $total[$key]['T_MATCHED_AD_REQUESTS'] / $days;
    //平均CPC
    $total[$key]['AVG_COST_PER_CLICK'] = floatval($total[$key]['T_COST_PER_CLICK'] / $days);
    //平均涵蓋率
    $total[$key]['AVG_AD_REQUESTS_COVERAGE'] = floatval($total[$key]['T_AD_REQUESTS_COVERAGE'] / $days);
    //平均CPM
    $total[$key]['AVG_PAGE_VIEWS_RPM'] = floatval($total[$key]['T_PAGE_VIEWS_RPM'] / $days);
}

foreach ($total as $domain => $data) {
    $separator = str_repeat('=', 10) . PHP_EOL;
    $tday      = $data['TODAY'];
    $str       = "網域:" . $domain . "({$tday['DATE']})" . PHP_EOL;
    $str .= "今日收益:$" . $tday['EARNINGS'] . "USD" . PHP_EOL;
    $str .= "今日瀏覽:" . $tday['PAGE_VIEWS'] . PHP_EOL;
    $str .= "今日廣告曝光量:" . $tday['MATCHED_AD_REQUESTS'] . PHP_EOL;
    $str .= "今日點擊:" . $tday['CLICKS'] . PHP_EOL;
    $str .= "今日CPC:$" . $tday['COST_PER_CLICK'] . PHP_EOL;
    $str .= "今日CPM:$" . $tday['PAGE_VIEWS_RPM'] . PHP_EOL;
    $str .= "今日覆蓋率:" . ($tday['AD_REQUESTS_COVERAGE'] * 100) . "%" . PHP_EOL;
    $str .= $separator . $FMD . " -> " . $TODAY . PHP_EOL . $separator;
    //累計收入
    $str .= "累計收入:$" . $data['T_EARNINGS'];
    $str .= "(平均:$" . round($data['AVG_EARNINGS'], 2) . ")USD" . PHP_EOL;
    //累計瀏覽數
    $str .= "累計瀏覽數:" . $data['T_PAGE_VIEWS'];
    $str .= "(平均:" . round($data['AVG_PAGE_VIEWS'], 2) . ")" . PHP_EOL;
    //累計廣告曝光量
    $str .= "累計廣告曝光量:" . $data['T_MATCHED_AD_REQUESTS'];
    $str .= "(平均:" . round($data['AVG_MATCHED_AD_REQUESTS'], 2) . ")" . PHP_EOL;
    //累計點擊數
    $str .= "累計點擊數:" . $data['T_CLICKS'];
    $str .= "(平均:" . round($data['AVG_CLICKS'], 2) . ")" . PHP_EOL;
    //平均CPC
    // $str .= "平均CPC:".$data['T_COST_PER_CLICK'];
    $str .= "平均CPC:$" . round($data['AVG_COST_PER_CLICK'], 2) . " USD" . PHP_EOL;
    $str .= "平均CPM:$" . round($data['AVG_PAGE_VIEWS_RPM'], 2) . " USD" . PHP_EOL;
    //累計涵蓋率
    // $str .= "".$data['T_AD_REQUESTS_COVERAGE'];
    $str .= "平均涵蓋率:" . round($data['AVG_AD_REQUESTS_COVERAGE'], 2) * 100 . "%" . PHP_EOL;
    $d = new DateTime(date('Y-m-d H:i:s'));
    $d->setTimeZone(new DateTimeZone('Asia/Taipei'));
    //台灣時間1,10,13,19,23才通知
    if (intval($d->format('H')) == 1 ||
        intval($d->format('H')) == 10 ||
        intval($d->format('H')) == 13 ||
        intval($d->format('H')) == 19 ||
        intval($d->format('H')) == 23) {
        line_notify($str);
    }
}

function line_notify($msg) {
    if ($msg == "") {
        return;
    }

    $body = array(
        'message' => PHP_EOL . $msg,
    );
    $headers = array(
        'Content-Type: application/x-www-form-urlencoded',
        'Authorization: Bearer YOUR_LINE_NOTIFY_TOKEN',
    );
    $url = 'https://notify-api.line.me/api/notify';

    $ch = curl_init();

    $params = array(
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => TRUE,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_SSL_VERIFYPEER => TRUE,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13',
        CURLOPT_CUSTOMREQUEST  => 'POST',
        CURLOPT_POSTFIELDS     => http_build_query($body),
    );

    curl_setopt_array($ch, $params);

    if (!$result = curl_exec($ch)) {
        if ($errno = curl_errno($ch)) {
            $error_message = curl_strerror($errno);
            error_log("cURL error ({$errno}):\n {$error_message}");
            curl_close($ch);
            return FALSE;
        }
    } else {
        curl_close($ch);
    }
}

Gist: Link

修改程式使用只要注意環境(PHP 版本大於 5.2 ,有 cURL 模組、安裝 Google Client 客戶端函式庫的路徑)與對應程式內參數的調整(改成自己的),大概不會有什麼問題。

後記

也多虧之前為了要串 Google 分析的資料研究過( [WordPress] 從 Google Analytics 匯入網站人氣的外掛組合技)所以這次很快就上手。

還有 [PHP] 使用 Google Client SDK 串接 Gmail API 發信的方法 這篇也寫過。

另外相對 AdSense 簡單報表需要的資料也不多,真的有很複雜的需求還是得打開網頁版查詢。

上述程式只有第一次授權的情況需要打開瀏覽器,等取得授權後就可以設定主機的 Cron job ,來定時執行。

主機上我設定每小時執行一次,讓他的 Token 固定有更新。然後把傳送通知的方法寫固定某幾個小時才通知。

*/60 * * * * /usr/bin/curl "https://www.mxp.tw/adsense/callback.php" > /dev/null 2>&1


Share:

作者: Chun

資訊愛好人士。主張「人人都該為了偷懶而進步」。期許自己成為斜槓到變進度條 100% 的年輕人。[//////////____30%_________]

參與討論

1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *