【问题标题】:compare two spreadsheet and output the difference using google app scripts比较两个电子表格并使用谷歌应用脚​​本输出差异
【发布时间】:2017-06-06 13:53:07
【问题描述】:

好吧,我正在尝试做标题中描述的事情。两个电子表格只有一张我正在比较的表格。一个电子表格是另一个电子表格的更新,所以我试图只获取新内容。 (如果它是一个类似 fc(dos 命令)的函数,这将很容易......)

在进行了一些搜索之后,我有了以下脚本,它应该适用于大多数情况,它为每张纸使用数组。

function test() {
  var Folder = DriveApp.getFoldersByName('theFolder').next();
  var FolderId =Folder.getId();
  //call old_spreadsheet
  var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var old_file = files.next();   
  var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
  var old_sheet = old_spreadsheet.getSheets()[0];
  var old_sheetname = old_sheet.getName();
  var old_array = old_sheet.getDataRange().getValues();
  Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);
  //call spreadsheet
  var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var file = files.next();   
  var spreadsheet = SpreadsheetApp.openById(file.getId());
  var sheet = spreadsheet.getSheets()[0];
  var sheetname = sheet.getName();
  var array = sheet.getDataRange().getValues();
  Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length);  
  var newarray = getNewData(array,old_array);
  Logger.log('there are ' + newarray.length + 'different rows');
}

function getNewData(array1,array2){    
  var diff =array2;   
  for (var i = 0; i<array1.length; i++){
    var duplicate = false;
    for (var j = 0;j<diff.length;j++){
      if (array1[i].join() == diff[j].join()){
        Logger.log('duplicated line found on rows ' + i + ':' + j);
        diff.splice(j,1);
        var duplicate= true;
        break;
      }    
    }
    if (duplicate==false) {
      Logger.log('not duplicated line found on row ' + i);
      diff.push(array1[i]);            
    }
  }
  return diff;
}

问题是文件太大了,差不多 30000 行,所以脚本超过了 5 分钟的执行限制。

有没有办法改善这一点,例如消除内部 for 循环? 还是有办法分部分做?比如前 5000 行,以此类推。

问候,

编辑:稍微分析电子表格后,我发现每一行都有一个 ID,所以现在我只能将搜索集中在每个电子表格的一列中。所以这是我的新实现:

function test(){
var Folder = DriveApp.getFoldersByName('theFolder').next();
  var FolderId =Folder.getId();
  //call old_spreadsheet
  var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var old_file = files.next();   
  var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
  var old_sheet = old_spreadsheet.getSheets()[0];
  var old_sheetname = old_sheet.getName();
  var old_array = old_sheet.getDataRange().getValues();
  Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);
  //call spreadsheet
  var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var file = files.next();   
  var spreadsheet = SpreadsheetApp.openById(file.getId());
  var sheet = spreadsheet.getSheets()[0];
  var sheetname = sheet.getName();
  var array = sheet.getDataRange().getValues();
  Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length); 
  //The COlumn has an indicator, so i search for that. I don't control the formatting of the files, so i search in both spreadsheet for the indicator
  var searchString = 'NAME';
  for (var i = 0; i < old_array.length; i++) {    
    for (var j = 0; j < old_array[i].length; j++) {    
      if (old_array[i][j] == searchString) {
        var Row_old = i+1;
        var Column_old = j;
        break;
      }      
    }
    if (Row_old != undefined){
      break;
    }
  }
  for (var i = 0; i < array.length; i++) {    
    for (var j = 0; j < array[i].length; j++) {    
      if (array[i][j] == searchString) {
        var Row = i+1;
        var Column = j;
        break;
      }      
    }
    if (Row != undefined){
      break;
    }
  }

  Logger.log(Row_old+':::'+Column_old+'\n'+Row+':::'+Column);  

  var diff_index =[];
  var row_ind = 0;  
  for (var i=Row;i<array.length;i++){        
    Logger.log(i);
    var existe = ArrayLib.indexOf(old_array, Column_old, array[i][Column]);
    if (existe==-1){      
      Logger.log(row_ind+'!!!');
      diff_index[row_ind]=i;
      row_ind++;          
    }
  }
  Logger.log(diff_index);
}

这仍然没有时间......我现在将尝试合并您的 cmets。

