---
# System prepended metadata

title: ♚尼腓工作室♚ (二) Google Apps Script(GAS)  Google Excel 效能優化

---

# ♚尼腓工作室♚ (二) Google Apps Script(GAS)  Google Excel 效能優化

## 前言
繼上篇文章[♚尼腓工作室♚ 利用Google Apps Script(GAS) 整理雲端硬碟資料到Excel](https://hackmd.io/@7o8lFAJGTgqk11Nd7vHtdw/B1TqrYGmU)，寫好的腳本已經能自動整理雲端硬碟的資料並輸出到Excel表單中，但隨著資料量的增加，有時就會出現逾時無法將指令跑完的問題。

![](https://i.imgur.com/UhOvDzO.png)

因為GAS避免濫用對腳本有限制，而我遇到時間限制如下圖，詳細的限制條件可參考 : [這裡](https://developers.google.com/apps-script/guides/services/quotas)。

![](https://i.imgur.com/bLuYX79.png)

可以看得出來Google對於教育非常的重視，提供學生與企業相同的配額，但30分鐘還沒跑完....就代表需要優化程式了~

## 程式碼與流程圖
先來回顧一下我們的程式碼與流程圖。
```Javascript=1
function myFunction() {
var app = SpreadsheetApp.openById("1YhaO1Jb-KBJhIDOJnD_sKyWUhHdOhMyB1HzAjviYLFA");// 試算表金鑰
app.setName("動漫資料庫")
var sheet = app.getSheets()[0];//EXCEL第一表格(資料夾)[資料夾名、資料夾金鑰、圖片名、圖片網址]
var range = sheet.getDataRange();//取得ㄉ表格內內容
var values = range.getValues();//取得內容中的資料
sheet.setName("動漫資料夾")
var sheet2 = app.getSheets()[1];//EXCEL第二表格(影片總級數)[資料夾名、、影片金鑰、影片名、影片網址]
var range2 = sheet2.getDataRange();//取得表格內內容
var values2 = range2.getValues();//取得內容中的資料
sheet2.setName("影片資料庫")

var ImageFormat=["jpg","jpeg","png","tif","tiff","bmp"]//圖片格式
var Videoformat=["mp4","mkv","rmvb"]//影片格式

//存取表單現有內容

var FolderName=[];
for (var i = 0; i < values.length; i++) {
  FolderName[i] = values[i][0];
}
var VideoName=[];
for (var i = 0; i < values2.length; i++) {
  VideoName[i] = values2[i][2];
}
    var videoFolder = DriveApp.getFolderById("1YlkZ_iBFmUCbCTC_MQGb8DdpYE0bn0hd");//影片資料夾金鑰
    var foldersInVideoFolder = videoFolder.getFolders();//獲取目錄中所資料夾的集合
    var folder;
    var folderID =[];//影片資料夾金鑰

    for (var i = 0; foldersInVideoFolder.hasNext(); i++)  //hasNext()，確定調用是否next()將返回一個項目
    {
        folder = foldersInVideoFolder.next();//next()獲取文件或文件夾集合中的下一項。
        folderID[i] = folder.getId();//在 folderID[]中存影片資料夾金鑰
        //var data =[folder.getName(),folder.getId()]
        // sheet.appendRow(data);
    }
      for (var i = 0; i < folderID.length; i++){
      var folder = DriveApp.getFolderById(folderID[i]);//folder讀取folderID[]中存影片資料夾金鑰
      var files = folder.getFiles();//getFiles()獲取目錄中所有文件的集合
      var file; 
      var DataQuantity=0,run=0;
   for(var j=0;files.hasNext();j++){
      file=files.next();
      DataQuantity++;
      dotSplit = file.getName().split("."); //dotSplit取得文件名稱，並以"."來分割
      
    var count=0; count2=0//count圖片格式，count2影片格式
    
    while((count<ImageFormat.length)){
    
    if(dotSplit[dotSplit.length-1]==ImageFormat[count]){//判斷副檔名是否為圖片格式
    var iia =0;
    while(iia<FolderName.length){
    if(folder.getName()==FolderName[iia]){
    sheet.getRange(iia+1,1).setValue(folder.getName());
    sheet.getRange(iia+1,2).setValue(folder.getId());
    sheet.getRange(iia+1,3).setValue(file.getName());
    sheet.getRange(iia+1,4).setValue(file.getUrl());
    
    iia=FolderName.length;
    }else{//沒有相同名稱
    iia++
    if(iia==FolderName.length){
    var data =[folder.getName(),folder.getId(),file.getName(),file.getUrl()]
    sheet.appendRow(data);
    }
    }
    }
    //var data =[folder.getName(),folder.getId(),file.getName(),file.getUrl()]
    //sheet.appendRow(data);
    count=ImageFormat.length;//成功輸出過就暫停
    
    }else{count++,run++
    
    while(count2<Videoformat.length){
    
    if(dotSplit[dotSplit.length-1]==Videoformat[count2]){//判斷是否為影片
    var iic=0;
    while(iic<VideoName.length){
    if(file.getName()==VideoName[iic]){
    sheet2.getRange(iic+1,1).setValue(folder.getName());
    sheet2.getRange(iic+1,2).setValue(file.getId());
    sheet2.getRange(iic+1,3).setValue(file.getName());
    sheet2.getRange(iic+1,4).setValue("https://drive.google.com/file/d/"+file.getId()+"/preview");
    //sheet2.getRange(iic+1,4).setValue(file.getUrl());
    iic=VideoName.length;
    }else{
    iic++;
    if(iic==VideoName.length){
    var data =[folder.getName(),file.getId(),file.getName(),file.getUrl()]
    sheet2.appendRow(data);
    }
    }
    }
    
    //var data =[folder.getName(),file.getId(),file.getName(),file.getUrl()]
    //sheet2.appendRow(data);
    count2=Videoformat.length;
    }else{count2++;}
    }
    }   //繼續判斷副檔名
    }  
}

if(DataQuantity==run/ImageFormat.length){//如果資料夾中沒有圖片，判斷是否重複
  var iib=0;
  while(iib<FolderName.length){
  if(folder.getName()==FolderName[iib]){
//
//sheet.createTextFinder(folder.getName()).replaceAllWith(folder.getName(),folder.getId());//更新相同動但名稱資訊
sheet.getRange(iib+1,1).setValue(folder.getName());
sheet.getRange(iib+1,2).setValue(folder.getId());

  iib=FolderName.length;
  }else{//沒有相同名稱就新增
  iib++;
  if(iib==FolderName.length) {
  
  var dara=[folder.getName(),folder.getId()];
  sheet.appendRow(dara);
        }
  }    
    } 
      }    
        }
    //因為影片很亂所以來做排序
    sheet.sort(1);//影片資料夾名按照資料夾名稱排序
    sheet2.sort(3).sort(1);//動漫資料庫先以片名排序後再依照資料夾名稱排序
}

```

流程圖
![](https://i.imgur.com/i5EDwGj.png)
## 第一步 找出問題所在
在Excel完全空白的情況下執行約8XX秒執行完成，而當Excel中已經有資料時執行腳本會比對資料來進行更新，花費的時間約9XX秒甚至會超過限制1800秒，進行資料比對時拖累整體的速度，可能的原因有以下幾點。

* ### 腳本迴圈過多
我編寫的GAS腳本有許多的迴圈，可能影響整體的運行速度，例
```javascript=13
var ImageFormat=["jpg","jpeg","webp"]//圖片格式
```
```avascript=51
while((count<ImageFormat.length)){
    
if(dotSplit[dotSplit.length-1]==ImageFormat[count]){//判斷副檔名是否為圖片格式
```
**說明**:```code 13```將副檔名放在陣列中，```code 51```行再依序使用迴圈來比對，而迴圈可能會增加運行時間，但理論上應該影響不大，~~如果這種迴圈會影響那他的電腦就太破了~~。

* ### 網路資源消耗
因為要判斷雲端硬碟中資料與Excel表單的資料是否重複，所以利用迴圈進行比對，這些指令會像雲端硬碟提出連線要求，如果這個方法又寫在迴圈中造成不必要的資源浪費，例
```javascript=51
    while((count<ImageFormat.length)){
    
    if(dotSplit[dotSplit.length-1]==ImageFormat[count]){//判斷副檔名是否為圖片格式
    var iia =0;
    while(iia<FolderName.length){
    if(folder.getName()==FolderName[iia]){
```
 ```code 56``` 的 ```folder.getName()``` 會像雲端硬碟連線請求檔案名稱，總共地連線次速約=總檔案數*(1+總檔案數)/2，圖表如下。

![](https://i.imgur.com/sX96QaJ.png)

隨著檔案增加，連線次數會以指數性暴增造，很有可能就是拖慢腳本執行速度的罪魁禍首。


* ### 判定檔案順序不佳
在我的資料中有圖片與影片兩種檔案需要進行判定，判定的方法就是在自己設的迴圈中新增該檔案的副檔名，如下方的程式碼。
```javascript=13
var ImageFormat=["jpg","jpeg","png","tif","tiff","bmp"]//圖片格式
var Videoformat=["mp4","mkv","rmvb"]//影片格式
```

經過先前的腳本運行可以知道圖片與影片的比例如下圖。

![](https://i.imgur.com/re0xySp.png)

由此可知影片的比例較多，但先前的腳本會先判定檔案是否為圖片，這樣就有94.3%(影片)多跑了圖片判定，造成速度的拖慢。





## 第三部 問題探討
### 迴圈過多解決辦法
```avascript
if(dotSplit[dotSplit.length-1]="jpg"||
dotSplit[dotSplit.length-1]="jpeg"||
dotSplit[dotSplit.length-1]="webp"||){
輸出資料}
```
可以利用 ```if``` 的條件中 ```||``` 這個方式減少 ```While```、```for``` 等迴圈執行次數，但經過測試影響不大。
### 網路資源消耗 解決辦法
```javascript
while((count<ImageFormat.length)){
if(dotSplit[dotSplit.length-1]==ImageFormat[count]){//判斷副檔名是否為圖片格式
var iia =0,var temp=folder.getName();
while(iia<FolderName.length){
if(temp==FolderName[iia]){
```
在迴圈外先宣告 ```var temp= folder.getName();``` 在迴圈內需要比較時利用 ```temp``` 比對就不會占用額外的網路資源，大大的減少運行時間。
### 判定檔案順序不佳 解決辦法
雖然就只需要將判定圖片與影片的程式碼調換順序，但由於先前的程式在編寫時是多個迴圈包再一起，直接調換順序容易出現錯，以及後續的維護不易，所以我打算以模組化的方式重新編寫，方便日後的更新與優化。
## 第四部 修正問題 
按照**第三部問題探討中**的優化方式我花了一些時間調整程式碼，又順帶發現了一個BUG，這邊檔案的判定順序是以堆疊(Stack)進行，愈晚上傳的影片會愈早讀取，然而有些資料夾在上傳資料時事先上傳圖片之後才上傳影片，所以就會造成這種狀況如下圖。

![](https://i.imgur.com/bTnG9bg.png)

這個原因就是第一個讀取的是影片，不是圖片，所以腳本判定該資料夾無圖片，但之後又發現有圖片造成兩次輸出，所以我多新增了 ```run``` 與 ```totocount``` 兩個變數， ```rum``` 在輸出圖片或影片時就會+1，而 ```totocount``` 會等於該資料夾的檔案數量，無圖片的判定必須要```run=totocount``` 時才會輸出，這樣就不會有圖片與無圖片條件都成立造成資料不一致的問題。


### 修正後執行成果
Excel完全空白執行腳本
![](https://i.imgur.com/SjSqOxH.png)
第二次執行時間
![](https://i.imgur.com/98GZ3vX.png)
### 修正後程式碼_Bata
```javascript=
function myFunction() {

var app = SpreadsheetApp.openById("1YhaO1Jb-KBJhIDOJnD_sKyWUhHdOhMyB1HzAjviYLFA");// 試算表金鑰
app.setName("動漫資料庫")
var sheet = app.getSheets()[0];//EXCEL第一表格(資料夾)[資料夾名、資料夾金鑰、圖片名、圖片網址]
var range = sheet.getDataRange();//取得ㄉ表格內內容
var values = range.getValues();//取得內容中的資料
sheet.setName("動漫資料夾")
var sheet2 = app.getSheets()[1];//EXCEL第二表格(影片總級數)[資料夾名、、影片金鑰、影片名、影片網址]
var range2 = sheet2.getDataRange();//取得表格內內容
var values2 = range2.getValues();//取得內容中的資料
sheet2.setName("影片資料庫")

var Imagetemp=["jpg","jpeg","png","webp","tif","tiff","bmp"]//圖片格式
var Videotemp=["mp4","mkv","rmvb","avi"]//影片格式

//存取表單現有內容

var FolderName=[];//取得Excel第一張表格資料夾名
for (var i = 0; i < values.length; i++) {
    FolderName[i] = values[i][0];}
var VideoName=[];//取得Excel第二張表格影片檔名
for (var i = 0; i < values2.length; i++) {
    VideoName[i] = values2[i][2];}  
var videoFolder = DriveApp.getFolderById("1YlkZ_iBFmUCbCTC_MQGb8DdpYE0bn0hd");//影片資料夾金鑰
var foldersInVideoFolder = videoFolder.getFolders();//獲取目錄中所資料夾的集合
var folder;
var folderID =[];//影片資料夾金鑰

for (var i = 0; foldersInVideoFolder.hasNext(); i++){  //hasNext()，確定調用是否next()將返回一個項目
folder = foldersInVideoFolder.next();//next()獲取文件或文件夾集合中的下一項。
folderID[i] = folder.getId();//在 folderID[]中存影片資料夾金鑰 
}
for (var i = 0; i < folderID.length; i++){
var folder = DriveApp.getFolderById(folderID[i]);//folder讀取folderID[]中存影片資料夾金鑰
var files = folder.getFiles();//getFiles()獲取目錄中所有文件的集合
var file; 
for(var j=0;files.hasNext();j++){
file=files.next();
dotSplit = file.getName().split("."); //dotSplit取得文件名稱，並以"."來分割

var count=0,outputcount=0,range=0,temp=dotSplit[dotSplit.length-1],Name;
var run=0,totocount=folder.getFiles().length;
//判斷影片
while((outputcount==0)&&(count<Videotemp.length)){
if(temp==Videotemp[count]){//判斷副檔名是否為影片
Name=file.getName();
count=Videotemp.length;
outputcount=1;
}else{count++}}
//確認檔案為影片，開始判斷Excel中是否有重複
while((outputcount==1)&&(range<VideoName.length)){
if(Name==VideoName[range]){
sheet2.getRange(range+1,1).setValue(folder.getName());
sheet2.getRange(range+1,2).setValue(file.getId());
sheet2.getRange(range+1,3).setValue(file.getName());
sheet2.getRange(range+1,4).setValue("https://drive.google.com/file/d/"+file.getId()+"/preview");
outputcount++,run++;}else{range++;
if(range==VideoName.length){
var data =[folder.getName(),file.getId(),file.getName(),"https://drive.google.com/file/d/"+file.getId()+"/preview"];
sheet2.appendRow(data);
outputcount++,run++;}    }     }    //影片判斷結束
//圖片判斷開始
count=0;
while((outputcount==0)&&(count<Imagetemp.length)){
if(temp==Imagetemp[count]){//判斷副檔名是否為圖片
Name=folder.getName();
count=Videotemp.length;
outputcount=1;
}else{count++}}
//確認檔案為圖片，開始判斷Excel中是否有重複
while((outputcount==1)&&(range<FolderName.length)){
if(Name==FolderName[range]){
sheet.getRange(range+1,1).setValue(folder.getName());
sheet.getRange(range+1,2).setValue(folder.getId());
sheet.getRange(range+1,3).setValue(file.getName());
sheet.getRange(range+1,4).setValue(file.getUrl());
outputcount++,run++;}else{range++;
if(range==FolderName.length){
var data =[folder.getName(),folder.getId(),file.getName(),file.getUrl()]
sheet.appendRow(data);
outputcount++,run++;}    }     }    //有圖片判斷結束
//開始判斷無圖片
Name=folder.getName();
while((outputcount==0)&&(run==totocount)&&(range<FolderName.length)){
if(Name==FolderName[range]){
sheet.getRange(range+1,1).setValue(folder.getName());
sheet.getRange(range+1,2).setValue(folder.getId());
outputcount++,run++;}else{range++}}
if((outputcount==0)&&(range==FolderName.length)){
var data =[folder.getName(),folder.getId()];
sheet.appendRow(data);
outputcount++;}//無圖片判斷結束
 }   }  
sheet.sort(1);//影片資料夾名按照資料夾名稱排序
sheet2.sort(3).sort(1);//動漫資料庫先以片名排序後再依照資料夾名稱排序
 }
```
### 再次發現問題
我發現了Excel中有無資料會影響整體腳本的速度，原因是更新內容與新增內容的方法是不同的。
```javascript=
//新增的程式碼
var data =["文字一","文字二","文字三"];
sheet2.appendRow(data);
//更新的程式碼
sheet2.getRange(Excel行數,Excel列數).setValue("文字一");
```
我猜會影響速度是因為appendRow這個方法會向表單傳送指令要求回傳Excel中最下方無資料的表格，之後再進行輸出，這個作法可能會有網路延遲，而getRange這個方法必須要自己設定 (行數,列數) 這樣就不用像表單請求資料，所以速度比較快。

於是我在宣告兩個變數 ```FolderNamerange``` 與 ```VideoNamerange``` 讓分別等於Excel中圖片與影片的行數。

```javascript
var FolderNamerange=FolderName.length;
var VideoNamerange=VideoName.length;
```

```javascript
//舉例原本新增的方式，(速度較慢)
//var data =["文字一","文字二","文字三"];
//sheet2.appendRow(data);

//改成效能更佳的方式
sheet2.getRange(VideoNamerange+1,1).setValue("文字一");
sheet2.getRange(VideoNamerange+1,2).setValue("文字二");
sheet2.getRange(VideoNamerange+1,3).setValue("文字三");
//VideoNamerange+1是因為陣列會從0開始計算，+1才會在Excel中正常輸出
VideoNamerange++;
//輸出完VideoNamerange++讓下一筆需要新增資料可以跑到下一欄位，不會覆蓋到原本資料
```


### 優化後程式碼

優化後比較不同的是 ```appendRow("data");``` 會看到執行過程，如下圖，但整體速度較慢。

![](https://i.imgur.com/SgZpTwu.gif)

而 ```getRange(x,y).setValue("data");``` 要整體跑完後才會一次性顯示出來。

![](https://i.imgur.com/MnxAMYd.gif)

運行速度上無論是第一次跑或者後續更新都很穩定約92秒完成，程式碼優化後快了將近12倍，不再有超時問題。

![](https://i.imgur.com/W7Jg5Cp.png)

最終整理資料的腳本如下
```javascript=
function myFunction() {

var app = SpreadsheetApp.openById("1YhaO1Jb-KBJhIDOJnD_sKyWUhHdOhMyB1HzAjviYLFA");// 試算表金鑰
app.setName("動漫資料庫")
var sheet = app.getSheets()[0];//EXCEL第一表格(資料夾)[資料夾名、資料夾金鑰、圖片名、圖片網址]
var range = sheet.getDataRange();//取得ㄉ表格內內容
var values = range.getValues();//取得內容中的資料
sheet.setName("動漫資料夾")
var sheet2 = app.getSheets()[1];//EXCEL第二表格(影片總級數)[資料夾名、、影片金鑰、影片名、影片網址]
var range2 = sheet2.getDataRange();//取得表格內內容
var values2 = range2.getValues();//取得內容中的資料
sheet2.setName("影片資料庫")

var Imagetemp=["jpg","jpeg","png","webp","tif","tiff","bmp"]//圖片格式
var Videotemp=["mp4","mkv","rmvb","avi"]//影片格式

//存取表單現有內容
var FolderName=[];//取得Excel第一張表格資料夾名
for (var i = 0; i < values.length; i++) {
    FolderName[i] = values[i][0];}
var VideoName=[];//取得Excel第二張表格影片檔名
for (var i = 0; i < values2.length; i++) {
    VideoName[i] = values2[i][2];}  
var FolderNamerange=FolderName.length,VideoNamerange=VideoName.length;
var videoFolder = DriveApp.getFolderById("1YlkZ_iBFmUCbCTC_MQGb8DdpYE0bn0hd");//影片資料夾金鑰
var foldersInVideoFolder = videoFolder.getFolders();//獲取目錄中所資料夾的集合
var folder;
var folderID =[];//影片資料夾金鑰

for (var i = 0; foldersInVideoFolder.hasNext(); i++){  //hasNext()，確定調用是否next()將返回一個項目
folder = foldersInVideoFolder.next();//next()獲取文件或文件夾集合中的下一項。
folderID[i] = folder.getId();//在 folderID[]中存影片資料夾金鑰 
}
for (var i = 0; i < folderID.length; i++){
var folder = DriveApp.getFolderById(folderID[i]);//folder讀取folderID[]中存影片資料夾金鑰
var files = folder.getFiles();//getFiles()獲取目錄中所有文件的集合
var file; 
for(var j=0;files.hasNext();j++){
file=files.next();
dotSplit = file.getName().split("."); //dotSplit取得文件名稱，並以"."來分割

var count=0,outputcount=0,range=0,temp=dotSplit[dotSplit.length-1],Name;
var run=0,totocount=folder.getFiles().length;

while((outputcount==0)&&(count<Videotemp.length)){//判斷影片
if(temp==Videotemp[count]){//判斷副檔名是否為影片
Name=file.getName();
count=Videotemp.length;
outputcount=1;
}else{count++}}
while((outputcount==1)&&(range<VideoName.length)){//確認檔案為影片，開始判斷Excel中是否有重複
if(Name==VideoName[range]){
sheet2.getRange(range+1,1).setValue(folder.getName());
sheet2.getRange(range+1,2).setValue(file.getId());
sheet2.getRange(range+1,3).setValue(file.getName());
sheet2.getRange(range+1,4).setValue("https://drive.google.com/file/d/"+file.getId()+"/preview");
outputcount++,run++;}else{range++;
if(range==VideoName.length){
sheet2.getRange(VideoNamerange+1,1).setValue(folder.getName());
sheet2.getRange(VideoNamerange+1,2).setValue(file.getId());
sheet2.getRange(VideoNamerange+1,3).setValue(file.getName());
sheet2.getRange(VideoNamerange+1,4).setValue("https://drive.google.com/file/d/"+file.getId()+"/preview");
outputcount++,run++,VideoNamerange++;}    }     }    //影片判斷結束
count=0;//圖片判斷開始
while((outputcount==0)&&(count<Imagetemp.length)){
if(temp==Imagetemp[count]){//判斷副檔名是否為圖片
Name=folder.getName();
count=Videotemp.length;
outputcount=1;
}else{count++}}
while((outputcount==1)&&(range<FolderName.length)){//確認檔案為圖片，開始判斷Excel中是否有重複
if(Name==FolderName[range]){
sheet.getRange(range+1,1).setValue(folder.getName());
sheet.getRange(range+1,2).setValue(folder.getId());
sheet.getRange(range+1,3).setValue(file.getName());
sheet.getRange(range+1,4).setValue(file.getUrl());
outputcount++,run++;}else{range++;
if(range==FolderName.length){
sheet.getRange(FolderNamerange+1,1).setValue(folder.getName());
sheet.getRange(FolderNamerange+1,2).setValue(folder.getId());
sheet.getRange(FolderNamerange+1,3).setValue(file.getName());
sheet.getRange(FolderNamerange+1,4).setValue(file.getUrl());
outputcount++,run++,FolderNamerange++;}    }     }    //有圖片判斷結束
//開始判斷無圖片
if(outputcount==0){
Name=folder.getName();
while((run==totocount)&&(range<FolderName.length)){
if(Name==FolderName[range]){
sheet.getRange(range+1,1).setValue(folder.getName());
sheet.getRange(range+1,2).setValue(folder.getId());
outputcount++,run++;}else{range++}}
if((outputcount==0)&&(range==FolderName.length)){
sheet.getRange(FolderNamerange+1,1).setValue(folder.getName());
sheet.getRange(FolderNamerange+1,2).setValue(folder.getId());
outputcount++,FolderNamerange++;}}//無圖片判斷結束
 }   }  
sheet.sort(1);//影片資料夾名按照資料夾名稱排序
sheet2.sort(3).sort(1);//動漫資料庫先以片名排序後再依照資料夾名稱排序
 }
```
## 結論
整理資料的腳本到這邊就告一段落了，理論上就算再塞入更多的檔案也不會造成超時的問題，寫程式真的需要謹慎的思考，看來我還要多練習，找Bug真的好累阿，下一步就是就是架設資料庫囉。

![](https://i.imgur.com/qzJABA6.png)
