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

1. 概览

在此 Codelab 中,您将学习如何将 Google 幻灯片用作自定义演示文稿工具,以分析最常见的软件许可。您将使用 BigQuery API 查询 GitHub 上的所有开源代码,并使用 Google Sheets API 创建一个演示文稿来演示结果。虽然示例应用是使用 Node.js 构建的,但相同的基本原则适用于所有架构。

学习内容

  • 使用幻灯片 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. 获取客户端密钥

为了使用幻灯片、BigQuery 和云端硬盘 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. 选择信息中心标签页,然后点击启用 API 按钮并启用以下 3 个 API:
  2. BigQuery API
  3. Google Drive API
  4. Google 幻灯片 API

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

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

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

  1. 选择凭据标签页,点击创建凭据按钮,然后选择服务账号密钥
  2. 在下拉菜单中,选择新建服务账号。为您的服务选择名称 Slides API Codelab Service。然后点击角色并滚动到 BigQuery,同时选择 BigQuery Data ViewerBigQuery Job User
  3. 对于密钥类型,选择 JSON
  4. 点击创建。密钥文件会自动下载到您的计算机中。点击关闭,退出显示的对话框。
  5. 将 Secret 文件重命名为 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 让我们能够在几秒钟内查询海量数据集。在以编程方式查询之前,我们先使用网页界面。如果您之前从未设置过 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. 创建幻灯片

接下来到了充满乐趣的部分了!我们通过调用幻灯片 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 Sheets API 和 BigQuery 创建一个演示文稿,以报告对最常见的软件许可的分析。

可能的改进措施

您可以参考以下几点其他建议,了解如何让集成更具吸引力:

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

了解详情