【GAS】スプレッドシートに入力したキーワードで検索してヒットした画像をGoogleドライブに保存する

Code

はじまり

リサちゃん
リサちゃん

これ1個ずつ検索してダウンロードするの面倒くさいなあ・・・

135ml
135ml

300個あるもんなあ

リサちゃん
リサちゃん

はああ・・・

135ml
135ml

ちょっとでも作業を減らせれば良いなあ。

概要

今回は、Google Apps Script(GAS)を使って、Googleのサービスを自動化していきます。

実装する処理の目的は、アプリのロゴをなるべく自動で集めて楽をすることです。

そこで、GASを使用してスプレッドシートに入力されたキーワードに基づいて画像を検索し、見つかった最初の画像をGoogleドライブに自動的に保存する処理を作っていきます。

どれぐらい行けそうなものか試してみましょう。

こんな風に入力して、

こんな風に画像が保存されることを期待します。

前準備

スクリプトを書く前に、諸々と準備が必要です。

  • Custom Search APIを有効にする。
  • APIキーを入手する。
  • カスタム検索エンジン(Custom Search Engine)を設定する。

Custom Search APIを有効にする

まずは、Google Cloud Platformのコンソール画面で、「Custom Search API」というものを有効にします。

「APIとサービス」>「ライブラリ」と遷移して、検索します。

そして、「Custom Search API」が有効になっていることを確認します。

APIキーを入手する

次に、このCustom Search APIを利用するためのAPIキーを入手しましょう。

「APIとサービス」>「認証情報」と遷移した先の画面で、「認証情報を作成」をクリックして、「APIキー」を選択します。

APIキーが作成されたら編集します。

デフォルトでは、プロジェクトで有効にしている全てのAPIにアクセスできるようになっていると思うので、一つのAPIキーで利用できるAPIを一つに限定させます。

「APIの制限」の部分で、「Custom Search API」だけを選択して保存します。

これで、このAPIキーはCustom Search APIしか利用できないキーになりました。セキュリティセキュリティ。

「API Key」にある値は、後で使います。

カスタム検索エンジン(Custom Search Engine)を設定する

そして、この準備も必要です。

「カスタム検索エンジン」というものを作ることで、スクリプトで実行する検索方法を設定します。

まずは、以下のサイトに飛んで、「使ってみる」を選びます。

Google のプログラム可能な検索エンジン
ウェブサイトを訪れた人たちが、必要なものを簡単に検索できるようにしましょう。カスタマイズ可能な検索ボックスをウェブページに追加し、Google の技術を活用して関連性の高い検索結果をすばやく提供できます。

そして、カスタム検索エンジンを作ります。

ここで忘れてはならないのが、「検索設定」の部分で「画像検索」をONにすることです!

ちゃんと「画像検索」がONになっているカスタム検索エンジンが作成できたら、そのエンジンの「検索エンジンID」をメモります。後で使います。

スプレッドシートの準備

そうしたら、事前準備は終わったので、GASを実装していきましょう。

今回は、スプレッドシート上に入力したキーワードで検索するので、スプレッドシート上に入力部を準備します。

こんな感じの入力部分を作ります。今回は2列分作ります。

そして、冒頭のようにキーワードを並べていきます。

右側には、保存する画像ファイルの名前を入力します。拡張子は除いた形です。

スクリプトの実装

そして、やっとスクリプトを実装していきます。

処理の概要

まずは、処理の本流になります。

/**
 * @description Check whether the element is an empty value.
 * @param {any} element
 * @return {boolean}
*/
function checkEmpty(element) {
  return element !== undefined && element !== 0 && element !== null && element !== "";
}

/**
 * @description Check whether empty element is existing.
 * @param {any[]} array
 * @return {boolean}
*/
function isEmptyExisting(array) {
  let empties = array.filter(element => !checkEmpty(element));
  if(empties.length > 0){
    return true;
  }
  return false;
}

