Node.js でビッグデータから Google スライド プレゼンテーションを生成

1. 概要

この Codelab では、カスタム プレゼンテーション ツールとして Google スライドを使用して、最も一般的なソフトウェア ライセンスを分析する方法を学習します。BigQuery API を使用して GitHub のオープンソース コードに対してクエリを実行した後、Google Slides API を使用して、結果をプレゼンテーションするためのファイルを作成します。サンプル アプリケーションは Node.js を使って作成しますが、基本的な原則はどのアーキテクチャにも適用できます。

学習内容

  • Slides API を使用してプレゼンテーションを作成する
  • BigQuery を使用して大規模なデータセットから分析情報を得る
  • Google Drive API を使用してファイルをコピーする

必要なもの

  • Node.js がインストールされている
  • インターネット アクセスとウェブブラウザ
  • Google アカウント
  • Google Cloud Platform プロジェクト

2. サンプルコードを取得する

すべてのサンプルコードをパソコンにダウンロードできます。

または、コマンドラインから GitHub リポジトリのクローンを作成する。

git clone https://github.com/googleworkspace/slides-api.git

作業途中のバージョンを参照する必要がある場合に備えて、手順の各ステップに対応するディレクトリがリポジトリに用意されています。

start ディレクトリにあるコピーから作業を始めますが、必要に応じて他の場所のファイルを参照することや、ファイルをコピーしてくることもできます。

3. サンプルアプリを実行します。

まず、Node スクリプトを実行します。コードをダウンロードしたら、以下の手順に沿って Node.js アプリケーションをインストールして起動します。

  1. コンピュータでコマンドライン ターミナルを開き、Codelab の start ディレクトリに移動します。
  2. 次のコマンドを入力して、依存関係のある Node.js のモジュールをインストールします。
npm install
  1. 次のコマンドを入力してスクリプトを実行します。
node .
  1. このプロジェクトの手順を示すメッセージが表示されます。
-- Start generating slides. --
TODO: Get Client Secrets
TODO: Authorize
TODO: Get Data from BigQuery
TODO: Create Slides
TODO: Open Slides
-- Finished generating slides. --

TODO のリストは slides.jslicense.jsauth.js にあります。ここでは、JavaScript Promise を使用して、アプリの実行に必要なステップをチェーン化しています。それぞれのステップは、その前のステップの完了に依存するためです。

Promise について詳しくなくても、必要なコードはすべて用意されているので心配ありません。簡単に言うと、Promise では、非同期の処理を同期処理のように扱うことができます。

4. クライアント シークレットを取得する

スライド、BigQuery、ドライブの API を使用するため、OAuth クライアントとサービス アカウントを作成します。

Google Developers Console をセットアップする

  1. Google Developers Console でこちらのウィザードを使ってプロジェクトを作成するか既存のプロジェクトを選択すると、自動的に API が有効になります。[続行]、[認証情報に進む] の順にクリックします。
  2. [プロジェクトへの認証情報の追加] ページで、[キャンセル] ボタンをクリックします。
  3. ページ上部の [OAuth 同意画面] タブを選択します。[メールアドレス] を選択し、商品名 Slides API Codelab を入力して、[保存] ボタンをクリックします。

BigQuery、ドライブ、スライドの API を有効にする

  1. [ダッシュボード] タブを選択し、[API を有効にする] ボタンをクリックして、次の 3 つの API を有効にします。
  2. BigQuery API
  3. Google Drive API
  4. Google Slides API

OAuth クライアント シークレットのダウンロード(スライドとドライブ用)

  1. [認証情報] タブを選択し、[認証情報を作成] ボタンをクリックして、[OAuth クライアント ID] を選択します。
  2. アプリケーションの種類として [その他] を選択し、名前 Google Slides API Codelab を入力して、[作成] ボタンをクリックします。[OK] をクリックして、表示されたダイアログを閉じます。
  3. クライアント ID の右側にある file_download(JSON をダウンロード)ボタンをクリックします。
  4. シークレット ファイルの名前を client_secret.json に変更し、start/ ディレクトリと finish/ ディレクトリの両方にコピーします。