【问题讨论】:

  • 如果总是在相同的两个文件上运行,则获取ID out of the file's URL 并直接打开文件,而不是在驱动器中搜索它们。
  • 我不明白你直接打开的意思...但是,文件每天都会更新新信息(带有新信息(表)的新文件,下载/上传到驱动器并且旧文件在其名称后附加了_old,这是我要比较它们的地方)
  • 在您的代码中,您正在遍历文件并查找相关文件,而不是这样做,最好使用分配给它的唯一 ID 打开电子表格。打开电子表格时,您可以在 URL 中找到它
  • 好的。我不能这样做,因为文件每天都会更新(通过下载)(所以它们实际上是不同的文件)。我重命名它们以保留名称,但 ID 每天都在变化。此外,搜索这两个文件几乎不需要任何时间,所有执行时间都在for循环中。
  • 检查 Best Practices 以获取电子表格。它提到了优化技巧,如使用批处理操作、避免在 UI 繁重的脚本中使用库和使用缓存服务。

标签: google-apps-script google-sheets


【解决方案1】:

您的脚本有几个主要瓶颈会大大减慢它:

  • 每次从 0 开始两个循环都会导致其运行时间爆炸
  • 每次找到重复项时都需要将数组四处移动
  • 字符串在每次迭代时连接一个数组

我们可以通过以下方式规避这些问题:

  • 对第二个范围进行一次排序
  • 我确信通过对每一列进行迭代二进制搜索可以完成一些聪明的事情,但我们每次都必须使用二进制搜索第一列,然后进行线性搜索。

我们将使用ArrayLib 进行排序(我希望它是一种快速排序算法)。

让我们从一个函数开始,找到第一列与某个值匹配的第一行(当前行的第一列):

function firstRowMatchingCol1(target, lookupRange) {
  var min = 0;
  var max = lookupRange.length - 1;
  var guess;
  var guessVal;

  while(min <= max) {
    guess = (min + max) / 2 | 0;
    guessVal = lookupRange[guess][0];

    if (guessVal < target) {
      min = guess + 1;
    } else if (guessVal > target) {
      max = guess - 1; 
    } else {
      while (guess > 0 && lookupRange[guess - 1][0] === target) {
        guess -= 1; 
      }
      return guess;
    }
  }
  return -1;
}

现在我们可以线性遍历每一行并检查列是否匹配,直到第一列不再匹配。

function matchExists(row, lookupRange) {
  var index = firstRowMatchingCol1(row[0], lookupRange); 
  if (index === -1) {return false;}

  while (index < lookupRange.length && lookupRange[index][0] === row[0]) {
    for (var col = 1; col < row.length; col++) {
      if (row[col] !== lookupRange[index][col]) {break;}
      if (col === row.length - 1) {return true;} // This only works if the ranges are at least two columns wide but if they are one column wide you can just check if index > -1
    }
    index += 1;
  }
  return false;  
}

最后我们可以像这样得到副本:

function getNonDuplicates(r1, r2) {
  r2 = ArrayLib.sort(r2, 0, true);  
  return r1.filter(function(row) {return !matchExists(row, r2);});
}

就像 mTorres 的代码一样,这是未经测试的

【讨论】:

  • 您好,我会尽快尝试。为什么要先排序?这通过什么方式提高了速度?
  • 它允许我们做一个binary search。这是一个初始成本,因为您可以更快地搜索您正在寻找的元素,这在进行 30k 次时应该是非常值得的投资
  • 我明白了,但如果被排序的元素是数字,那不应该工作......在我的情况下,它们不是......我将编辑我的问题添加新信息
  • 它适用于任何可排序的对象,无论它们是字符串还是数字。
【解决方案2】:

我提出的解决方案是围绕时间限制“破解”。但是,如果您想要一个更简洁的解决方案,如果可能的话,您可以通过以某种方式对数组进行排序来重新组织并提高代码效率。

您没有在array1和array2中指定数据,如果行有某种ID字段,您可以按此ID排序并检查array1上的第i行和array2上的第i行,而不是将array1中的每一行与每一行进行比较在 array2 中(对于 30000 行,效率极低)。

如果您的数据没有 ID 字段来对行进行排序,那么您可以根据我提出的解决方案来做一些事情:为 array1 上的每个比较行添加一个轨道。当运行达到时间限制时,您再次运行该函数,但从最后一个比较行开始(您会知道这是因为您将跟踪比较的行),并且当第二次运行超时时您重复,依此类推.