/**
 * @description Search by keywords specified in the sheet and save into the Google Drive.
 * @param {boolean} isRecording
 * @return {boolean}
*/
function searchToSaveImageToDrive(isRecording=true) {
  const folderIdToSaveLogoBySearching = FOLDER_ID_TO_SAVE_LOGO_BY_SEARCHING;
  const sheetName = SHEET_NAME_6TH;
  const startRow = ROW_INDEX_5TH_TO_START_OUTPUTING_RECORDS;
  const startColumn = COLUMN_INDEX_5TH_OF_FILE_LIST_TO_SAVE_IMAGE_BY_SEARCH;
  
  console.log(`searchToSaveImageToDrive: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`);
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let records = sheet.getRange(startRow, startColumn, sheet.getLastRow() - startRow + 1, 2).getValues();
  records = records.filter(values => !isEmptyExisting(values));
  console.log(records);
  console.log(`searchToSaveImageToDrive: cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc`);

  let gottenBlobs = records.map(rec => {
    return searchByKeywordToGetImages(rec[0], rec[1]);
  });
  console.log(gottenBlobs);
  console.log(`searchToSaveImageToDrive: dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd`);

  let saveImageFiles = gottenBlobs.map(blobInfo => {
    return createImageFilesFromBlobs(blobInfo, folderIdToSaveLogoBySearching);
  });
  saveImageFiles = saveImageFiles.filter(value => checkEmpty(value));
  console.log(saveImageFiles);
  console.log(`searchToSaveImageToDrive: ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg`);

  const myPromise = new Promise((resolve, reject) => {
    recordLog(MY_SCRIPT_NAME, "searchToSaveImageToDrive", isRecording);
  });
  console.log(`searchToSaveImageToDrive: iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii`);

  let folderUrl = `https://drive.google.com/drive/folders/${folderIdToSaveLogoBySearching}`;
  displayHtml("index_html", "Saving Files By Searching Terminated...", `<h2>Done.</h2><p>${saveImageFiles.length} images saved in ${getHref(folderUrl, "this folder")}.</p>`);
  console.log(`searchToSaveImageToDrive: kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk`);

  return true;
}

処理流れとしては、以下の2部構成です。

  • 画像検索の処理
  • 画像を取得してGoogleドライブに保存する処理

画像検索の処理

まずは、gottenBlobsという配列に画像検索してヒットした画像のBlobオブジェクトを含む情報を格納していきます。

まずは、この行の処理で、スプレッドシートからキーワードとファイル名を取得します。

let records = sheet.getRange(startRow, startColumn, sheet.getLastRow() - startRow + 1, 2).getValues();

また、checkEmpty()isEmptyExisting()という関数は、nullや””(空文字)になっている要素を省くために使います。

/**
 * @description Check whether the element is an empty value.
 * @param {any} element
 * @return {boolean}
*/
function checkEmpty(element) {
  return element !== undefined && element !== 0 && element !== null && element !== "";
}

/**
 * @description Check whether empty element is existing.
 * @param {any[]} array
 * @return {boolean}
*/
function isEmptyExisting(array) {
  let empties = array.filter(element => !checkEmpty(element));
  if(empties.length > 0){
    return true;
  }
  return false;
}

そしたら、この処理でBlobオブジェクトを含んだオブジェクトの配列を取得します。

let gottenBlobs = records.map(rec => {
    return searchByKeywordToGetImages(rec[0], rec[1]);
  });

まずは、searchByKeywordToGetImages()の処理はこんな感じです。

/**
 * @description Search by specified keyword to get blob object.
 * @param {string} keyword
 * @param {string} gettingBlobName
 * @return {any{}}
*/
function searchByKeywordToGetImages(keyword, gettingBlobName){
  const apiKey = API_KEY_TO_SEARCH_BY_CUSTOM_SEARCH_API;
  const searchEngineId = SEARCH_ENGINE_ID_TO_SAVE_IMAGE;
  let blob, extension, blobFullName;
  let keywordToSearch = keyword.replaceAll(" ", "+").replaceAll(" ", "+");
  keywordToSearch = `${encodeURIComponent(keywordToSearch)}+logo`;
  let searchUrl = `https://www.googleapis.com/customsearch/v1?q=${keywordToSearch}&cx=${searchEngineId}&searchType=image&key=${apiKey}&num=1`;
  let response;
  try{
    response = UrlFetchApp.fetch(searchUrl);
  }catch(error){
    return {"blob": null, "name": null};
  }
  
  let results = JSON.parse(response.getContentText());
  console.log(results);
  console.log(`searchByKeywordToGetImages: eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`);
  
  if (results.items && results.items.length > 0) {
    let imageUrl = results.items[0].link;
    let imageResponse;
    try{
      imageResponse = UrlFetchApp.fetch(imageUrl);
    }catch(error){
      return {"blob": null, "name": null};
    }
    blob = imageResponse.getBlob();
    console.log(blob);
    console.log(`searchByKeywordToGetImages: gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg`);

    // 元の画像のURLからファイルの拡張子を取得します
    extension = imageUrl.substring(imageUrl.lastIndexOf('.'));
    // 拡張子がある場合、クエリパラメーターまたは他のパスの一部を含まないようにします
    if (extension.indexOf('?') !== -1) {
      extension = extension.substring(0, extension.indexOf('?'));
    }
    // 拡張子らしき文字列が6文字以上もしくは拡張子が正しく取得できない場合は、デフォルトとして.jpgを使用
    if (extension.length >= 6 || extension.indexOf('/') !== -1) {
      extension = '.jpg';
    }
    blobFullName = `${gettingBlobName}${extension}`
    console.log(blobFullName);
    console.log(`searchByKeywordToGetImages: iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii`);
  } else {
    console.log('No images found for: ' + keywordToSearch);
    console.log(`searchByKeywordToGetImages: jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj`);
  }
  console.log(`searchByKeywordToGetImages: kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk`);

  return {"blob": blob, "name": blobFullName};
}