サービス アカウント シークレットのダウンロード(BigQuery 用)

  1. [認証情報] タブを選択し、[認証情報を作成] ボタンをクリックして、[サービス アカウント キー] を選択します。
  2. プルダウンで [新しいサービス アカウント] を選択します。サービスの名前として Slides API Codelab Service を選択します。[ロール] をクリックし、[BigQuery] までスクロールして、[BigQuery データ閲覧者] と [BigQuery ジョブユーザー] の両方を選択します。
  3. [キーのタイプ] で [JSON] を選択します。
  4. [作成] をクリックします。キーファイルが自動的にパソコンにダウンロードされます。[閉じる] をクリックして、表示されたダイアログを閉じます。
  5. シークレット ファイルの名前を service_account_secret.json に変更し、start/ ディレクトリと finish/ ディレクトリの両方にコピーします。

クライアント シークレットを取得する

start/auth.js で、メソッド getClientSecrets を記述します。

auth.js

const fs = require('fs');

/**
 * Loads client secrets from a local file.
 * @return {Promise} A promise to return the secrets.
 */
module.exports.getClientSecrets = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('client_secret.json', (err, content) => {
      if (err) return reject('Error loading client secret file: ' + err);
      console.log('loaded secrets...');
      resolve(JSON.parse(content));
    });
  });
}

これで、クライアント シークレットが読み込まれました。認証情報が次の Promise に渡されます。node . でプロジェクトを実行して、エラーがないことを確認します。

5. OAuth2 クライアントを作成する

スライドを作成するために、Google API に認証を追加します。それには、次のコードを auth.js ファイルに追加します。この認証では、Google ドライブのファイルの読み取りと書き込み、Google スライドでのプレゼンテーションの作成、および Google BigQuery からの読み取り専用クエリの実行を行うために、ユーザーの Google アカウントへのアクセス許可をリクエストします。(注: getClientSecrets は変更していません)。

auth.js

const fs = require('fs');
const readline = require('readline');
const openurl = require('openurl');
const googleAuth = require('google-auth-library');
const TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
      process.env.USERPROFILE) + '/.credentials/';
const TOKEN_PATH = TOKEN_DIR + 'slides.googleapis.com-nodejs-quickstart.json';

// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/slides.googleapis.com-nodejs-quickstart.json
const SCOPES = [
  'https://www.googleapis.com/auth/presentations', // needed to create slides
  'https://www.googleapis.com/auth/drive', // read and write files
  'https://www.googleapis.com/auth/bigquery.readonly' // needed for bigquery
];

/**
 * Loads client secrets from a local file.
 * @return {Promise} A promise to return the secrets.
 */
module.exports.getClientSecrets = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('client_secret.json', (err, content) => {
      if (err) return reject('Error loading client secret file: ' + err);
      console.log('loaded secrets...');
      resolve(JSON.parse(content));
    });
  });
}

/**
 * Create an OAuth2 client promise with the given credentials.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback for the authorized client.
 * @return {Promise} A promise to return the OAuth client.
 */
module.exports.authorize = (credentials) => {
  return new Promise((resolve, reject) => {
    console.log('authorizing...');
    const clientSecret = credentials.installed.client_secret;
    const clientId = credentials.installed.client_id;
    const redirectUrl = credentials.installed.redirect_uris[0];
    const auth = new googleAuth();
    const oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);

    // Check if we have previously stored a token.
    fs.readFile(TOKEN_PATH, (err, token) => {
      if (err) {
        getNewToken(oauth2Client).then(() => {
          resolve(oauth2Client);
        });
      } else {
        oauth2Client.credentials = JSON.parse(token);
        resolve(oauth2Client);
      }
    });
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * fulfills the promise. Modifies the `oauth2Client` object.
 * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
 * @return {Promise} A promise to modify the oauth2Client credentials.
 */
function getNewToken(oauth2Client) {
  console.log('getting new auth token...');
  openurl.open(oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES
  }));

  console.log(''); // \n
  return new Promise((resolve, reject) => {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
    rl.question('Enter the code from that page here: ', (code) => {
      rl.close();
      oauth2Client.getToken(code, (err, token) => {
        if (err) return reject(err);
        oauth2Client.credentials = token;
        let storeTokenErr = storeToken(token);
        if (storeTokenErr) return reject(storeTokenErr);
        resolve();
      });
    });
  });
}

/**
 * Store token to disk be used in later program executions.
 * @param {Object} token The token to store to disk.
 * @return {Error?} Returns an error or undefined if there is no error.
 */