每次运行比较时,您都会询问它是否是第一次运行(或使用布尔值 - 我更喜欢询问用户,这样您就不会忘记更改布尔值),如果是第一次运行,则删除跟踪 列,如果它不是第一次运行,您将从最后一个跟踪行的下一个开始,因此基本上继续您的脚本结束的地方。我一直在使用这种技术,效果很好。

在代码中(未经测试,因此在使用真实数据运行之前先检查一下):

/**
 * Only checks if it's the first run and calls the real work function
 */
function test() {
  var firstRun = "yes" === Browser.msgBox("Question", "Is this the first run?", Browser.Buttons.YES_NO);
  doTest(firstRun);
}

/**
 * Gets the data of the 2 spreadsheets and also the starting
 * row
 */
function doTest(firstRun) {
  var Folder = DriveApp.getFoldersByName('theFolder').next();
  var FolderId = Folder.getId();

  //call old_spreadsheet
  var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var old_file = files.next();   
  var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
  var old_sheet = old_spreadsheet.getSheets()[0];
  var old_sheetname = old_sheet.getName();
  var old_array = old_sheet.getDataRange().getValues();

  /**
   * Here is the code to create the tracking hability
   */
  var strartFromRow = 0; // 0 because row 1 is array 0 index when you getValues();
  var trackSheet = old_spreadsheet.getSheetByName("Tracking");
  if (trackSheet === null) {
    trackSheet = old_spreadsheet.insertSheet("Tracking");
  }

  if (firstRun) {
    trackSheet.getRange("A:A").clearContent();   // make sure there no row is tracked yet
  }
  else {
    // we have to continue from the previous row, keep in mind you're making the comparison 
    // with array which is 0 based, but sheet is 1 based, but you want the next one so getLasRow()
    // should be the first item to compare on your array
    strartFromRow = trackSheet.getLastRow(); 
  }

  Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);

  //call spreadsheet
  var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";  
  var files = DriveApp.searchFiles(searchFor); 
  var file = files.next();   
  var spreadsheet = SpreadsheetApp.openById(file.getId());
  var sheet = spreadsheet.getSheets()[0];
  var sheetname = sheet.getName();
  var array = sheet.getDataRange().getValues();
  Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length);  

  // when you call the DIFF function, pass the tracking sheet and the start Row
  var newarray = getNewData(array,old_array, trackSheet, startFromRow);

  Logger.log('there are ' + newarray.length + 'different rows');
}

/**
 * Creates a diff array using array1 and array2
 * It marks each element on array1 once it has checked if it's in array2
 */
function getNewData(array1, array2, trackingSheet, startFromRow){
  var logRow = trackingSheet.getLastRow();
  var diff = array2;   
  for (var i = startFromRow; i < array1.length; i++){
    var duplicate = false;
    for (var j = 0; j < diff.length;j++){
      if (array1[i].join() == diff[j].join()){
        Logger.log('duplicated line found on rows ' + i + ':' + j);
        diff.splice(j,1);
        duplicate = true;
        break;
      }    
    }
    if (duplicate === false) {
      Logger.log('not duplicated line found on row ' + i);
      diff.push(array1[i]);            
    }
    trackingSheet.getRange(logRow++, 1).setValue("Checked!");  // Mark i row as checked
  }
  return diff;
}

【讨论】:

  • 您好,我会尽快尝试。可以使用缓存服务进行跟踪吗?那样会更快吗?
  • 可能,我真的不知道。跟踪行的写入减慢了整个执行的速度,但它使它更可靠。我还没有尝试过缓存服务,但是当我再次需要它时,我可能会尝试一下。 PS:如果你使用缓存服务,请用你自己的经验评论:-)
  • 使用缓存服务查看我的答案,我正在测试它
【解决方案3】:

这是绕过时间限制的替代解决方案。创建一个新的专用电子表格以及一个自定义侧边栏。侧边栏将要求您创建一些 HTML,这些 HTML 最终将嵌入并呈现在客户端的 iframe 中。您可以通过脚本标签将纯 javascript 嵌入到 HTML 中。

