D15 - 如何用 Apps Script 自动化地创造与客制 Google Docs?(二)快速生出大量寄件信封资料

今天的目标

每逢过年过节,不时会收到些礼物或送出些礼物,但要怎麽样依据不同的对象,来客制化我们的内容。尤其当超过百人要送礼时,又要怎麽简单快速地做出文件?这时可以考虑用 GAS 搭配 Google Sheet 与 Goolge Doc。那,就让我们看看如何「只调整 Google Doc 中的一点点部分」?

  1. 要如何自动依照范本创造并微调 Google 文件(Google Docs)?

今天只有一个问题,昨天我们回答了如何简单客制化,今天我们会用「复杂范本」当案例,那就让我们就开始吧!


Q1. 要如何自动依照范本创造并微调 Google 文件(Docs)?

今天我们的情境是,要大量产生给客户的内容;像是我们最近要准备中秋的礼品们,请问要怎麽自动化大型的标签?可以用 Google Sheet 拉,但如果想要大一点,用半张纸来排版,能怎麽做?

Step 1 开启 Google Sheet,并串起 GAS

好,那因为我们要用到 Google Sheet ,所以一样用其作为开启的管道。一样借用 D8 的影片。

一样执行时会有「需要验证」出现,借用一下 D2 的影片。

Step 2 设定要作为 Template 的 Google Docs ID

关於创造文件,我们可以选择复制范本(像是 D11),或是创造新的文件(像是 D12);昨天我们讲解了「创造新文件」,这篇会针对「复制范本」来做介绍。那现在一样抓起范本的 ID,而以下是我们抓 ID 的两种方式——

每一个 Google 产品都有特定的 ID,可以用我们上述的方式简单取得,如果想复习,在 D9 有完整的介绍可以参考。

那我们做一个范本如下——

有看到收件者、地址与电话吗?我这边直接用一个变数代称,分别是 receiver、address 和 phone number,晚点这三个变数都用到。

将范本的 ID 抓出。

var template_doc_id= "your_id_here"

完整取得 ID 的方式在 D9 有详细的介绍可参考:如何用 Google Apps Script 自动化对 Google Drive 的操作?(一)列出所有档案 ID 与相关资讯

Step 3 复制范本复制并调整

那我们要怎麽做到复制范本并调整呢?

要先提到的是 Google Doc 中主要分成两大物件,一个是 Element,主要就是文字、影像、段落、表格等等的「纯内容」、骨架。一个是 Attribute,包含字体大小、排版、颜色、粗斜体等等,是 Element 的外皮、外衣与皮肤 。(概念上,有点像是 html element 和 css style )。今天我们会主要讲 ElementType 的部分

这边先用一段程序码示范给大家看。

function createDocFromTemplate(){
  let row_data = ['101','Amy','经理','0911111111','','House of Amy']
  let doc_file = DriveApp.getFileById(template_ID);
  let title = row_data[0] + " " + row_data[1] + " " + row_data[2];
  let contact = '';
  let new_doc_Id = doc_file.makeCopy(title).getId()
  let new_doc = DocumentApp.openById(new_doc_Id);
  let doc_body = new_doc.getBody();

  if (row_data[3]){
    contact= row_data[3]
  }else{
    contact= row_data[4]
  }

  Logger.log(title+ " "+ contact+ " "+ row_data[5])
  doc_body.replaceText("receiver", title);
  doc_body.replaceText("address", row_data[5]);
  doc_body.replaceText("phone number", contact);
}

