D19 - 如何用 Apps Script 自动化地创造与客制 Google Docs?(六)更改特定内容格式的 Attribute 操作技巧

今天的目标

要怎麽抓出文件中的特定文字或段落,直接改字体的大小、颜色、背景、粗细与字型?先来看今天的结果之一,把文章中的「人」字都改成红字、粗体与放大——

今天,我们会教用 GAS 搭配 Goolge Doc 来设定,这个要动用到的元素叫做 Attribute。那因为在 Google Slide 中的 Element、Attribute 也很多有重叠,所以这边就会讲细一点,之後就可以一起服用。换句话说,今天会教说怎麽透过 GAS 调整 Google Doc 和 Google Slide 里面元素的「格式」。我们先复习一下前几天讲过的议题——

那今天,我们会针对上述议题中的「元素」进行。基本上前几天讲了上述元素的操作,今天终於可以进入到「格式调整」了,今天会专注在以下内容——

  1. 如何更改 Google Document 中元素的样式?

就让我们开始吧!

前情提要一下

考虑到有些夥伴不一定会都有细读的文章,就捞叨一点把基本步骤再次附上,如果会的夥伴可以直接跳到 Q5。

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

下图主要是列举几个常见用於操作 Element 的 Attribute。

这边就节录一本书中的「段落、照片、表格与清单」,来作为今天我们的范例。

好,大致理解基本概念後,就让我们开始吧。

Step 1 从 Document 中进入 GAS

那这次我们不会用 Google Sheet,而是直接用 Google Doc 进入,借一下 D16 的影片。

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

Step 2 设定好 getBody()

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

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

因为更新有比较复杂的细节,我们就先来讲讲删除。

Q5. 如何用 GAS 设定 Google Doc 的样式?

