在 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 中查看我们的 TODO 列表。请注意,由于每个步骤都依赖于完成的上一步,因此我们使用 JavaScript Promise串联完成应用所需的步骤。

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

4. 获取客户端密钥

如需使用 Google 幻灯片、BigQuery 和云端硬盘 API,我们将创建一个 OAuth 客户端和一个服务账号。

设置 Google 管理中心

  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. 选择应用类型 Other,输入名称 Google Slides API Codelab,然后点击 Create 按钮。点击 OK 关闭随即显示的对话框。
  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 控制台,浏览 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. 打开 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 通过电子邮件分享幻灯片
  • 将模板幻灯片自定义为命令行参数

了解详情