function storeToken(token) {
  try {
    fs.mkdirSync(TOKEN_DIR);
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
  } catch (err) {
    if (err.code != 'EEXIST') return err;
  }
  console.log('Token stored to ' + TOKEN_PATH);
}

6. BigQuery をセットアップする

BigQuery を調べる(省略可)

BigQuery では、大規模なデータセットに対するクエリを数秒で実行できます。プログラムでクエリを実行する前に、ウェブ インターフェースから実行してみます。BigQuery をまだ設定したことがない場合は、こちらのクイックスタートの手順に沿って設定してください。

Cloud コンソールを開き、BigQuery で利用可能な GitHub データを参照して、独自のクエリを実行します。GitHub で最も使用されているソフトウェア ライセンスを調べてみましょう。次のようにクエリを記述し、[実行] ボタンをクリックします。

bigquery.sql

WITH AllLicenses AS (
  SELECT * FROM `bigquery-public-data.github_repos.licenses`
)
SELECT
  license,
  COUNT(*) AS count,
  ROUND((COUNT(*) / (SELECT COUNT(*) FROM AllLicenses)) * 100, 2) AS percent
FROM `bigquery-public-data.github_repos.licenses`
GROUP BY license
ORDER BY count DESC
LIMIT 10

数百万件に及ぶ GitHub の公開リポジトリを分析し、よく使用されているライセンスがわかりました。分析がうまくいったようです。次に、同じクエリをプログラムから実行してみましょう。

BigQuery をセットアップする

license.js ファイルのコードを置き換えます。関数 bigquery.queryPromise を返します。

license**.js**

const google = require('googleapis');
const read = require('read-file');
const BigQuery = require('@google-cloud/bigquery');
const bigquery = BigQuery({
  credentials: require('./service_account_secret.json')
});

// See codelab for other queries.
const query = `
WITH AllLicenses AS (
  SELECT * FROM \`bigquery-public-data.github_repos.licenses\`
)
SELECT
  license,
  COUNT(*) AS count,
  ROUND((COUNT(*) / (SELECT COUNT(*) FROM AllLicenses)) * 100, 2) AS percent
FROM \`bigquery-public-data.github_repos.licenses\`
GROUP BY license
ORDER BY count DESC
LIMIT 10
`;

/**
 * Get the license data from BigQuery and our license data.
 * @return {Promise} A promise to return an object of licenses keyed by name.
 */
module.exports.getLicenseData = (auth) => {
  console.log('querying BigQuery...');
  return bigquery.query({
    query,
    useLegacySql: false,
    useQueryCache: true,
  }).then(bqData => Promise.all(bqData[0].map(getLicenseText)))
    .then(licenseData => new Promise((resolve, reject) => {
      resolve([auth, licenseData]);
    }))
    .catch((err) => console.error('BigQuery error:', err));
}

/**
 * Gets a promise to get the license text about a license
 * @param {object} licenseDatum An object with the license's
 *   `license`, `count`, and `percent`
 * @return {Promise} A promise to return license data with license text.
 */
function getLicenseText(licenseDatum) {
  const licenseName = licenseDatum.license;
  return new Promise((resolve, reject) => {
    read(`licenses/${licenseName}.txt`, 'utf8', (err, buffer) => {
      if (err) return reject(err);
      resolve({
        licenseName,
        count: licenseDatum.count,
        percent: licenseDatum.percent,
        license: buffer.substring(0, 1200) // first 1200 characters
      });
    });
  });
}

Promise のコールバックで使用されるデータの一部を console.log して、オブジェクトの構造とコードの実際の動作を確認してみましょう。

7. スライドを作成する

ここからが面白いところです。Slides API の create メソッドと batchUpdate メソッドを呼び出して、スライドを作成しましょう。ファイルは次のように置き換えます。

slides.js

const google = require('googleapis');
const slides = google.slides('v1');
const drive = google.drive('v3');
const openurl = require('openurl');
const commaNumber = require('comma-number');

const SLIDE_TITLE_TEXT = 'Open Source Licenses Analysis';

/**
 * Get a single slide json request
 * @param {object} licenseData data about the license
 * @param {object} index the slide index
 * @return {object} The json for the Slides API
 * @example licenseData: {
 *            "licenseName": "mit",
 *            "percent": "12.5",
 *            "count": "1667029"
 *            license:"<body>"
 *          }
 * @example index: 3
 */