基本上之後的步骤会分成:

  1. 读取/新增目标元素(Element)
    1. 全部读取
    2. 新增元素
    3. 部分读取(Specific Range)
  2. 读取现有属性(getAttributes()
  3. 更改现有属性(setAttributes()

Step 3 读取/新增目标元素(Element)

3-1 和 3-2 主要就是 D16 Element 的读取与创造 的涵盖内容,这边就一样快速带给大家看。

Step 3-1 全部段落读取

这边就先上一段「读取」的程序码——

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

再来看读取的结果影片——

那如果要改成读取其他的 Element,可以参考之前的简易表单来做更换。

Step 3-2 新增元素

那如果我们今天要新增一段表格,可以怎麽做?我们可以运用 appendTable 的功能来执行。

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

跑起来长这样——

那如果要改成新增其他的 Element,可以参考之前的简易表单来做操作。

那以上这边如果觉得太快,或想知道更细部,记得可以回去看 D16 Element 的读取与创造 ,如果觉得时间OK,那我们就进入到新的部分,也就是「部分读取」。

Step 3-3 部分读取元素

比起整段做读取、更改,我们更常遇到的情况是只要更动部分。像是我想要把文件中的关键字抓出来标记颜色等、或是想要动从第三页到第八页的表格,详细要怎麽执行?

基本上第一部,我们都要先跟 GAS 说「我想抓出元素的___部分」。而这边就要用 findText() 来执行。

findText() 的基本使用架构如下,我们用来示范如何抓到段落中的「人」字。

function testFindText(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let target = '人';
  let searchResult = doc_body.findText(target);

  while (searchResult !== null) {
    Logger.log(searchResult.getElement().asText().getText());
    searchResult = doc_body.findText(target, searchResult);
  }
}

用法上, findText(searchPattern, from) 前面的 searchPattern 用的是 regex(严格说起来是 google 的 re2),我们就简单输入要找的文字;後面的 from 指的是当找到多个元素後,从哪一个开始?没输入的话预设会回传第一个,但在第一个之後如果要继续找,就要搭配 while 回圈并将 from 输入为前一次的搜寻结果。换句话说,就是把下图手刻的方式转成 while 回圈。

跑起来结果长这样——

那,从清单的只有回传两点发现,它确实有抓到「人」字,但是是回传了「人」字所在的整个元件(Element),如果是在段落(Paragraph)内,就回传段落,如果是在清单项目(ListItem)内就回传整个项目。那我们要怎麽抓出特定范围?这时就要提到 range 元素了。

range 元素与 offset 的概念

Range 是什麽?简单来说,当我们用滑鼠选取了一段内容後,都会伴随出range (当然省略掉 select 的部分,不过在这边我们先注重在 range)。

且每个 range 都有被伴随的 offset(位移),简单来说可以当成:是在段落中的哪个位置。

以上面的影片来说,「人」这个字即是位在第四个位置(Offset 为 3);更严格地说,「人」这个 range 的在段落中的开始位置(StartOffset) 和结束位置(EndOffset)都是为在 Offset 为 3的部分。

注意因为是程序语言,第一个位置我们要从 Offset 为 0 开始算。位移的概念可以参考如下

那为什麽这个重要?因为当我们等下要改内容时,这就会派上用场。实际上,对 GAS 来说,「部分选取」就是先选整段,再跟我说段落中的哪个位置(Offset)要改。我们快速示范如果要将 Offset 的文字都改成红色要怎麽做。

我们先来看看没上 Offset 的结果,会发现就等於整段改成红色。

同时看看上了 Offset 的结果,会发现只有我们要的「特定范围」变色。

实际上,我是透过 range 物件中的 getStartOffset()getEndOffsetInclusive() 达到的。先附上程序码,可以先专注看「Offset」的部分,我们马上就会来细讲改颜色的部分。

function highlightText() {
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    Logger.log(searchResult.getStartOffset() +' '+ searchResult.getEndOffsetInclusive())
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
}

好,那我们能抓到范围了,接下来就是要改属性了。但在改属性前,因为物件的属性算有点复杂,我个人是会建议先检查「有没有这个属性」。

Step 4 用 getAttributes() 来读取现有属性

通常我会先用 getAttributes() 来看我的目标物件有哪些可以调整的属性。这也可以帮助我们理解怎麽设定属性。这边写一段简单的程序,会印出目标「段落」、「清单项目」和「文件」的含有属性。

function testFindText(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let target = '人';
  let searchResult = doc_body.findText(target);

  while (searchResult !== null) {
    Logger.log(searchResult.getElement().asText().getText());
    Logger.log(searchResult.getElement().getAttributes());
    searchResult = doc_body.findText(target, searchResult);
  }

  Logger.log(doc_body.getAttributes())
}

跑起来长这样——

我这边把「段落」、「清单项目」与「文件」分开来看。

  1. 「段落」部分:
{STRIKETHROUGH=null, LINK_URL=null, FONT_FAMILY=Source Sans Pro, FONT_SIZE=null, UNDERLINE=null, BACKGROUND_COLOR=null, ITALIC=null, FOREGROUND_COLOR=null, BOLD=null}
  1. 「清单项目」部分
{LINK_URL=null, FOREGROUND_COLOR=null, BOLD=null, BACKGROUND_COLOR=null, UNDERLINE=null, STRIKETHROUGH=null, FONT_FAMILY=Source Sans Pro, FONT_SIZE=null, ITALIC=null}
  1. 「文件」部分
{MARGIN_TOP=21.25984251968504, LINK_URL=null, FONT_SIZE=null, UNDERLINE=null, STRIKETHROUGH=null, FOREGROUND_COLOR=null, FONT_FAMILY=null, BOLD=null, PAGE_HEIGHT=841.68, MARGIN_BOTTOM=72.0, MARGIN_LEFT=72.0, BACKGROUND_COLOR=null, PAGE_WIDTH=595.4399999999999, ITALIC=null, MARGIN_RIGHT=72.0}

这边要特别提一下我们抓出的资料结构,是一种叫做 dictionary 的结构。这边的重点会放在,所以如果我们要写入「属性」,我们也要套用这结构。

而其中的属性有三种特性——

  1. 越是上层(Parent)的物件,拥有的属性越多。子元素有的属性上层基本上都会有。
  2. 属性中的顺序不一定,但都是种 Key-value Pair(如果想知道原因,主要是因为资料结构是 Dictionary,其内部顺序不是重点)
  3. 跟 CSS 阶层一样,下层写死的属性会盖过上层的。这点应该算好理解,因为你设定全文为黑色时,仍是可以设定部分文字为红色。

特别把第一点拉出来说明,因为待会会用到这概念。

Step 5 用 setAttributes() 更改现有属性

好,那到底要怎麽改属性呢?我们先来一段程序码。

function setElementAttribute(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let style = {};
  style[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'
  let paragraphs = doc_body.getParagraphs();
  for(para of paragraphs){
    para.setAttributes(style);
  }
}

跑起来长这样——

这边先设定一个 dictionary 叫做 stylelet style = {}),并用 style[key]=value 设定期中的数值(精确来说是 Key-value pair),其中 key 的部分就是我们从 Step 4 中抓出来的属性们,只是前面多加了 DocumentApp.Attribute ;数值就是 Step 4 的数值们。

可以发现,就是将所有「段落」都改成红色。至於为什麽「清单」也会变色,复习是因为清单内的文字,其实也有包含一个段落的元素。好,那这是整段的更改,基本上就是要全部元素取得後,针对一个个元素抓出来设定。

那如果我们想做部分文字的更改呢?这边就可以用我们在 Step 3 最後面的段落,先贴上方便大家比较。

function highlightText() {
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    Logger.log(searchResult.getStartOffset() +' '+ searchResult.getEndOffsetInclusive())
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
}

基本上就是要在 setAttributes() 前加上「起始」与「结束」的 Offset,跑起来长这样。

好,那我们介绍了「单纯改字体颜色」,如果我们想调整的很多,要怎麽办?没问题的,这边直接上程序码,用「部分元素」做示范。

function highlightText() {
  // target,background
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'
  textStyle[DocumentApp.Attribute.FONT_FAMILY] = 'Calibri';
  textStyle[DocumentApp.Attribute.FONT_SIZE] = 18;
  textStyle[DocumentApp.Attribute.BOLD] = true;

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
  }
}

跑起来长这样,确定我们有改到「人」字的颜色、字体与粗体——

但如果想设定其他的参数呢?这边直接帮大家整理了表格。首先,如果是要改「部分文字」,可以用以下的功能们。

如果是要改「整个段落」,可以用以下的功能们。

如果是要改「文字」以外的部分,可以参考官方文件

好,那这边就是我们今天的内容。我有参考这篇:Can I color certain words in Google Document using Google Apps Script?,如果有夥伴想将上述功能改成有 UI 的 Add-On,里面有完整的程序码。另外,官方也有文件说明 Editing and styling text,也可以搭配参考。


好,那今天就到这边。今天我们主要交代了 Attribute 的「如何更新」,总算把 Document 的部分告一段落,明天会进入 Slide 的部分,最後会讲 Sheet。如果还有问题,透过留言之外,也可以到 Facebook Group,想开很久这次铁人赛才真的开起来,欢迎来当 Founding Member。如果不想错过可以订阅按赞小铃铛(?),也欢迎留言跟我说你还想知道什麽做法/主题。我们明天见。


<<:  [Day5]UTXO未花费的交易输出

>>:  [Day 7] 非监督式学习-降维

【没钱买ps,PyQt自己写】Day 22 - PyQt 视窗的个性化/属性控制 setWindowFlags,禁止放大缩小、永远显示於最上层/最下层

看完这篇文章你会得到的成果图 之前内容的重点复习 (前情提要) 我们接下来的讨论,会基於读者已经先读...

day6 初级系统工程师 (雷)管理眼花撩乱的机房,不是幸福

来部落格看图文并茂文章 补觉鸣诗 时间回到我入行第二年 这时才算是正式的系统工程师并开机接触机房 最...

[13th][Day30] 结语

其实我原本是打算 docker 跟 k8s 各暂一半篇幅的 ... 但 docker 看着看着 .....

追求JS小姊姊系列 Day25 -- 工具人、姐妹的存活原理:宣告变数的有效区域

前情提要: 看完记忆体储存差异,现在要来谈谈全域污染这件事。 基本scope概念 所谓的范畴Scop...

为什麽要使用VPN?综合3款VPN推荐给大家

最近有朋友回中国内地工作,刚好问我有什麽VPN推荐一下给他,需要在内地能翻墙,连脸书就可以了,了解他...