在 Node.js 中基于大数据生成 Google 幻灯片演示文稿

1. 概览

在此 Codelab 中,您将了解如何将 Google 幻灯片用作自定义演示工具,以分析最常见的软件许可。您将使用 BigQuery API 查询 GitHub 上的所有 开源代码,并使用 Google Slides API 创建 Google 幻灯片演示文稿来展示结果。示例应用是使用 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 Console 中创建或选择项目,并自动启用 API。依次点击继续转到凭据
  2. 向项目添加凭据 页面上,点击取消 按钮。
  3. 在页面顶部,选择 OAuth 权限请求页面 标签页。选择一个电子邮件地址 ,输入商品名称 Slides API Codelab,然后点击保存 按钮。

启用 BigQuery、Drive 和 Slides API

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

下载 OAuth 客户端密钥(适用于 Slides 和 Drive)

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

下载服务账号密钥(适用于 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 客户端

如需创建幻灯片,请将以下代码添加到 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 可让我们在几秒钟内查询海量数据集。在以编程方式查询之前,我们先使用 Web 界面。如果您之前从未设置过 BigQuery,请按照此快速入门中的步骤操作

打开 Cloud Console,浏览 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.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 幻灯片 API 和 BigQuery 创建演示文稿,以报告对最常见软件许可的分析。

可能的改进

以下是一些其他想法,可让集成更具吸引力:

  • 向每张幻灯片添加图片
  • 使用 Gmail API 通过电子邮件共享幻灯片
  • 将模板幻灯片自定义为命令行实参

了解详情