📀

Google Drive/Slides API さわってみた

に公開

1. はじめに

社内メンバーと一緒に提案書自動生成アプリの開発を試みた際、Google Drive API や Google Slide API を使って、Google Drive上のファイルの読み書きやGoogleスライドの作成や編集を行ったことがありました。

コーディングAIにほとんど書かせていましたが、意外と引っ掛かってばかりだったので、備忘録として残しておきます。

なお、アプリ開発については以下の記事をご覧ください。(執筆中)

https://zenn.dev/ncdc/articles/d0518375c583db

2. Google Drive API 等の認証について

最初に必要となるのがOAuth2.0認証です。この章では、認証の仕組みから実装方法まで、実際のコード例を交えながら解説します。

OAuth2.0の簡単な流れ

セキュリティ確保のため、OAuth2.0という認証プロトコルが使用されています。簡単に言うと、以下のような流れで認証が行われます:

  1. アプリケーションがGoogleに認証リクエストを送る
  2. ユーザーがブラウザでGoogleアカウントにログインし、アプリケーションへの権限を許可
  3. Googleがアクセストークンとリフレッシュトークンを発行
  4. アプリケーションがこのトークンを使ってAPIにアクセス

重要なポイントは、アプリケーションがユーザーのパスワードを直接扱わないことです。代わりに、トークンという一時的な認証情報を使用します。

環境構築

プロジェクトをセットアップしておきます。

# プロジェクトディレクトリを作成して移動
mkdir google-api-project
cd google-api-project

package.jsonを作成

package.json
{
  "name": "google-api-project",
  "version": "1.0.0",
  "type": "module",
  "scripts": {},
  "devDependencies": {}
}
# TypeScriptとts-nodeをインストール
$ npm install -D typescript tsx @types/node

# TypeScriptの設定ファイルを作成
$ npx tsc --init

# 必要なパッケージをインストール
$ npm install googleapis @google-cloud/local-auth google-auth-library

認証情報の取得方法(Google Cloud Console)

Google Drive API 等を使うには、まずGoogle Cloud Consoleで認証情報を作成する必要があります。

手順

  1. Google Cloud Consoleにアクセス

  2. プロジェクトを作成

    • 新しいプロジェクトを作成(または既存のプロジェクトを選択)
  3. APIを有効化

    • 「APIとサービス」→「ライブラリ」から以下のAPIを検索して有効化:
      • Google Drive API
      • Google Slides API
  4. OAuth同意画面を設定

    • 「APIとサービス」→「OAuth同意画面」
    • ユーザータイプを選択(内部)
    • アプリケーション名などの基本情報を入力
  5. 認証情報を作成

    • 「APIとサービス」→「認証情報」→「認証情報を作成」
    • 「OAuthクライアントID」を選択
    • アプリケーションの種類:「デスクトップアプリ」
    • 作成後、JSONファイルをダウンロード(これがcredentials.json

トークンの取得と保存

認証情報(credentials.json)を取得したら、次は実際にトークンを取得してみましょう。

ディレクトリ構成

google-api-project/
├── package.json
├── package-lock.json
├── tsconfig.json
├── credentials.json  # Google Cloud Consoleからダウンロードした認証情報
├── token.json        # 生成されるトークンファイル(初回実行後)
├── getToken.ts       # トークン取得スクリプト(この章で作成)
├── utils.ts          # トークン読み込み関数(この章で作成、全章で使用)
├── searchAndReadPdfFiles.ts  #(3章で作成)
├── createPresentationInFolder.ts  #(4章で作成)
└── createPresentation.ts  #(4章で作成)

トークン取得スクリプトの実装

getToken.ts
import { promises as fs } from "fs";
import type { Credentials } from "google-auth-library";
import { authenticate } from "@google-cloud/local-auth";

// アプリケーションがアクセスできる範囲を定義。
// 必要最小限のスコープを指定することがセキュリティのベストプラクティスだが
// 本記事全体では簡単のために全権限を付与。
const SCOPES = [
  "https://www.googleapis.com/auth/drive", // Google Drive の全権限
  "https://www.googleapis.com/auth/presentations" // Google Slides の全権限
];

// 認証情報のファイルパス(プロジェクトルートに配置)
const CREDENTIALS_PATH = "./credentials.json";
const TOKEN_PATH = "./token.json";

/**
 * 認証情報を保存する
 * 認証情報のファイルはtoken.jsonに保存する
 */
async function saveCredentials(credentials: Credentials): Promise<void> {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content.toString());
  const key = keys.installed; // OAuthクライアントはデスクトップアプリ設定の想定
  const payload = JSON.stringify({
    type: "authorized_user",
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
  console.log("Token saved to:", TOKEN_PATH);
}

/**
 * 認証情報のファイルを使用してOAuth2認証を行う
 * 認証情報のファイルはcredentials.jsonを使用する
 * 認証情報のファイルはtoken.jsonに保存する
 */
async function main(): Promise<void> {
  console.log("新しい認証を開始します...");
  console.log("ブラウザが自動で開きます。認証を完了してください。");

  // 認証情報のファイルを使用してOAuth2認証を行い、トークンを取得する
  const client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });

  // 認証情報
  const credentials = client.credentials;

  // 認証情報が存在する場合は保存する
  if (credentials) {
    console.log("認証が完了しました。トークンを保存します...");
    await saveCredentials(credentials);
  } else {
    console.log("認証に失敗しました - credentialsが空です");
  }
}

