複数のドキュメントのコンテンツを集約する

コーディング レベル: 初級
所要時間: 15 分
プロジェクト タイプ: カスタム メニューを使用した自動化

目標

  • ソリューションの機能を理解します。
  • ソリューション内で Apps Script サービスが何を行うかを理解します。
  • スクリプトを設定します。
  • スクリプトを実行します。

このソリューションについて

時間を節約し、手動でコピー&ペーストする際のエラーを減らすには、複数のドキュメントのコンテンツを 1 つのメイン ドキュメントに自動的にインポートします。このソリューションはプロジェクトのステータス レポートの集計に重点を置いていますが、ニーズに合わせて編集できます。

インポートされたプロジェクトの概要のスクリーンショット

仕組み

スクリプトは、コンテンツのインポート元となるドキュメントを保存するフォルダと、インポートの開始元となるテンプレート ドキュメントを作成します。このスクリプトには、このソリューションのデモを行うためのサンプル ドキュメントを作成する関数も含まれています。

ユーザーがカスタム メニューから [Import summaries] を選択すると、スクリプトはフォルダ内のすべてのドキュメント ファイルを取得し、それぞれを反復処理します。スクリプトは、コピーする必要がある要約テキストを特定するために、特定の文字列と見出しタイプを探します。テキストがコピーされると、スクリプトは重複を軽減するために識別子文字列のテキストの色を変更します。スクリプトは、要約をメイン ドキュメントに貼り付けます。各要約は、それぞれ独自の単一セル テーブルに貼り付けられます。

Apps Script サービス

このソリューションでは、次のサービスを使用します。

  • ドキュメント サービス - テンプレートとサンプル ソース ドキュメントを作成します。各ソース ドキュメントを反復処理して、インポートする新しいプロジェクトの概要を探します。概要をメイン ドキュメントにインポートします。要約が複数回インポートされないように、ソース ドキュメントを更新します。
  • ドライブ サービス - ソース ドキュメントを保存するフォルダを作成します。テンプレート ドキュメントとサンプル ソース ドキュメントをフォルダに追加します。
  • ユーティリティ サービス - スクリプトがソース ドキュメントから要約をインポートするたびに、スクリプトがメイン ドキュメントに追加する日付の形式を設定します。
  • 基本サービス - Session クラスを使用して、スクリプトのタイムゾーンを取得します。このスクリプトは、インポートの日付をメイン ドキュメントに追加する際にタイムゾーンを使用します。

前提条件

このサンプルを使用するには、次の前提条件を満たしている必要があります。

  • Google アカウント(Google Workspace アカウントの場合、管理者の承認が必要となる可能性があります)。
  • インターネットにアクセスできるウェブブラウザ。

スクリプトを設定する

下のボタンをクリックして、コンテンツの集約ドキュメントのコピーを作成します。
コピーを作成

スクリプトを実行する

サンプル ドキュメントを使用してデモを実行する

  1. [Import summaries] > [Configure] > [Run demo setup with sample documents] をクリックします。このカスタム メニューを表示するには、ページの更新が必要になる場合があります。
  2. メッセージが表示されたら、スクリプトを承認します。OAuth 同意画面に「このアプリは確認されていません」という警告が表示された場合は、[詳細] > [{プロジェクト名} に移動(安全でない)] を選択して続行します。

  3. [Import summaries] > [Configure] > [Run demo setup with sample documents] をもう一度クリックします。

  4. プロンプトが表示されたら、次の手順で使用するドライブ フォルダの URL をコピーします。

  5. [OK] をクリックします。

  6. [要約をインポート] > [要約をインポート] をクリックします。

  7. プロンプトが表示されたら、[OK] をクリックします。

  8. サンプル ドキュメントからインポートされたプロジェクトの概要を確認します。

概要を追加してインポートする

  1. 新しいブラウザタブで、フォルダの URL を貼り付けて [Project status] フォルダを開きます。
  2. [Project ABC] ファイルを開きます。
  3. インポートする新しい要約を作成するには、ドキュメントの末尾に次の内容を追加します。
    1. Summary」と入力し、テキストのスタイルを [見出し 3] に設定します。
    2. Summary の直下に 1x1 の表を挿入します。Summary とテーブルの間に空白行がないことを確認します。
    3. テーブルに「Hello world!」と入力します。
  4. メインのドキュメントに戻り、[要約をインポート] > [要約をインポート] をクリックします。
  5. プロンプトが表示されたら、[OK] をクリックします。
  6. 最新のインポートはドキュメントの末尾に表示されます。

コードを確認する

