# ♚尼腓工作室♚ (二) 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)