Googleドキュメントの内容を別のドキュメントに可変で追加していくGAS

GAS

Googleドキュメントって結構曲者ですね・・・
半日くらい苦悩したので同じようなことしたいと思っている人の参考になれば幸いです。

ちなみにChatGPT君に聞いてもうまく聞けずにあんまり役に立ちませんでした。
こちらがGoogleドキュメントの仕組みを理解していなかったから聞き方がダメでしたね。
結局最初の最初は自分で掴みにいかないとなんですよね。

スポンサーリンク

やりたいこと

  1. テンプレートとして使うGoogleドキュメントがある
  2. テンプレートの中には置換文字列が書いてある
    例えば、<今日>というテキストが書いてあると、処理後には2023-04-22と変換してくれる
  3. スプシにレコードが載っており、選択した複数のレコードの数だけ新しいドキュメントにページを作り置換語の内容を表示する

イメージ湧きにくいかもしれないので
具体的に言うとこんなテンプレがあって、

GASを実行するとこんな風にレコードの内容に合わせて1つのドキュメントにいろんなページが自動で作成されます。

で、最初はGASでGoogleドキュメント→PDFに変換してローカルにダウンロードって感じでやっていたんですが、レコードが10くらいになるとタイムアウトしてしまうくらい重い。

重すぎるので、Googleドキュメントにして軽くしようと。
紙で求められるものだったので印刷が必要です。アウトプットは1つのGoogleドキュメントにまとめて一回ポチで印刷できるようにします。
結果的に結構苦戦したのですが・・・

コード

Googleドキュメントをいじる部分だけ書きます。
前提のスプシや置換文字列の設定、レコードの取得部分は省きます。