main();

実行方法

# トークン取得スクリプトを実行
$ npx tsx getToken.ts

実行すると、自動的にブラウザが開き、Googleアカウントでのログインと権限の許可を求められます。許可すると、token.jsonが生成されます。

なお、credentials.jsonとtoken.jsonには機密情報が含まれているため、Gitのコミットや公開リポジトリへのアップロードは絶対にしないようにしてください。

保存したトークンの読み込み

一度トークンを取得したら、次回以降はtoken.jsonを読み込むだけでAPIを使用できます。

utils.ts
import { promises as fs } from "fs";
import { google, Auth } from "googleapis";

// 認証情報のファイルパス(プロジェクトルートに配置)
const TOKEN_PATH = "./token.json";

/**
 * token.jsonを読み込む
 */
export async function loadSavedCredentials(): Promise<Auth.OAuth2Client> {
    try {
        const content = await fs.readFile(TOKEN_PATH, "utf-8");
        const credentials = JSON.parse(content);
        console.log("トークンを読み込みました");
        return google.auth.fromJSON(credentials) as Auth.OAuth2Client;
    } catch (err) {
        throw new Error("トークンが見つかりません。新しい認証が必要です。");
    }
}

使用イメージ

import { loadSavedCredentials } from "./utils";
import { google } from "googleapis";

async function useDriveAPI() {
  // 保存したトークンを読み込む
  const auth = await loadSavedCredentials();

  // Drive APIクライアントを初期化
  const drive = google.drive({ version: "v3", auth });

  // APIを使用する
  const res = await drive.files.list({
    pageSize: 10,
  });

  console.log("Files:", res.data.files);
}

3. Google Drive API

この章では、Google Drive APIを使ったファイル操作をまとめています。PDFファイルの検索とダウンロード処理を中心に、共有ドライブの扱い方についても触れています。

3.1 PDFファイルの検索とダウンロード

以下は共有ドライブからPDFファイルを検索してダウンロードする実装の例です。

searchAndReadPdfFiles.ts
import { google, Auth } from "googleapis";
import { Readable } from "stream";
import { loadSavedCredentials } from "./utils.js";

// 共有ドライブID(https://drive.google.com/drive/folders/XXXXX のXXXXXがID)
const FOLDER_ID = "your_shared_drive_id_here";

// 検索キーワード
const SEARCH_TERM = "検索したいキーワード";

/**
 * PDFファイルを検索してダウンロードする
 */