このソリューションの Apps Script コードを確認するには、下の [ソースコードを表示] をクリックします。

ソースコードを表示

コード.gs

solutions/automations/aggregate-document-content/Code.js
// To learn how to use this script, refer to the documentation: // https://developers.google.com/apps-script/samples/automations/aggregate-document-content  /* Copyright 2022 Google LLC  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at      https://www.apache.org/licenses/LICENSE-2.0  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */  /**   * This file containts the main application functions that import data from  * summary documents into the body of the main document.  */  // Application constants const APP_TITLE = 'Document summary importer'; // Application name const PROJECT_FOLDER_NAME = 'Project statuses'; // Drive folder for the source files.  // Below are the parameters used to identify which content to import from the source documents // and which content has already been imported. const FIND_TEXT_KEYWORDS = 'Summary'; // String that must be found in the heading above the table (case insensitive). const APP_STYLE = DocumentApp.ParagraphHeading.HEADING3; // Style that must be applied to heading above the table. const TEXT_COLOR = '#2e7d32'; // Color applied to heading after import to avoid duplication.  /**  * Updates the main document, importing content from the source files.  * Uses the above parameters to locate content to be imported.  *   * Called from menu option.  */ function performImport() {   // Gets the folder in Drive associated with this application.   const folder = getFolderByName_(PROJECT_FOLDER_NAME);   // Gets the Google Docs files found in the folder.    const files = getFiles(folder);    // Warns the user if the folder is empty.   const ui = DocumentApp.getUi();   if (files.length === 0) {     const msg =       `No files found in the folder '${PROJECT_FOLDER_NAME}'.       Run '${MENU.SETUP}' | '${MENU.SAMPLES}' from the menu       if you'd like to create samples files.`     ui.alert(APP_TITLE, msg, ui.ButtonSet.OK);     return;   }    /** Processes main document */   // Gets the active document and body section.   const docTarget = DocumentApp.getActiveDocument();   const docTargetBody = docTarget.getBody();    // Appends import summary section to the end of the target document.    // Adds a horizontal line and a header with today's date and a title string.   docTargetBody.appendHorizontalRule();   const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');   const headingText = `Imported: ${dateString}`;   docTargetBody.appendParagraph(headingText).setHeading(APP_STYLE);   // Appends a blank paragraph for spacing.   docTargetBody.appendParagraph(" ");    /** Process source documents */   // Iterates through each source document in the folder.   // Copies and pastes new updates to the main document.   let noContentList = [];   let numUpdates = 0;   for (let id of files) {      // Opens source document; get info and body.     const docOpen = DocumentApp.openById(id);     const docName = docOpen.getName();     const docHtml = docOpen.getUrl();     const docBody = docOpen.getBody();      // Gets summary content from document and returns as object {content:content}     const content = getContent(docBody);      // Logs if document doesn't contain content to be imported.     if (!content) {       noContentList.push(docName);       continue;     }     else {       numUpdates++       // Inserts content into the main document.       // Appends a title/url reference link back to source document.       docTargetBody.appendParagraph('').appendText(`${docName}`).setLinkUrl(docHtml);       // Appends a single-cell table and pastes the content.       docTargetBody.appendTable(content);     }     docOpen.saveAndClose()   }   /** Provides an import summary */   docTarget.saveAndClose();   let msg = `Number of documents updated: ${numUpdates}`   if (noContentList.length != 0) {     msg += `\n\nThe following documents had no updates:`     for (let file of noContentList) {       msg += `\n ${file}`;     }   }   ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); }  /**  * Updates the main document drawing content from source files.  * Uses the parameters at the top of this file to locate content to import.  *   * Called from performImport().  */ function getContent(body) {    // Finds the heading paragraph with matching style, keywords and !color.   var parValidHeading;   const searchType = DocumentApp.ElementType.PARAGRAPH;   const searchHeading = APP_STYLE;   let searchResult = null;    // Gets and loops through all paragraphs that match the style of APP_STYLE.   while (searchResult = body.findElement(searchType, searchResult)) {     let par = searchResult.getElement().asParagraph();     if (par.getHeading() == searchHeading) {       // If heading style matches, searches for text string (case insensitive).       let findPos = par.findText('(?i)' + FIND_TEXT_KEYWORDS);       if (findPos !== null) {          // If text color is green, then the paragraph isn't a new summary to copy.         if (par.editAsText().getForegroundColor() != TEXT_COLOR) {           parValidHeading = par;         }       }     }   }    if (!parValidHeading) {     return;   } else {     // Updates the heading color to indicate that the summary has been imported.          let style = {};     style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR;     parValidHeading.setAttributes(style);     parValidHeading.appendText(" [Exported]");      // Gets the content from the table following the valid heading.     let elemObj = parValidHeading.getNextSibling().asTable();     let content = elemObj.copy();      return content;   } }  /**  * Gets the IDs of the Docs files within the folder that contains source files.  *   * Called from function performImport().  */ function getFiles(folder) {   // Only gets Docs files.   const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);   let docIDs = [];   while (files.hasNext()) {     let file = files.next();     docIDs.push(file.getId());   }   return docIDs; }