function createSlideJSON(licenseData, index) {
  // Then update the slides.
  const ID_TITLE_SLIDE = 'id_title_slide';
  const ID_TITLE_SLIDE_TITLE = 'id_title_slide_title';
  const ID_TITLE_SLIDE_BODY = 'id_title_slide_body';

  return [{
    // Creates a "TITLE_AND_BODY" slide with objectId references
    createSlide: {
      objectId: `${ID_TITLE_SLIDE}_${index}`,
      slideLayoutReference: {
        predefinedLayout: 'TITLE_AND_BODY'
      },
      placeholderIdMappings: [{
        layoutPlaceholder: {
          type: 'TITLE'
        },
        objectId: `${ID_TITLE_SLIDE_TITLE}_${index}`
      }, {
        layoutPlaceholder: {
          type: 'BODY'
        },
        objectId: `${ID_TITLE_SLIDE_BODY}_${index}`
      }]
    }
  }, {
    // Inserts the license name, percent, and count in the title
    insertText: {
      objectId: `${ID_TITLE_SLIDE_TITLE}_${index}`,
      text: `#${index + 1} ${licenseData.licenseName}  — ~${licenseData.percent}% (${commaNumber(licenseData.count)} repos)`
    }
  }, {
    // Inserts the license in the text body paragraph
    insertText: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      text: licenseData.license
    }
  }, {
    // Formats the slide paragraph's font
    updateParagraphStyle: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      fields: '*',
      style: {
        lineSpacing: 10,
        spaceAbove: {magnitude: 0, unit: 'PT'},
        spaceBelow: {magnitude: 0, unit: 'PT'},
      }
    }
  }, {
    // Formats the slide text style
    updateTextStyle: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      style: {
        bold: true,
        italic: true,
        fontSize: {
          magnitude: 10,
          unit: 'PT'
        }
      },
      fields: '*',
    }
  }];
}

/**
 * Creates slides for our presentation.
 * @param {authAndGHData} An array with our Auth object and the GitHub data.
 * @return {Promise} A promise to return a new presentation.
 * @see https://developers.google.com/apis-explorer/#p/slides/v1/
 */
module.exports.createSlides = (authAndGHData) => new Promise((resolve, reject) => {
  console.log('creating slides...');
  const [auth, ghData] = authAndGHData;

  // First copy the template slide from drive.
  drive.files.copy({
    auth: auth,
    fileId: '1toV2zL0PrXJOfFJU-NYDKbPx9W0C4I-I8iT85TS0fik',
    fields: 'id,name,webViewLink',
    resource: {
      name: SLIDE_TITLE_TEXT
    }
  }, (err, presentation) => {
    if (err) return reject(err);

    const allSlides = ghData.map((data, index) => createSlideJSON(data, index));
    slideRequests = [].concat.apply([], allSlides); // flatten the slide requests
    slideRequests.push({
      replaceAllText: {
        replaceText: SLIDE_TITLE_TEXT,
        containsText: { text: '{{TITLE}}' }
      }
    })

    // Execute the requests
    slides.presentations.batchUpdate({
      auth: auth,
      presentationId: presentation.id,
      resource: {
        requests: slideRequests
      }
    }, (err, res) => {
      if (err) {
        reject(err);
      } else {
        resolve(presentation);
      }
    });
  });
});

8. スライドを開く

最後に、ブラウザでプレゼンテーションを開きます。slides.js で次のメソッドを更新します。

slides.js

/**
 * Opens a presentation in a browser.
 * @param {String} presentation The presentation object.
 */
module.exports.openSlidesInBrowser = (presentation) => {
  console.log('Presentation URL:', presentation.webViewLink);
  openurl.open(presentation.webViewLink);
}

プロジェクトをもう一度実行して、最終結果を表示します。

9. 完了

BigQuery を使用して分析したデータから、Google スライドを生成できました。今回は、スクリプトで Google Slides API と BigQuery を使用して、最もよく使われているソフトウェア ライセンスの分析結果を報告するプレゼンテーションを作成しました。

改善の可能性

さらに魅力的な統合を行うためのアイデアを紹介します。

  • 各スライドに画像を追加する
  • Gmail API を使用してスライドをメールで共有する
  • テンプレートのスライドをコマンドライン引数としてカスタマイズする

詳細