async function searchAndReadPdfFiles(
  oAuth2Client: Auth.OAuth2Client,
  searchTerm: string
): Promise<Buffer[]> {
  // 認証済みのOAuth2クライアントを使用してDrive APIを初期化
  const drive = google.drive({ version: "v3", auth: oAuth2Client });

  // PDFファイルを検索するクエリ
  // ファイル名または内容に searchTerm を含むPDFを検索。
  // `fullText`での検索は、テキストベースのPDFでのみ機能する。
  // 画像のみのPDFではテキストが抽出できないため、検索にヒットしないようだ。
  const query = `
    trashed = false and
    mimeType="application/pdf" and
    (name contains "${searchTerm}" or fullText contains "${searchTerm}")
  `;

  // 共有ドライブ対応のファイル検索
  const response = await drive.files.list({
    q: query,  // 検索条件
    pageSize: 5,  // 取得ファイル件数
    fields: "nextPageToken, files(id, name, mimeType, createdTime, size)",

    // --- 共有ドライブ対応の必須フラグ ---
    // 検索対象を「特定の共有ドライブ内のみ」に限定
    // 他の選択肢: "user"(マイドライブのみ)、"allDrives"(すべてのドライブ)
    corpora: "drive",

    // 共有ドライブ内のアイテムを検索結果に含める(デフォルトは false)
    includeItemsFromAllDrives: true,

    // 共有ドライブを使うことを明示(共有ドライブのファイル操作時は必須)
    supportsAllDrives: true,

    // 検索対象の共有ドライブID(corpora: "drive" の場合は必須)
    driveId: FOLDER_ID,
  });

  const files = response.data.files;
  const bufferList: Buffer[] = [];

  if (!files || files.length === 0) {
    console.log(`検索語 "${searchTerm}" に一致するPDFファイルが見つかりませんでした。`);
    return bufferList;
  }

  console.log(`検索結果: ${files.length}件のPDFファイルが見つかりました`);

  // 各ファイルをダウンロード
  for (const file of files) {
    if (file.id && file.mimeType === "application/pdf") {
      try {
        console.log(`PDFファイルをダウンロード中: ${file.name}`);

        // ストリーム形式でファイルをダウンロード
        const downloadResponse = await drive.files.get({
          fileId: file.id,
          alt: "media",  // メタデータではなく実際のコンテンツを取得
          supportsAllDrives: true,  // 共有ドライブ対応
        }, {
          // 大きなファイルをダウンロードする際は、
          // ストリームを使うことで一度にすべてをメモリに読み込まず、
          // 小さな塊ごとに処理できる。
          // これによりメモリ不足を防げる。
          responseType: "stream"  // ストリームとして受け取る(メモリ効率向上)
        });

        // ストリームをバッファに変換
        const chunks: Buffer[] = [];
        const stream = downloadResponse.data as Readable;

        for await (const chunk of stream) {
          chunks.push(chunk);
        }

        bufferList.push(Buffer.concat(chunks));
      } catch (error) {
        console.error(`ダウンロードエラー (${file.name}):`, error);
      }
    }
  }

  return bufferList;
}

/**
 * メイン処理
 */
async function main() {
  // OAuth2クライアントを取得(utils.tsのloadSavedCredentials関数を使用)
  const oAuth2Client = await loadSavedCredentials();

  // PDFファイルを検索してダウンロード
  const buffers = await searchAndReadPdfFiles(oAuth2Client, SEARCH_TERM);

  console.log(`${buffers.length}件のPDFファイルをダウンロードしました`);
}

main();

実行方法

  1. コード内の定数を設定:
// 共有ドライブIDを実際のIDに変更
const FOLDER_ID = "your_shared_drive_id_here";

// 検索キーワードを設定
const SEARCH_TERM = "検索したいキーワード";
  1. 実行:
$ npx tsx searchAndReadPdfFiles.ts