这种方法的美妙之处在于,这些脚本不会在服务器端运行,而是在客户端上独立于 Google Apps 脚本的服务器端环境运行,并且不受 6 分钟的限制。此外,它们还可以调用您的 Google 脚本中的函数。因此,一种方法是让客户端脚本调用 Google Script 函数来检索必要的数据,在客户端脚本中进行所有繁重的处理,然后将结果发送回服务器端脚本以更新表。

以下是设置自定义侧边栏的链接以帮助您入门: https://developers.google.com/apps-script/guides/dialogs#custom_sidebars

【讨论】:

    【解决方案4】:

    最后,我决定选择缓存服务选项,这是代码,我正在对其进行测试,看看我是否继续这样做。

    function getNewData() {
      //deleting triggers
      var triggers = ScriptApp.getProjectTriggers();    
      for (var i = 0; i < triggers.length; i++) {
        if (triggers[i].getHandlerFunction()=='getNewData'){
          ScriptApp.deleteTrigger(triggers[i]);
        }
      }  
      //max running time = 5.5 min
      var MAX_RUNNING_TIME = 330000;
      var startTime= (new Date()).getTime();
      //get cache
      var cache = CacheService.getUserCache();
      var downloaded =JSON.parse(cache.get('downloaded'));  
      var compared =JSON.parse(cache.get('compared'));
      //start
      if (downloaded==1 && compared!=1){
        //folder
        var Folder = DriveApp.getFoldersByName('theFolder').next();
        var FolderId = licitacionesFolder.getId();
        //call old_spreadsheet
        var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";  
        var files = DriveApp.searchFiles(searchFor); 
        var old_file = files.next();  
        var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
        var old_sheet = old_spreadsheet.getSheets()[0];
        var old_array = old_sheet.getDataRange().getValues();
        //call spreadsheet
        var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";  
        var files = DriveApp.searchFiles(searchFor); 
        var file = files.next();  
        var spreadsheet = SpreadsheetApp.openById(old_file.getId());
        var sheet = spreadsheet.getSheets()[0];
        var array = sheet.getDataRange().getValues();
        Logger.log(array.length+'::'+old_array.length); 
        // Column
        var searchString = 'NAME';
        var RC = getColumn(array,searchString);    
        var Row = RC.Row;
        var Column = RC.Column;
        var RC = getColumn(old_array,searchString);    
        var Row_old = RC.Row;
        var Column_old = RC.Column;    
        Logger.log(Row_old+':::'+Column_old+'\n'+Row+':::'+Column);      
        //compare 
        var diff_index =JSON.parse(cache.get('diff_index'));
        var row_ind =JSON.parse(cache.get('row_ind'));  
        var Roww =JSON.parse(cache.get('Row'));  
        if (diff_index==null){var diff_index = [];}
        if (row_ind==null){var row_ind = 0;}
        if (Roww==null){var Roww = Row;}    
        Logger.log(row_ind+'\n'+Roww);    
        for (var i=Roww;i<array.length;i++){  
          var currTime = (new Date()).getTime();
          if(currTime - startTime >= MAX_RUNNING_TIME){
            Logger.log((currTime - startTime)/(1000*60));
            Logger.log(i+'::'+row_ind);
            cache.putAll({'diff_index': JSON.stringify(diff_index),'row_ind': JSON.stringify(row_ind),'Row': JSON.stringify(i-1)},21600);   
            ScriptApp.newTrigger('getNewData').timeBased().after(2 * 60 * 1000).create();
            return;
          } else {
            Logger.log(i);
            var existe = ArrayLib.indexOf(old_array, Column_old, array[i][Column]);
            if (existe==-1){      
              Logger.log(row_ind+'!!!');
              diff_index[row_ind]=i;
              row_ind++;          
            }
          }
        }    
        cache.putAll({'diff_index': JSON.stringify(diff_index),'Row': JSON.stringify(Row),'compared': JSON.stringify(1)},21600);
      } else {
        Logger.log('file not downloaded yet or already compared');
      }
    }
    
    function getColumn(array,searchString){
      for (var i = 0; i < array.length; i++) {    
        for (var j = 0; j < array[i].length; j++) {    
          if (array[i][j] == searchString) {
            var Row = i+1;
            var Column = j;
            break;
          }      
        }
        if (Row != undefined){
          break;
        }
      }
      return {Row: Row, Column: Column};
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-11
      • 2011-05-29
      • 2012-06-15
      • 1970-01-01
      相关资源
      最近更新 更多