יצירת מצגות Google Slides מ-Big Data ב-Node.js

1. סקירה כללית

ב-Codelab הזה תלמדו איך להשתמש ב-Google Slides ככלי מותאם אישית למצגות, כדי לנתח את רישיונות התוכנה הנפוצים ביותר. כשמריצים שאילתות על כל קוד הקוד הפתוח ב-GitHub באמצעות BigQuery API, יוצרים מצגת באמצעות Google Slides API כדי להציג את התוצאות. האפליקציה לדוגמה נוצרת באמצעות Node.js, אבל אותם עקרונות בסיסיים חלים על כל ארכיטקטורה.

מה תלמדו

  • יצירת מצגות באמצעות ה-API של Slides
  • שימוש ב-BigQuery כדי לקבל תובנות על מערך נתונים גדול
  • העתקת קובץ באמצעות Google Drive API

למה תזדקק?

  • Node.js הותקן
  • גישה לאינטרנט ולדפדפן אינטרנט
  • חשבון Google
  • פרויקט ב-Google Cloud Platform

2. לקבלת הקוד לדוגמה

אפשר להוריד את כל הקוד לדוגמה למחשב...

...או משכפלים את המאגר של GitHub משורת הפקודה.

git clone https://github.com/googleworkspace/slides-api.git

המאגר מכיל קבוצת ספריות שמייצגת כל שלב בתהליך, למקרה שתצטרכו להפנות לגרסה פעילה.

המערכת תסגור את העותק שנמצא בספרייה start, אבל אפשר להפנות לקבצים האחרים או להעתיק מהם קבצים לפי הצורך.

3. הרצת האפליקציה לדוגמה

תחילה נפעיל את הסקריפט של הצומת. לאחר הורדת הקוד, פועלים לפי ההוראות הבאות כדי להתקין ולהפעיל את אפליקציית Node.js:

  1. פותחים טרמינל של שורת הפקודה במחשב ועוברים לספריית start של Codelab.
  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.js, ב-license.js וב-auth.js. שימו לב שאנחנו משתמשים ב-JavaScript Promises (הבטחות JavaScript) כדי לשרשר את השלבים הדרושים להשלמת האפליקציה, כי כל שלב תלוי בשלב הקודם שהשלמתם.

אם אינך יודע מה יהיו ההבטחות, אל דאגה, נספק לך את כל הקוד שדרוש לך. בקיצור, הבטחות מאפשרות לנו לטפל בעיבוד אסינכרוני בצורה אסינכרונית יותר.

4. אחזור סודות לקוח

כדי להשתמש בממשקי ה-API של Slides , BigQuery ו-Drive, ניצור לקוח OAuth וחשבון שירות.

הגדרת Google Developers Console

  1. משתמשים באשף הזה כדי ליצור או לבחור פרויקט ב-Google Developers Console ולהפעיל באופן אוטומטי את ה-API. לוחצים על Continue (המשך) ואז על Go to credentials (מעבר לפרטי הכניסה).
  2. בדף Add credentials to your project, לוחצים על הלחצן Cancel.
  3. בחלק העליון של הדף, לוחצים על הכרטיסייה OAuth screen consent (מסך ההסכמה של OAuth). בוחרים כתובת אימייל, מזינים את שם המוצר Slides API Codelab ולוחצים על הלחצן שמירה.

הפעלת ממשקי ה-API של BigQuery, Drive ו-Slides

  1. בוחרים בכרטיסייה מרכז שליטה, לוחצים על הלחצן Enable API ומפעילים את 3 ממשקי ה-API הבאים:
  2. BigQuery API
  3. Google Drive API
  4. API של Google Slides

הורדת סוד לקוח של OAuth (ל-Slides ו-Drive)

  1. בוחרים בכרטיסייה Credentials, לוחצים על Create credentials (יצירת פרטי כניסה) ובוחרים באפשרות OAuth client ID (מזהה לקוח OAuth).
  2. בוחרים את סוג האפליקציה Other, מזינים את השם Google Slides API Codelab ולוחצים על הלחצן Create (יצירה). לוחצים על OK כדי לסגור את תיבת הדו-שיח שמופיעה.
  3. לוחצים על הלחצן file_download (הורדת JSON) משמאל למזהה הלקוח.
  4. משנים את השם של הקובץ הסודי ל-client_secret.json ומעתיקים אותו לספרייה start/ (התחלה) ו-Finish/ (סיום).

הורדת סוד של חשבון שירות (ל-BigQuery)

  1. בוחרים בכרטיסייה Credentials, לוחצים על הלחצן Create credentials ואז בוחרים באפשרות Service account key.
  2. בתפריט הנפתח, בוחרים באפשרות New Service Account (חשבון שירות חדש). צריך לבחור את השם Slides API Codelab Service לשירות. לאחר מכן לוחצים על Role וגוללים אל BigQuery ובוחרים ב-BigQuery Data Viewer וב-BigQuery Job User.
  3. בשדה Key type, בוחרים באפשרות JSON.
  4. לוחצים על יצירה. תתבצע הורדה אוטומטית של קובץ המפתח למחשב שלכם. לוחצים על Close כדי לצאת מתיבת הדו-שיח שמופיעה.
  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

כדי ליצור שקפים, צריך להוסיף אימות ל-Google APIs על ידי הוספת הקוד הבא לקובץ auth.js. האימות יבקש גישה לחשבון Google שלכם כדי לקרוא ולכתוב קבצים ב-Google Drive, ליצור מצגות ב-Google Slides ולהפעיל שאילתות לקריאה בלבד מ-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 כדי לעיין בנתוני GitHub שזמינים ב-BigQuery ולהריץ שאילתות משלכם. כדי למצוא את רישיונות התוכנה הפופולריים ביותר ב-GitHub, כותבים את השאילתה הזאת ולוחצים על Run.

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 תחזיר promis.

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 חלק מהנתונים בקריאה החוזרת (callback) של Promise שלנו, כדי להבין את מבנה האובייקטים שלנו ולראות את הקוד בפעולה.

7. יצירת שקפים

עכשיו לחלק הכיפי! בואו ליצור שקפים על ידי קריאה ל-methods create ו-batchUpdate של Slides API. צריך להחליף את הקובץ שלנו בניסוח הזה:

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

לסיום, נפתח את המצגת בדפדפן. אפשר לעדכן את השיטה הבאה ב-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. מעולה!

יצרתם בהצלחה Google Slides מנתונים שנותחו באמצעות BigQuery. הסקריפט יוצר מצגת באמצעות ה-API של Google Slides ו-BigQuery כדי לדווח על ניתוח של רישיונות התוכנה הנפוצים ביותר.

שיפורים אפשריים

הנה כמה רעיונות נוספים ליצירת שילוב מושך עוד יותר:

  • להוסיף תמונות לכל שקף
  • לשתף את השקפים באימייל באמצעות ממשק ה-API של Gmail
  • התאמה אישית של שקף התבנית כארגומנט בשורת הפקודה

מידע נוסף