solutions/automations/aggregate-document-content/Menu.js
/**  * Copyright 2022 Google LLC  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  *      http://www.apache.org/licenses/LICENSE-2.0  *  * Unless required by applicable law or agreed to in writing, software  * distributed under the License is distributed on an "AS IS" BASIS,  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  */  /**   * This file contains the functions that build the custom menu.  */ // Menu constants for easy access to update. const MENU = {   NAME: 'Import summaries',   IMPORT: 'Import summaries',   SETUP: 'Configure',   NEW_INSTANCE: 'Setup new instance',   TEMPLATE: 'Create starter template',   SAMPLES: 'Run demo setup with sample documents' }  /**  * Creates custom menu when the document is opened.  */ function onOpen() {   const ui = DocumentApp.getUi();   ui.createMenu(MENU.NAME)     .addItem(MENU.IMPORT, 'performImport')     .addSeparator()     .addSubMenu(ui.createMenu(MENU.SETUP)       .addItem(MENU.NEW_INSTANCE, 'setupConfig')       .addItem(MENU.TEMPLATE, 'createSampleFile')       .addSeparator()       .addItem(MENU.SAMPLES, 'setupWithSamples'))     .addItem('About', 'aboutApp')     .addToUi() }  /**  * About box for context and contact.  * TODO: Personalize  */ function aboutApp() {   const msg = `   ${APP_TITLE}   Version: 1.0   Contact: <Developer Email goes here>`    const ui = DocumentApp.getUi();   ui.alert("About this application", msg, ui.ButtonSet.OK); }

Setup.gs

