在 Node.js 中運用大數據產生 Google 簡報檔案

1. 總覽

在本程式碼研究室中,您將瞭解如何使用 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. 在電腦上開啟指令列終端機,然後前往程式碼研究室的 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. --

您可以在 slides.jslicense.jsauth.js 中查看我們的 TODO 清單。請注意,我們之所以使用 JavaScript Promise,來鏈結完成應用程式所需的步驟,因為每個步驟都取決於上一個步驟。

如果您不熟悉承諾內容,也請別擔心,我們會提供所有您需要的程式碼。簡單來說,保證讓我們能以更同步的方式處理非同步處理作業。

4. 取得用戶端密鑰

如要使用 Slides、BigQuery 和 Drive API,我們會建立 OAuth 用戶端和服務帳戶。

設定 Google Developers Console

  1. 使用這個精靈在 Google Developers Console 中建立或選取專案,並自動啟用 API。依序點選「繼續」和「前往憑證」
  2. 在「Add credentials to your project」(新增憑證至專案) 頁面,按一下「Cancel」(取消) 按鈕。
  3. 選取頁面頂端的「OAuth 同意畫面」分頁標籤。選取「電子郵件地址」並輸入產品名稱 Slides API Codelab,然後按一下「儲存」按鈕

啟用 BigQuery、Drive 和 Slides API

  1. 選取「Dashboard」分頁標籤,然後按一下「Enable API」按鈕,並啟用下列 3 個 API:
  2. BigQuery API
  3. Google Drive API
  4. Google Slides API

下載 OAuth 用戶端密鑰 (適用於 Google 簡報和雲端硬碟)

  1. 選取「Credentials」(憑證) 分頁標籤,按一下「Create credentials」(建立憑證) 按鈕,然後選取「OAuth client ID」(OAuth 用戶端 ID)
  2. 選取「Other」應用程式類型,輸入名稱 Google Slides API Codelab,然後按一下「Create」按鈕。按一下「OK」,關閉顯示的對話方塊。
  3. 按一下用戶端 ID 右側的 file_download (Download JSON) 按鈕。
  4. 將密鑰檔案重新命名為 client_secret.json,然後複製到 start/finish/ 目錄中。

下載服務帳戶密鑰 (適用於 BigQuery)

  1. 選取「憑證」分頁標籤,按一下「建立憑證」按鈕,然後選取「服務帳戶金鑰」
  2. 在下拉式選單中,選取「New Service Account」。為服務選擇名稱「Slides API Codelab Service」。接著,按一下「Role」(角色) 並捲動至「BigQuery」,然後選取「BigQuery Data Viewer」(BigQuery 資料檢視者) 和「BigQuery Job User」(BigQuery 工作使用者)
  3. 在「Key type」(金鑰類型) 部分,選取「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));
    });
  });
}

我們現在已載入用戶端密鑰。憑證會傳遞至下一個承諾。使用「node .」執行專案,確保沒有錯誤。

5. 建立 OAuth2 用戶端

為了製作投影片,我們將下列程式碼新增至 auth.js 檔案,以新增驗證至 Google API。這項驗證程序會要求存取您的 Google 帳戶,以便讀取及寫入 Google 雲端硬碟中的檔案、在 Google 簡報中建立簡報,以及執行 Google BigQuery 的唯讀查詢。(注意:我們並未變更 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 資料並執行自己的查詢。我們會編寫這項查詢並按下「Run」按鈕,找出 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.query 函式會傳回「promise」

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
      });
    });
  });
}

嘗試console.log Promise 回呼內的部分資料,以瞭解物件的結構,看看程式碼的實際運作情形。

7. 建立簡報

現在來到有趣的部分吧!現在呼叫 Slides API 的 createbatchUpdate 方法來建立投影片。檔案應替換為下列內容:

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 透過電子郵件分享簡報
  • 將範本投影片自訂為指令列引數

瞭解詳情