这段程序码的意思是——

  1. 我先写一个预计会收到的资料 ['101','Amy','经理','0911111111','','House of Amy']
  2. 并将 Title 设定为前三个字串加在一起,以此为例就是 101 Amy 经理
  3. 用 Title 其作为档案的名称,注意这边我是针对前面 file Object.makeCopy(title) 的,如果对 Drive 不熟,可以参考 D8。
  4. 同时取得其 ID 後,再用 DocumentApp.openById 改成以 Document Object 操作。并用其抓出 body() 这边跟昨天的 D14 一样。
  5. 接着这段说明一下,因为原本的名单可能有两种电话,一种是手机号码 row_data[3],一种是市话 row_data[4],一种是没号码。我写了一段 for 回圈,用意是想说当有手机的话,联络资讯就留手机,没手机就留市话(是话与手机都没有的话,会填预设的市话 (row_data[4] ==""),也就是空白的字串。
  6. 最後,我用 replaceText 来取代掉 Template 里面的文字。

第六小点我多说明一些。我们使用的是 body Object 的 method,完整的写法是 replaceText(searchPattern, replacement)

  • 前面要放(要被取代的)pattern ,实际上是 regex 的表示法,如果不熟悉,可以当成「变数」来想像。 但实际上功能很强大,可以搜寻特字元。好,那同时在范本设定变数中,要小心设定,别设定文中可能有的文字,不然在没有设定好 Pattern 的情况下会一起被更换。
  • 後面要放(要用来取代的)replacement 内容。基本上就是文字

我们来看跑出来的结果——

好,那看起来没问题,那我们要怎麽大量执行。

Step 4 针对每一份资料复制并调整

那我们要长出什麽样子呢?这边先给大家看我们今天的参数。

接下来的步骤是:

  1. 取得资料
  2. 将一笔笔的资料与 Step 3 结合

一样,先来看看完整程序码。

var template_ID = "your_doc_template_id_here";

// 从 Google Sheet 的 B2 开取到 G7
function readSheetData(){
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getActiveSheet();
  let start_row = 2;
  let start_col = 2;
  let numRows = sheet.getLastRow() - start_row +1;
  let numCols = sheet.getLastColumn() - start_col +1;;
  let values = sheet.getRange(start_row,start_col,numRows,numCols).getValues();
  return values;
}

// 转接「读变数」与「写文件」
function writePages(){
  let data = readSheetData();
  for (row_data of data){
    createDocFromTemplate(row_data);
  }
}

// 微调的「写文件」功能
function createDocFromTemplate(row_data){
  let doc_file = DriveApp.getFileById(template_ID);
  let title = row_data[0] + " " + row_data[1] + " " + row_data[2];
  let contact = '';
  let new_doc_Id = doc_file.makeCopy(title).getId()
  let new_doc = DocumentApp.openById(new_doc_Id);
  let doc_body = new_doc.getBody();

  if (row_data[3]){
    contact= row_data[3]
  }else{
    contact= row_data[4]
  }

  Logger.log(title+ " "+ contact+ " "+ row_data[5])
  doc_body.replaceText("receiver", title);
  doc_body.replaceText("address", row_data[5]);
  doc_body.replaceText("phone number", contact);
}

取得资料部分,如果不懂为什麽有这样的架构、要这样写,可以参考 D4;写资料部分,则就单纯是把上面的 Step 3 改一下。

最後跑出来长这样——

好,那今天就完成了!事後有朋友问,那如果想要将很多档案合并起来怎麽办?不想要这麽分散。

Step 5

这边就直接做给大家看。简单来说,我们创造一份新文件(DocumentApp.create()),然後把每一份从 Template 复制的内容(template_file.getBody().copy())改完後丢回原本的新文件,再加上分页 (appendPageBreak())。

function moveFile(fileId, destinationFolderId) {
  let destinationFolder = DriveApp.getFolderById(destinationFolderId);
  DriveApp.getFileById(fileId).moveTo(destinationFolder);
}

function writePages(){
  let data = readSheetData();
  let new_doc_Id = DocumentApp.create("all_docs").getId();
  moveFile(new_doc_Id,folder_ID);
  let template_file = DocumentApp.openById(template_ID);
  let new_doc = DocumentApp.openById(new_doc_Id);
  for (row_data of data){
    mergeDocFromTemplate(row_data, new_doc, template_file);
    new_doc.appendPageBreak();
  };
}

function mergeDocFromTemplate(row_data, new_doc,template_file){
  let title = row_data[0] + " " + row_data[1] + " " + row_data[2];
  let contact = '';
  let copied_body = template_file.getBody().copy();


  if (row_data[3]){
    contact= row_data[3]
  }else{
    contact= row_data[4]
  }

  Logger.log(title+ " "+ contact+ " "+ row_data[5])
  copied_body.replaceText("receiver", title);
  copied_body.replaceText("address", row_data[5]);
  copied_body.replaceText("phone number", contact);

  let totalElements = copied_body.getNumChildren();
  for( let j = 0; j < totalElements; ++j ) {
      let element = copied_body.getChild(j).copy();
      new_doc.appendParagraph(element);
  }
}

那跑起来长这样——

比较特别要提的是,Google Docs 里面每一个段落、文字、影像等都是一个物件,这也是为何我们会需要先透过 .getNumChildren() 来抓出要复制的内容,再一个个的用 appendParagraph() 贴上。提醒的是,所谓 paragraph 对 word 来说,是按下一次 enter 就会算创造一个新的段落。

我们这边用的 appendParagraph() 是「仅限文字段落」的复制,如果要复制影像、Table、List 等其他格式等,可以参考这篇来实作: How to Merge Multiple Google Documents


今天的主题是为了应景中秋节看夥伴很辛苦地用名单,顺手写出来的。想说就赶紧先上应用版,明天再上完整辞典版。

一样提醒的是,创造文件有 Quota 限制——每天不超过 250 件。好,那今天就是我们的 D15,明天我会介绍要怎麽样操作更多的 Element。

如果还有问题,透过留言之外,也可以到 Facebook Group,想开很久这次铁人赛才真的开起来哈哈哈,欢迎来当 Founding Member。如果不想错过可以订阅按赞小铃铛(?),也欢迎留言跟我说你还想知道什麽做法/主题。我们明天见。


<<:  Day 15 读 Go Concurrency Patterns - Rob Pike II

>>:  # Day 6 Supporting PMUs on RISC-V platforms (二)

[早餐吃到饱 - 1] 薆悦酒店 - 五权馆(台中) #您有多久没有好好的吃顿早餐了呢~

今天用卤肉跟大家说声早安!! 薆悦酒店的早餐是有对外(非住客)开放的,但是要先预约。 今年的4月10...

单元测试(Unit testing)

-示例单元测试 单元测试通常由程序员开发。这是一个白盒测试。为了最大化单元测试的价值,伴随着敏捷方...

[Day18] TS:理解 Omit 的实作

是我们今天要聊的内容,老样的,如果你已经可以轻松看懂,欢迎直接左转去看同事 Ken 精彩的文章 —...

DAY 8 『 CollectionView 』Part1

CollectionView:Storyboard、Xib + Collection View + ...

Sorting Algorithms

排序演算法在程序中是非常重要的以下会先来介绍三个基本的排序演算法 Bubble sort Inser...