D16 - 如何用 Apps Script 自动化地创造与客制 Google Docs?(三)Element 的读取与创造

今天的目标

要怎麽简单快速地做出客制化地文件?今天,我们会教用 GAS 搭配 Goolge Doc。那因为在 Google Slide 中的 Element 也是相同的,所以这边就会讲细一点,之後就可以一起饮用。换句话说,今天会教说怎麽透过 GAS 调整 Google Doc 和 Google Slide 里面的元素。那今天的问题可以有以下的排列组合——

虽然总共有 4x4 共 16 种的排列组合,我们会用案例一个个来说明。基本上今天会先讲新增与读取,明天会讲更新与删除,就让我们开始吧!

先来了解 Google Docs 的架构

复习一下,我们已经知道大致上,每一个 Google 文件都会有 Element (元件),且每一个 Element 都会有 Attribute (属性)。今天我们主要会介绍蓝色的 Element 的部分。

那我们把 Element 展开来看,里面有很多小的物件,这边抓出其中最常用的四种。分别是段落、照片、表格与清单。

完整的架构概念,可以参考 Google 的官方文件

那今天,我们的目标就是透过读取、写入、更新与删除(对的,参照 CRUD 的 format)来带大家让是怎麽操作这些表格。这边就节录一本书中的「段落、照片、表格与清单」,来作为今天我们的范例。

那在开始前,先来设定一下...

Step 1 从 Document 中进入 GAS

那这次我们不会用 Google Sheet,而是直接用 Google Doc 进入。

一样第一次会有存取验证需要大家按一下。这边仍是借用一下 D2 的影片。

Step 2 设定好 getBody()

我们先用 getActiveDocument() 抓出正在绑定的文件;那假设我们都是针对主要内文(Body)的部分,所以我们先设定好 getbody()

let doc_body = DocumentApp.getActiveDocument().getBody();

如果是要对 header 或 footer 做就将上面改成 getHeader()getFooter()

如何读取 Google 文件中的段落(Paragraph)?

Step 3 读取文件中的段落

基本上在 Google Doc 中,只要有看到文字,外面就会包一层段落。

换句话说,在看到文字时有分成外层的「段落」(以 getParagraph() 获得)与段落内的「文字」(以 getText() 获得)。我们先直接用一段简单的 Code 来看执行起来会长什麽样子。

function readParagraph(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let paragraphs = doc_body.getParagraphs();
  Logger.log(paragraphs)
}

跑起来长这样——

可以发现,实际上读取出的 Paragraph 中的文字,会连表格、清单内的一起读出来,这是因所有「文字」外面的一层「段落」。另外值得注意的是,「空白列」也算是「段落」,理解上,可以想像在我们打字时,只要打字时按下「Enter」的部分,就是在创造一个新的 Paragraph,包括断行的空白。

好,那要如果要读取的话,基本上要用 getText() 但不能直接对全部的 paragraphs 直接用。而是要将每一个 paragraph 读出再使用。

function readParagraph(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  // (X)let paragraphs = doc_body.getParagraphs().getText();
  let paragraphs = doc_body.getParagraphs();
  for (let i=0;i<paragraphs.length;i++){
    Logger.log(paragraphs[i].getText())
  }
}

读起来像是这样子——

可以从影片中发现,对 Google Doc 来说,「表格内的每一个、清单内的每一项都是一个 Paragraph Object (我们才会读取到)。补充的是,表格内每一段要放文字的,外面都有再包一层 paragraph ,使用时要再小心。

好,那就顺便补充基本四大档案读取方式。

使用方式是只要将上方的 getParagraph() 换成四大档案的读取,就可以读取了。而特定段落、表单的读取,基本上都是先用这四大读取方式来取得再进行调整。

补充:findElement() 的使用说明

有些夥伴说问说有看到一个 API 叫 findElement() ,能用它来抓特定档案吗?

我们先来看一段程序码。请大家在往下看跑起来会长什麽样子之前,想一下,这段会有什麽样的结果输出?

function searchParagraph(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let paragraphType = DocumentApp.ElementType.PARAGRAPH;
  let paragraphs = doc_body.findElement(paragraphType).getElement();
  Logger.log(paragraphs.getText());
  for (let i=0;i<paragraphs.length;i++){
    Logger.log(paragraphs[i].getText())
  }
}

那跑起来是长这样——

可以发现,只有抓出第一个段落,那为什麽?这个 findElement() 的 API 确实可以帮我们抓到元素(Element),但不一定都那麽适合用,这个 API 被设计为,你想要抓「特定物件」中的「特定元素」。所以举凡

  1. 在 body 中抓 paragraph
  2. 在 header 中抓 image
  3. 在 table 中抓 list

都可以运用这个 API。但你会发现,这个 API 主要的目的是当「特定物件」不是常用的大架构如 Header、Body 与 Footer 的时候,要是在 Table 中抓 List,或是 Table 中抓 Table 才比较有用。不然我们可以很简单地套用 getParagraph()getImages() 等 API 直接取得。

此外, findElement() 有两种写法。一种是 findElement(type) ,这种写法帮助有限,原因是因为它就是直接抓出你的「第一个」段落,不会让你读取第二、第三段,除非你设定一个范围。也就是我上方那段程序码与影片所呈显的。而第二种写法,另一种则是 findElement(type, range),这会牵扯到 range 。而 range 的取得方式如下图。