画像検索をする流れは以下です。

  • 半角スペースと全角スペースを考慮した検索文字列にする。
  • その検索文字列に「logo」というキーワードも追加して、ロゴが画像検索にヒットしやすくする。
  • 検索用URLが出来たら、UrlFetchApp.fetch()を実行する。
  • 検索結果から、最初にヒットした画像があるURLを取得する。
  • 再び、UrlFetchApp.fetch()を実行して、画像のデータを取得する。
  • 取得した画像にファイル名を付ける。
  • 「Blobオブジェクト」と「ファイル名」を返す。

特に、「取得した画像にファイル名を付ける。」処理ですが、その処理は以下の流れです。

  • ここではまず、元々の画像の拡張子を取得します。
  • しかし、URLから取得した拡張子にはクエリパラメータが含まれている可能性があるので、クエリパラメーターを削ります。
  • そして、クエリパラメーターが削られたら、「.jpeg」とか「.png」とか「.webp」といった拡張子が来ることを期待しますが、ファイル名に「.」が含まれていてちゃんと取得できない可能性があります。
  • そこで、拡張子が6文字以上だったら、「.jpg」で拡張子を決定します。(画像フォーマットだったら大体このくらいの長さでいいんじゃないでしょうか。)

「.」を考慮しないと、こんな画像ファイルが生まれてきてしまうので、

「.」の長さを考慮することにします。

これで、画像を保存する下地は出来ました。

画像を取得してGoogleドライブに保存する処理

画像オブジェクトをGoogleドライブに保存します。

/**
 * @description Create image files into the Google Drive.
 * @param {any{}} blobInfo
 * @param {string} folderId
 * @return {DriveApp.File}
*/
function createImageFilesFromBlobs(blobInfo, folderId=FOLDER_ID_TO_SAVE_LOGO_BY_SEARCHING){
  let folder = DriveApp.getFolderById(folderId);
  console.log(`createImageFilesFromBlobs: eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`);

  if(Object.prototype.toString.call(blobInfo["name"]) === "[object Null]"){
    return null;
  }

  let fileName = blobInfo["name"];
  let file = DriveApp.createFile(blobInfo["blob"].setName(fileName));
  file.moveTo(folder);
  console.log(`createImageFilesFromBlobs: iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii`);
  console.log('Image saved to Drive with name: ' + fileName);
  
  console.log(`createImageFilesFromBlobs: kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk`);

  return file;
}

ここではもう、渡されたオブジェクトをファイルオブジェクトのプロパティに割り当てているだけになります。

これで、画像が目当てのGoogleドライブフォルダの中に保存されました!

処理の本流にあるdisplayHtml()という処理では、何個のファイルが保存されたかを表示しています。

displayHtml("index_html", "Saving Files By Searching Terminated...", `<h2>Done.</h2><p>${saveImageFiles.length} images saved in ${getHref(folderUrl, "this folder")}.</p>`);

じゃあ、どれぐらいロゴが保存できるんだろう?

さて、処理が出来たところで、この画像検索による処理の精度が気になってきます。

Googleドライブで画像を一覧で眺めてみると・・・、

ふむふむ、なるほど・・・

まあ、画像検索のロゴを取得する精度としては、50~60点というところでしょうか。

個人的には、以下の点がまだ至っていないなあと思いました。

  • 文字が入っていないロゴが欲しい。
  • 少しマイナーなサービスだと厳しいかなあ。
  • なんか画像じゃないファイルが混ざってる・・・(これは、画像検索処理で結果を複数取得して、先頭から画像かどうかで処理を分岐させれば回避できそうですね。)
  • あと、画像の取得処理が遅い!(非同期にすれば全然速くなりそうです。)

まとめ

これで、スプレッドシートに入力されたキーワードで画像を検索し、ヒットした最初の画像をGoogleドライブに自動で保存する機能が一旦完成しました。

本記事は、以下の流れで書かれました。

  • Custom Search APIの有効化
  • APIキーの取得
  • カスタム検索エンジンの設定
  • Googleスプレッドシートの準備
  • GASスクリプトの実装(画像検索して画像を取得)
  • GASスクリプトの実装(取得した画像をGoogleドライブに保存)

Googleのポリシーと利用規約や、APIの使用制限、クォータなどにも気を付けて使ってみましょう。保存される画像の使用範囲や著作権にも注意ですね。(ちなみに、Custom Search APIは、1日100リクエストより多くリクエストすると有料になります。)

おしまい

リサちゃん
リサちゃん

まあ、作業が半分以上は減ったと思うと悪くないかなあ

135ml
135ml

手頃に作りました。

以上になります!

コメント

タイトルとURLをコピーしました