在 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. --

您可以在 slides.jslicense.jsauth.js 中查看我们的待办事项列表。请注意,我们使用 JavaScript Promise串联完成应用所需的步骤,因为每个步骤都依赖于前一个步骤的完成。

如果您不熟悉 Promise,也不用担心,我们会提供您所需的所有代码。简而言之,Promise 提供了一种以更同步的方式处理异步处理的方法。

4. 获取客户端密钥

为了使用 Slides、Bigquery 和 Drive API,我们将创建一个 OAuth 客户端和服务账号。

设置 Google Developers Console

  1. 使用此向导在 Google Developers 控制台中创建或选择项目,并自动开启 API。依次点击继续前往凭据
  2. 向项目添加凭据页面上,点击取消按钮。
  3. 在页面顶部,选择 OAuth 权限请求页面标签页。选择一个电子邮件地址,输入商品名称 Slides API Codelab,然后点击保存按钮。

启用 BigQuery、云端硬盘和 Slides API

  1. 选择信息中心标签页,点击启用 API 按钮,然后启用以下 3 个 API:
  2. BigQuery API
  3. Google Drive API
  4. Google Slides API

下载 OAuth 客户端密钥(适用于 Google 幻灯片和 Google 云端硬盘)

  1. 选择凭据标签页,点击创建凭据按钮,然后选择 OAuth 客户端 ID
  2. 选择应用类型其他,输入名称 Google Slides API Codelab,然后点击创建按钮。点击确定以关闭显示的对话框。
  3. 点击客户端 ID 右侧的 file_download(下载 JSON)按钮。
  4. 将您的密文文件重命名为 client_secret.json,并将其复制到 start/finish/ 目录中。

下载服务账号 Secret(适用于 BigQuery)

  1. 选择凭据标签页,点击创建凭据按钮,然后选择服务账号密钥
  2. 在下拉菜单中,选择新建服务账号。为您的服务选择名称 Slides API Codelab Service。然后,点击角色,滚动到 BigQuery,然后同时选择 BigQuery Data ViewerBigQuery Job User
  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 幻灯片中创建演示文稿,以及从 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
      });
    });
  });
}

尝试对 Promise 回调中的部分数据进行 console.log,以了解对象的结构并查看代码的实际运行效果。

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. 打开 Google 幻灯片

最后,让我们在浏览器中打开演示文稿。更新 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 通过电子邮件分享幻灯片
  • 以命令行实参的形式自定义模板幻灯片

了解详情