function buildRange(){
    let doc = DocumentApp.getActiveDocument();
    let rangeBuilder = doc.newRange();
    let tables = doc.getBody().getTables();
    for (let i = 0; i < tables.length; i++) {
        if (tables[i].getNumChildren() == 4){
          rangeBuilder.addElement(tables[i]);
        }
    }
    return rangeBuilder.build()
}

function findImageInTablesWithFourRows(){
    let doc_body = DocumentApp.getActiveDocument().getBody();
    let imageType = DocumentApp.ElementType.IMAGE;
    let target_table = doc_body.findElement(imageType, buildRange()).getElement();
    // your function
}

这段程序码的意思是,我用 getRange() 抓出所有高度是四列的 Table,然後再用 findElement 回传里头含有的图片。但其实我们想用 findElement 做到的是,已经可以直接用 buildRange 那段的程序码抓到(直接对 table[i].children() 中去寻找 Type 等於 Image者),换句话说,再多写一段有点多此一举。但并不代表 getElement 很废,如果你想考虑的是当使用者在用滑鼠标记特定的文字段落时会很有用,这我们之後再聊。

而接着,如果我们想将照片写入的话,又要怎麽做?

如何写入 Google 文件中的照片(Image)?

在 Google Document 中,「加入」内容主要会分成:

  1. 新增:append()insert :在指定位置的前或後加入跟原本种类不同的(像是想在文字中插入图片)
  2. 更新:主要是改文字内容,那就用 set() 更改文件、内容,在下一段会讲到。

那在图片的部分,又主要分成两种:

  1. 跟文字排列的 InlineImage
  2. 漂浮於文字之上 or 下的 PositionedImage

好,那为了说明方便,我将原本的图片上传到 Google Drive 并取得其 ID。我们先来看看用 appendImage() 会长怎麽样子。

function addImage(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let image = DriveApp.getFileById(example_image_ID).getBlob();
  doc_body.appendImage(image)
}

跑起来长这样——

会发现图片确实有加入,且是预设的 InlineImage 但变得好大!所以我们要:

  1. 抓出其尺寸比例 (image.getWidth() / image.getHeight()
  2. 并依照页面的宽度(doc.getAttributes()['PAGE_WIDTH']
  3. 去进行调整 (image.setWidth() / image.setHeight()
function addImage(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let image = DriveApp.getFileById(example_image_ID).getBlob();
  let append_image = doc_body.appendImage(image)

  let page_width = doc_body.getAttributes()['PAGE_WIDTH'];
  let image_ration = append_image.getHeight() / append_image.getWidth();
  let image_new_width = page_width;
  let image_new_height = image_new_width * image_ration;
  append_image.setWidth(image_new_width);
  append_image.setHeight(image_new_height);
}

执行起来跑这样——

那其他部分的执行,大致上程序码在这,我们会找时间补齐的(遥望)

同场加应:如何写入 Google 文件中的清单(List)?

有遇到夥伴在写其他的时候遇到挑战,这边就一起示范一下怎麽用。 List 的部分就是不断用 Append List 来进行,直接上示范。

function addList(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let item1 = doc_body.appendListItem('Item 1');
  let item2 = doc_body.appendListItem('Item 2');
  item2.setNestingLevel(1)
  let item3 = doc_body.appendListItem('Item 3');
  item3.setNestingLevel(3)
}

那执行起来长这样——

同场加应:如何写入 Google 文件中的表单(Table)?

一起示范怎麽写表单。表单的逻辑跟 Google Sheet 一样,是写入 array in array。就直接示范了~

function addTable(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let cells = [[1,2,3],[4,5,6]];
  doc_body.appendTable(cells)
}

跑起来长这样——

同场加应:如何写入 Google 文件中的段落(Paragraph)?

而段落最简单,这边就直接放上程序码罗,应该可以很快套用。

function addParagraph(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  doc_body.appendParagraph("Sentences you want to share")
}

那跑起来长这样——

结论

好,那今天我们主要教了如何读取与写入,考量到篇幅,更新与删除我们留到之後分享;如果还有问题,透过留言之外,也可以到 Facebook Group,想开很久这次铁人赛才真的开起来哈哈哈,欢迎来当 Founding Member。如果不想错过可以订阅按赞小铃铛(?),也欢迎留言跟我说你还想知道什麽做法/主题。我们明天见。


<<:  JavaScript入门 Day11_有关数字的语法3

>>:  未来狂想:工业制造

[Day4] Face Detection - 使用Google Cloud Vision API

reference: medium - Filtering Image content with ...

图的拓扑排序 - DAY 29

说明 拓朴排序的图,不能为环 主要应用在专案是否可以照顺序进行 利用堆叠去找到流程 橘色数字为执行到...

[Day10]Cubes

上一篇介绍了Beat the Spread!,是一题算出平均值的题目,算是基本的一题。 今天讲解的题...

【day11】争厚厚切牛排

位於南港车站附近的争厚厚切牛排 也是我们常去的口袋名单之一 平日营业时间是两段式的 但像今天周日的话...

【Day30】App 开发以及中年职涯的选择

参考连结: 40岁软件工程师面试心得 中年大叔的失业危机 要怎麽面对? 中年转职选择: BI大数据v...