トークンを読み込みました
検索結果: 1件のPDFファイルが見つかりました
PDFファイルをダウンロード中: sample.pdf
1件のPDFファイルをダウンロードしました

3.2 補足:検索クエリのバリエーション

Google Drive APIでは、クエリ言語を使って柔軟にファイルを検索できます。

// ゴミ箱に入っていないファイル
const query1 = `trashed = false`;

// PDFファイルのみ
const query2 = `mimeType="application/pdf"`;

// ファイル名に特定の文字列を含む
const query3 = `name contains "提案書"`;

// 複数条件を組み合わせる
const query4 = `
  trashed = false and
  mimeType="application/pdf" and
  (name contains "提案書" or fullText contains "提案書")
`;

// 特定の日付以降に作成されたファイル
const query5 = `createdTime >= "2024-01-01T00:00:00"`;

4. Google Slides API

この章では、Google Slides APIを使ったプレゼンテーション作成について説明します。Drive APIとの連携、スライドの作成、テキストの挿入など、基本的な操作を実践します。

4.1 プレゼンテーションの作成

プレゼンテーションを作成する際は、Slides APIではなくDrive APIを使用します(Slides APIではフォルダを指定して作成できない)。これにより、Google Drive上の特定のフォルダにプレゼンテーションを作成できます。

createPresentationInFolder.ts
import { google, Auth } from "googleapis";

/**
 * Google Driveにプレゼンテーションを作成し、IDを返す関数
 */
export async function createPresentationInFolder(
  oAuth2Client: Auth.OAuth2Client,
  title: string,
  folderId: string
): Promise<string> {
  // Google Drive APIを初期化する
  const drive = google.drive({ version: "v3", auth: oAuth2Client });

  // Google Drive上にプレゼンテーションを作成する
  const response = await drive.files.create({
    requestBody: {
      name: title,

      // Google スライドのファイルとして作成
      mimeType: "application/vnd.google-apps.presentation",
      parents: [folderId],
    },
    fields: "id", // レスポンスに含める属性

    // 共有フォルダも含めて検索・操作の対象にする。
    // 共有ドライブを使う場合は必須。
    supportsAllDrives: true,
  });

  // 作成したプレゼンテーションのIDを取得
  const presentationId = response.data.id;
  if (!presentationId) {
    throw new Error("指定したフォルダにプレゼンテーションを作成できませんでした");
  }

  // プレゼンテーションを作成すると、1ページ目のタイトルスライドが自動的に作成される
  console.log(`プレゼンテーションを作成しました: ${presentationId}`);
  return presentationId;
}

4.2 スライド作成・テキスト挿入

プレゼンテーションの作成からスライド追加、テキスト挿入までの実装例です。

createPresentation.ts
import { google, slides_v1 } from "googleapis";
import { loadSavedCredentials } from "./utils.js";
import { createPresentationInFolder } from "./createPresentationInFolder.js";

// Google DriveのフォルダID(https://drive.google.com/drive/folders/XXXXX のXXXXXがID)
const FOLDER_ID = "yout-folder-id";

/**
 * メイン関数
 */