function createDocument(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dest_doc_folder = checkDirectory("output");
  const log_sheet = ss.getSheetByName("log");  
  const templete_urls = getTemplete();
  let templete = DocumentApp.openByUrl(templete_urls[0]);
  let templete_id = templete.getId();
  let docA = DocumentApp.openById(templete_id);
  let bodyA = docA.getBody();
  let bodyA_copy = bodyA.copy();
  let templete_document = DriveApp.getFileById(templete_id);

  const today = Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd");
  const target = getTarget();
  const titles = target.titles;
  const contents = target.contents;
  const info = target.info;
  const count_index = 0;
  const person = 2
  const organization_index = 3;
  const zip_index = 4;
  const address1_index = 5;
  const address2_index = 6;
  const tel_index = 7;
  const fax_index = 8;
  const total_files = (titles.length * info.length);
  let num = 1;  
  let newDocumentName = today + "_" + titles[0];  
  let docB = templete_document.makeCopy(newDocumentName, dest_doc_folder);
  let docB_obj= DocumentApp.openById(docB.getId());
  let bodyB = docB_obj.getBody(); 
  for (let i = 0; i < titles.length; i++) { 
    let title = titles[i];
    let content = contents[i];
    for (let o = 0; o < info.length; o++) {
      let count = info[o][count_index];
      let organization = info[o][1][organization_index];
      let zip = info[o][1][zip_index];
      let address1 = info[o][1][address1_index];
      let address2 = info[o][1][address2_index];
      let tel = info[o][1][tel_index];
      let fax = info[o][1][fax_index];
      const before_words = ["<今日>", "<枚数>", "<タイトル>", "<本文>", "<郵便番号>", "<住所1>", "<住所2>", "<名称>", "<担当者>", "<電話番号>", "<fax>"];
      const after_words = [today, count, title, content, zip, address1, address2, organization, person, tel, fax];
      for (let j = 0; j < before_words.length; j++) {
        bodyB.replaceText(before_words[j], after_words[j]);
      }
      log_sheet.appendRow([Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd hh:mm:ss") + "_" + title + "_" + organization]);

      ss.toast('進行状況: '+ num +'/'+ total_files+' ファイル', '作成中', 5);
      num++;
      if(i == titles.length -1 && o == info.length-1){break;}

      //新たにテンプレのページを追加
      bodyB.appendPageBreak();
      let insertedBodyElements = bodyA_copy.copy();
      let insertedElements = insertedBodyElements.getNumChildren();
      for (let j = 0; j < insertedElements;j++) {
        let element = insertedBodyElements.getChild(j).copy();
        let elementType = element.getType();
        switch ( elementType ) {
          case DocumentApp.ElementType.PARAGRAPH:
            if ( element.asParagraph( ).getNumChildren( ) != 0 ) {
              switch ( element.asParagraph( ).getChild( 0 ).getType( ) ) {
                case DocumentApp.ElementType.TEXT:
                case DocumentApp.ElementType.HORIZONTAL_RULE:
                  bodyB.appendParagraph( element.copy( ) );
                  break;
                case DocumentApp.ElementType.PAGE_BREAK:
                  break;
                case DocumentApp.ElementType.INLINE_DRAWING:
                  var blob = element.asParagraph( ).getChild( 0 ).asInlineDrawing( );
                  bodyB.appendImage( blob );
                  bodyB.appendParagraph( element.copy( ) );
                  break;
                case DocumentApp.ElementType.INLINE_IMAGE:
                  var blob = element.asParagraph( ).getChild( 0 ).asInlineImage( ).getBlob( );
                  bodyB.appendImage( blob );
                  bodyB.appendParagraph( element.copy( ).asParagraph() );
                  break;
              }
            }else {
              bodyB.appendParagraph( element.copy( ) );
            }
            break;
          case DocumentApp.ElementType.TABLE:
            bodyB.appendTable( element.copy( ) );
            break;
          case DocumentApp.ElementType.LIST_ITEM:
            var elCopy = element.copy( );
            var listItem = element.asListItem();
            bodyB.appendListItem( listItem.getText( ) ).setAttributes( elCopy.getAttributes( ) ); 
            break;
          case DocumentApp.ElementType.INLINE_IMAGE:
            bodyB.appendImage( element.copy( ) );
            break;
          default:
            break;
        }
      }
    }
  }
  docB_obj.saveAndClose();
}

解説

なんやねんこれ・・・
特に苦労したのが
//新たにテンプレのページを追加
から下のところです。

スプシやプレゼンテーションのシートやスライドみたいな概念があればよかったのですが、Googleドキュメントにはページという概念がないんですね。
最初はテンプレのファイルをコピーして各レコードの内容の置換を繰り返してページオブジェクト追加、出力と楽勝かなとか勝手に想像していたのですが大間違いでした。

つまり、Googleドキュメントは「ヘッダー」「フッター」「body」「フッターノート(?)」の4種類から構成されており、ページという概念がなさそうでした。
さらにメインの「body」は”ListItem”、”Paragraph”、”Table”、”TableOfContents”の4つの要素を持ちこれらはElemetという親子関係のオブジェクトで構成されている・・・と。
よくわかりません。

大きい意味で言うと、HTMLっぽい作りになっていますということでした。
細かいところは公式へ

で、コードに戻りますがまずはこれ

let insertedBodyElements = bodyA_copy.copy();
let insertedElements = insertedBodyElements.getNumChildren();
//・・・中略・・・
let element = insertedBodyElements.getChild(j).copy();

まずdocument.openbyIDか何かでGoogleドキュメントを変数にいれてます。
そのGoogleドキュメントの内容のうち、「body」を.getBody();で取得します。
で、そのbodyのコピーをinsertedBodyElements に入れています。

elementには”ListItem”、”Paragraph”、”Table”、”TableOfContents”が格納されます。

そこまでやっとテンプレの内容をコピーできるのですが、ペーストするのも大変。
ここでペーストしてます

switch (elementType) {
          case DocumentApp.ElementType.PARAGRAPH:
            if (element.asParagraph().getNumChildren() != 0 ) {
              switch(element.asParagraph().getChild(0).getType()) {
                case DocumentApp.ElementType.TEXT:
                case DocumentApp.ElementType.HORIZONTAL_RULE:
                  bodyB.appendParagraph(element.copy());
                  break;
                case DocumentApp.ElementType.PAGE_BREAK:
                  break;
                case DocumentApp.ElementType.INLINE_DRAWING:
                  var blob = element.asParagraph().getChild(0).asInlineDrawing();
                  bodyB.appendImage(blob);
                  bodyB.appendParagraph(element.copy());
                  break;
                case DocumentApp.ElementType.INLINE_IMAGE:
                  var blob = element.asParagraph().getChild(0).asInlineImage().getBlob();
                  bodyB.appendImage( blob );
                  bodyB.appendParagraph(element.copy().asParagraph());
                  break;
              }
            }else {
              bodyB.appendParagraph(element.copy());
            }
            break;
          case DocumentApp.ElementType.TABLE:
            bodyB.appendTable(element.copy());
            break;
          case DocumentApp.ElementType.LIST_ITEM:
            var elCopy = element.copy();
            var listItem = element.asListItem();
            bodyB.appendListItem(listItem.getText( )).setAttributes(elCopy.getAttributes()); 
            break;
          case DocumentApp.ElementType.INLINE_IMAGE:
            bodyB.appendImage(element.copy());
            break;
          default:
            break;
        }

switch文ですよ。個人的には凄いひさしぶりに使いました。

取得したelementの中身を繰り返しで全部さらっていきます。
しかもelementの中身も親子関係になっていたりするのでgetNumChildren()で入れ子になっている要素を確認しながら処理します。

エレメントのタイプがパラグラフであればテキストが入っていたり、テーブルであれば表が入っていたりするのですが、ペーストする際のメソッドがそれぞれ違う・・・
なのでスイッチでタイプ別にペースト処理を書いてあげなければいけません。
Googleドキュメント、いけてないですよね。。。

そしてテンプレ内容の追記ができたら繰り返しが回って、次のレコードの置換、またテンプレ記載して置換…を繰り返します。
レコードの内容がすべて回ったらbreak;して繰り返しを脱出します。

最初はページっていう概念でサクサクっと実装できると思ったのに、誤算でした。

日本語でググっても全然やりたいことがヒットしなかったのですが、英語でググったらそれっぽい記事がでてきて助かりました。

How can I generate a multipage text document from a single page template in google-apps-script?
I have to generate labels from a list of user data stored in a spreadsheet. Right now I get everything working nicely ex...

ChatGPTも英語で聞けたらズバッと解決してくれたかもしれませんが、AIが英語ベースになるとますます英語力が重要になってきそうなのが非英語圏としては辛いですね、、

コメント

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