solutions/automations/aggregate-document-content/Setup.js
/**  * Copyright 2022 Google LLC  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  *      http://www.apache.org/licenses/LICENSE-2.0  *  * Unless required by applicable law or agreed to in writing, software  * distributed under the License is distributed on an "AS IS" BASIS,  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  */  /**   * This file contains functions that create the template and sample documents.  */  /**  * Runs full setup configuration, with option to include samples.  *   * Called from menu & setupWithSamples()  *   * @param {boolean} includeSamples - Optional, if true creates samples files. *   */ function setupConfig(includeSamples) {    // Gets folder to store documents in.   const folder = getFolderByName_(PROJECT_FOLDER_NAME)    let msg =     `\nDrive Folder for Documents: '${PROJECT_FOLDER_NAME}'    \nURL: \n${folder.getUrl()}`    // Creates sample documents for testing.   // Remove sample document creation and add your own process as needed.   if (includeSamples) {     let filesCreated = 0;     for (let doc of samples.documents) {       filesCreated += createGoogleDoc(doc, folder, true);     }     msg += `\n\nFiles Created: ${filesCreated}`   }   const ui = DocumentApp.getUi();   ui.alert(`${APP_TITLE} [Setup]`, msg, ui.ButtonSet.OK);  }  /**  * Creates a single document instance in the application folder.  * Includes import settings already created [Heading | Keywords | Table]  *   * Called from menu.   */ function createSampleFile() {    // Creates a new Google Docs document.   const templateName = `[Template] ${APP_TITLE}`;   const doc = DocumentApp.create(templateName);   const docId = doc.getId();    const msg = `\nDocument created: '${templateName}'   \nURL: \n${doc.getUrl()}`    // Adds template content to the body.   const body = doc.getBody();    body.setText(templateName);   body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);   body.appendParagraph('Description').setHeading(DocumentApp.ParagraphHeading.HEADING1);   body.appendParagraph('');    const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');   body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE);   body.appendTable().appendTableRow().appendTableCell('TL;DR');   body.appendParagraph("");    // Gets folder to store documents in.   const folder = getFolderByName_(PROJECT_FOLDER_NAME)    // Moves document to application folder.   DriveApp.getFileById(docId).moveTo(folder);    const ui = DocumentApp.getUi();   ui.alert(`${APP_TITLE} [Template]`, msg, ui.ButtonSet.OK); }  /**  * Configures application for demonstration by setting it up with sample documents.  *   * Called from menu | Calls setupConfig with option set to true.   */ function setupWithSamples() {   setupConfig(true) }  /**   * Sample document names and demo content.   * {object} samples[] */ const samples = {   'documents': [     {       'name': 'Project GHI',       'description': 'Google Workspace Add-on inventory review.',       'content': 'Reviewed all of the currently in-use and proposed Google Workspace Add-ons. Will perform an assessment on how we can reduce overlap, reduce licensing costs, and limit security exposures. \n\nNext week\'s goal is to report findings back to the Corp Ops team.'     },     {       'name': 'Project DEF',       'description': 'Improve IT networks within the main corporate building.',       'content': 'Primarily focused on 2nd thru 5th floors in the main corporate building evaluating the network infrastructure. Benchmarking tests were performed and results are being analyzed. \n\nWill submit all findings, analysis, and recommendations next week for committee review.'     },     {       'name': 'Project ABC',       'description': 'Assess existing Google Chromebook inventory and recommend upgrades where necessary.',       'content': 'Concluded a pilot program with the Customer Service department to perform inventory and update inventory records with Chromebook hardware, Chrome OS versions, and installed apps. \n\nScheduling a work plan and seeking necessary go-forward approvals for next week.'     },   ],   'common': 'This sample document is configured to work with the Import summaries custom menu. For the import to work, the source documents used must contain a specific keyword (currently set to "Summary"). The keyword must reside in a paragraph with a set style (currently set to "Heading 3") that is directly followed by a single-cell table. The table contains the contents to be imported into the primary document.\n\nWhile those rules might seem precise, it\'s how the application programmatically determines what content is meant to be imported and what can be ignored. Once a summary has been imported, the script updates the heading font to a new color (currently set to Green, hex \'#2e7d32\') to ensure the app ignores it in future imports. You can change these settings in the Apps Script code.' }  /**  * Creates a sample document in application folder.  * Includes import settings already created [Heading | Keywords | Table].  * Inserts demo data from samples[].  *   * Called from menu.   */ function createGoogleDoc(document, folder, duplicate) {    // Checks for duplicates.   if (!duplicate) {     // Doesn't create file of same name if one already exists.     if (folder.getFilesByName(document.name).hasNext()) {       return 0 // File not created.     }   }    // Creates a new Google Docs document.   const doc = DocumentApp.create(document.name).setName(document.name);   const docId = doc.getId();    // Adds boilerplate content to the body.   const body = doc.getBody();    body.setText(document.name);   body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);   body.appendParagraph("Description").setHeading(DocumentApp.ParagraphHeading.HEADING1);   body.appendParagraph(document.description);   body.appendParagraph("Usage Instructions").setHeading(DocumentApp.ParagraphHeading.HEADING1);   body.appendParagraph(samples.common);    const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');   body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE);   body.appendTable().appendTableRow().appendTableCell(document.content);   body.appendParagraph("");    // Moves document to application folder.   DriveApp.getFileById(docId).moveTo(folder);    // Returns if successfully created.   return 1 }

Utilities.gs

solutions/automations/aggregate-document-content/Utilities.js
/**  * Copyright 2022 Google LLC  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  *      http://www.apache.org/licenses/LICENSE-2.0  *  * Unless required by applicable law or agreed to in writing, software  * distributed under the License is distributed on an "AS IS" BASIS,  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  */  /**   * This file contains common utility functions.  */  /**  * Returns a Drive folder located in same folder that the application document is located.  * Checks if the folder exists and returns that folder, or creates new one if not found.  *  * @param {string} folderName - Name of the Drive folder.   * @return {object} Google Drive folder  */ function getFolderByName_(folderName) {   // Gets the Drive folder where the current document is located.   const docId = DocumentApp.getActiveDocument().getId();   const parentFolder = DriveApp.getFileById(docId).getParents().next();    // Iterates subfolders to check if folder already exists.   const subFolders = parentFolder.getFolders();   while (subFolders.hasNext()) {     let folder = subFolders.next();      // Returns the existing folder if found.     if (folder.getName() === folderName) {       return folder;     }   }   // Creates a new folder if one doesn't already exist.   return parentFolder.createFolder(folderName)     .setDescription(`Created by ${APP_TITLE} application to store documents to process`); }  /**  * Test function to run getFolderByName_.  * @logs details of created Google Drive folder.  */ function test_getFolderByName() {    // Gets the folder in Drive associated with this application.   const folder = getFolderByName_(PROJECT_FOLDER_NAME);    console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`)   // Uncomment the following to automatically delete the test folder.   // folder.setTrashed(true); }

寄稿者

このサンプルは、Google デベロッパー エキスパートの協力を得て Google が管理しています。

次のステップ