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

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

בקודלאב הזה תלמדו איך להשתמש ב-Google Slides ככלי להצגת מצגות בהתאמה אישית, לצורך ניתוח של רישיונות התוכנה הנפוצים ביותר. תשלחו שאילתה לכל הקוד בקוד פתוח ב-GitHub באמצעות BigQuery API ותיצרו מצגת באמצעות 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. פותחים טרמינל של שורת פקודה במחשב ועוברים לספרייה 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.js, ב-license.js וב-auth.js. חשוב לדעת שאנחנו משתמשים ב-JavaScript Promises כדי לקשר את השלבים הנדרשים להשלמת האפליקציה, כי כל שלב תלוי בהשלמת השלב הקודם.

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

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

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

הגדרת Google Developers Console

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

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

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

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

  1. בוחרים בכרטיסייה Credentials, לוחצים על הלחצן Create credentials ובוחרים באפשרות OAuth client ID.
  2. בוחרים את סוג האפליקציה אחר, מזינים את השם 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) וגם באפשרות BigQuery Job User (משתמש ב-BigQuery Job).
  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 API על ידי הוספת הקוד הבא לקובץ 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 תחזיר 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 חלק מהנתונים בתוך הפונקציה הלא סטטית (callback) של Promise כדי להבין את המבנה של האובייקטים שלנו ולראות איך הקוד פועל.

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

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

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

ריכזנו כאן כמה רעיונות נוספים לשילוב אפקטיבי יותר:

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

מידע נוסף