async function main(): Promise<void> {
  // 認証情報を読み込む(2章を参照)
  const oAuth2Client = await loadSavedCredentials();

  // プレゼンテーションを作成
  const presentationId = await createPresentationInFolder(
    oAuth2Client,
    "サンプルプレゼンテーション",
    FOLDER_ID
  );

  // Google Slides APIの初期化
  const slides = google.slides({ version: "v1", auth: oAuth2Client });

  // プレゼンテーションを取得する
  const presentation = await slides.presentations.get({ presentationId });

  // Google スライドには複数のレイアウトテンプレートが用意されている。
  // 新しいスライドを追加する際は、これらのレイアウトを指定する。
  // 利用可能なレイアウト名(displayName)の例:
  //   "タイトルと本文"
  //   "タイトルのみ"
  //   "セクション ヘッダー"
  // など
  const slideLayout = presentation.data.layouts?.find(
    (layout) => layout.layoutProperties?.displayName === "タイトルと本文"
  );
  if (!slideLayout || !slideLayout.objectId) {
    throw new Error("スライドレイアウトが見つかりませんでした");
  }

  const layoutId = slideLayout.objectId;

  // スライドの内容を定義
  const slideContents = [
    { title: "タイトルスライド", content: "プレゼンテーションの概要" },
    { title: "第1章", content: "内容1\n内容2\n内容3" },
    { title: "第2章", content: "内容A\n内容B\n内容C" },
  ];

  // ステップ1: 必要な数だけスライドを一括作成
  // 1ページ目は自動作成されているので、2ページ目以降を作成
  const createSlideRequests: slides_v1.Schema$Request[] = slideContents.slice(1).map((_, index) => ({
    createSlide: {
      insertionIndex: index + 1,
      slideLayoutReference: {
        layoutId: layoutId,
      },
    },
  }));

  await slides.presentations.batchUpdate({
    presentationId,
    requestBody: {
      requests: createSlideRequests,
    },
  });
  console.log(`${slideContents.length}枚のスライドを作成しました`);

  // ステップ2: 再度getして最新の状態を取得
  // 重要: batchUpdate後は再度getしないと、新しく作成されたスライドのobjectIdが取得できない
  const updatedPresentation = await slides.presentations.get({ presentationId });

  // ステップ3: 各スライドにテキストを挿入
  for (let i = 0; i < slideContents.length; i++) {
    const slide = updatedPresentation.data.slides?.[i];
    if (!slide || !slide.pageElements) {
      throw new Error(`${i + 1}ページ目のスライドの取得に失敗しました`);
    }

    const pageElements = slide.pageElements;

    // pageElementsは、各スライド内の要素(テキストボックス、図形など)の配列。
    // 「タイトルと本文」レイアウトの場合:
    //   pageElements[0]: タイトル用のテキストボックス
    //   pageElements[1]: 本文用のテキストボックス
    // 各要素にはobjectIdという一意のIDが割り当てられており、
    // このIDを使ってテキストを挿入したり要素を編集したりする。
    const titleObjectId = pageElements[0]?.objectId;
    const bodyObjectId = pageElements[1]?.objectId;

    if (!titleObjectId || !bodyObjectId) {
      throw new Error(`スライド ${i + 1} のテキストボックスIDが見つかりません`);
    }

    const textInsertRequests = [
        // タイトルを挿入
      {
        insertText: {
          objectId: titleObjectId,  // タイトル用テキストボックスのID
          text: slideContents[i]?.title ?? "",
        },
      },
      // 本文を挿入
      {
        insertText: {
          objectId: bodyObjectId,  // 本文用テキストボックスのID
          text: slideContents[i]?.content ?? "",
        },
      },
    ];

    await slides.presentations.batchUpdate({
      presentationId,
      requestBody: {
        requests: textInsertRequests,
      },
    });
  }
  console.log("すべてのスライドにテキストを挿入しました");

  console.log("プレゼンテーションの作成が完了しました");
  console.log(`URL: https://docs.google.com/presentation/d/${presentationId}/edit`);
}

main();

実行方法

  1. コード内の定数を設定:
// 共有ドライブIDを実際のIDに変更
const FOLDER_ID = "yout-folder-id";
  1. 実行:
$ npx tsx createPresentation.ts

トークンを読み込みました
プレゼンテーションを作成しました: 1Oa......8AU
3枚のスライドを作成しました
すべてのスライドにテキストを挿入しました
プレゼンテーションの作成が完了しました
URL: https://docs.google.com/presentation/d/1Oa......8AU/edit

以下のようなスライドが作成されるはずです。

5. さいごに

ここまでご覧いただきありがとうございました。
Google Drive API や Google Slide API を使ってみたい人やエラーばかりで技術検証が進まない方の参考になれば幸いです。

参考

OAuth 2.0 認証

認証情報の作成

Google Drive API

Google Slides API

NCDC テックブログ

Discussion