更快找到第一个空行的方法

时间:2011-07-30 08:53:23

标签: performance google-apps-script google-sheets

我制作了一个脚本,每隔几个小时就会在Google Apps电子表格中添加一个新行。

这是我找到第一个空行的功能:

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct);
}

它工作正常,但是当达到大约100行时,它变得非常慢,甚至十秒。 我担心当达到数千行时,它会太慢,可能会超时或更糟。 还有更好的方法吗?

16 个答案:

答案 0 :(得分:44)

Google Apps脚本博客在optimizing spreadsheet operations上发布了一篇帖子,其中讨论了批量读取和写入,这些内容可以真正加快速度。我在100行的电子表格上尝试了你的代码,大约花了7秒钟。通过使用Range.getValues(),批处理版本需要一秒钟。

function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct][0] != "" ) {
    ct++;
  }
  return (ct);
}

如果电子表格足够大,您可能需要抓取100或1000行的数据而不是抓取整个列。

答案 1 :(得分:34)

此问题现在已超过 12K次观看 - 因此需要更新,因为新表格的性能特征与Serge ran his initial tests时的性能特征不同。

好消息:整体表现要好得多!

最快:

与第一次测试一样,只需读取一次工作表的数据,然后在阵列上操作,就可以获得巨大的性能优势。有趣的是,Don的原始功能比Serge测试的修改版本表现得更好。 (似乎whilefor更快,这是不合逻辑的。)

样本数据的平均执行时间仅为 38ms ,低于之前的 168ms

// Don's array approach - checks first column only
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

测试结果:

以下是结果,在100行×3列的电子表格中总结了50多次迭代(填充了Serge的测试功能)。

函数名称与下面脚本中的代码匹配。

screenshot

"第一个空行"

最初的问题是找到第一个空行。之前的任何脚本都没有真正实现。许多人只检查一列,这意味着他们可以给出误报结果。其他人只找到所有数据后面的第一行,这意味着错过了非连续数据中的空行。

这是一个符合规范的功能。它被包含在测试中,虽然比闪电般快速的单柱检查器慢,但它的出现时间相差68毫秒,正确答案的溢价为50%!

/**
 * Mogsdad's "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

完整的脚本:

如果您想重复测试,或者将自己的功能添加到混音中作为比较,只需获取整个脚本并在电子表格中使用它。

/**
 * Set up a menu option for ease of use.
 */
function onOpen() {
  var menuEntries = [ {name: "Fill sheet", functionName: "fillSheet"},
                      {name: "test getFirstEmptyRow", functionName: "testTime"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

/**
 * Test an array of functions, timing execution of each over multiple iterations.
 * Produce stats from the collected data, and present in a "Results" sheet.
 */
function testTime() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.getSheets()[0].activate();
  var iterations = parseInt(Browser.inputBox("Enter # of iterations, min 2:")) || 2;

  var functions = ["getFirstEmptyRowByOffset", "getFirstEmptyRowByColumnArray", "getFirstEmptyRowByCell","getFirstEmptyRowUsingArray", "getFirstEmptyRowWholeRow"]

  var results = [["Iteration"].concat(functions)];
  for (var i=1; i<=iterations; i++) {
    var row = [i];
    for (var fn=0; fn<functions.length; fn++) {
      var starttime = new Date().getTime();
      eval(functions[fn]+"()");
      var endtime = new Date().getTime();
      row.push(endtime-starttime);
    }
    results.push(row);
  }

  Browser.msgBox('Test complete - see Results sheet');
  var resultSheet = SpreadsheetApp.getActive().getSheetByName("Results");
  if (!resultSheet) {
    resultSheet = SpreadsheetApp.getActive().insertSheet("Results");
  }
  else {
    resultSheet.activate();
    resultSheet.clearContents();
  }
  resultSheet.getRange(1, 1, results.length, results[0].length).setValues(results);

  // Add statistical calculations
  var row = results.length+1;
  var rangeA1 = "B2:B"+results.length;
  resultSheet.getRange(row, 1, 3, 1).setValues([["Avg"],["Stddev"],["Trimmed\nMean"]]);
  var formulas = resultSheet.getRange(row, 2, 3, 1);
  formulas.setFormulas(
    [[ "=AVERAGE("+rangeA1+")" ],
     [ "=STDEV("+rangeA1+")" ],
     [ "=AVERAGEIFS("+rangeA1+","+rangeA1+',"<"&B$'+row+"+3*B$"+(row+1)+","+rangeA1+',">"&B$'+row+"-3*B$"+(row+1)+")" ]]);
  formulas.setNumberFormat("##########.");

  for (var col=3; col<=results[0].length;col++) {
    formulas.copyTo(resultSheet.getRange(row, col))
  }

  // Format for readability
  for (var col=1;col<=results[0].length;col++) {
    resultSheet.autoResizeColumn(col)
  }
}

// Omiod's original function.  Checks first column only
// Modified to give correct result.
// question https://stackoverflow.com/questions/6882104
function getFirstEmptyRowByOffset() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var cell = spr.getRange('a1');
  var ct = 0;
  while ( cell.offset(ct, 0).getValue() != "" ) {
    ct++;
  }
  return (ct+1);
}

// Don's array approach - checks first column only.
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var column = spr.getRange('A:A');
  var values = column.getValues(); // get all data in one call
  var ct = 0;
  while ( values[ct] && values[ct][0] != "" ) {
    ct++;
  }
  return (ct+1);
}

// Serge's getFirstEmptyRow, adapted from Omiod's, but
// using getCell instead of offset. Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowByCell() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  var arr = []; 
  for (var i=1; i<=ran.getLastRow(); i++){
    if(!ran.getCell(i,1).getValue()){
      break;
    }
  }
  return i;
}

// Serges's adaptation of Don's array answer.  Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowUsingArray() {
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n=0; n<data.length ;  n++){
    if(data[n][0]==''){n++;break}
  }
  return n+1;
}

/**
 * Mogsdad's "whole row" checker.
 */
function getFirstEmptyRowWholeRow() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var values = range.getValues();
  var row = 0;
  for (var row=0; row<values.length; row++) {
    if (!values[row].join("")) break;
  }
  return (row+1);
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

// Function to test the value returned by each contender.
// Use fillSheet() first, then blank out random rows and
// compare results in debugger.
function compareResults() {
  var a = getFirstEmptyRowByOffset(),
      b = getFirstEmptyRowByColumnArray(),
      c = getFirstEmptyRowByCell(),
      d = getFirstEmptyRowUsingArray(),
      e = getFirstEmptyRowWholeRow(),
      f = getFirstEmptyRowWholeRow2();
  debugger;
}

答案 2 :(得分:21)

它已作为Sheet上的getLastRow方法存在。

var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;

参考https://developers.google.com/apps-script/class_sheet#getLastRow

答案 3 :(得分:8)

使用 5k观看看到这篇旧帖子我首先检查了'最佳答案'并对其内容感到非常惊讶......这确实是一个非常缓慢的过程!当我看到Don Kirkby的答案时,我感觉更好,阵列方法确实更有效率!

但效率更高?

所以我在1000行的电子表格上写了这个小测试代码,结果如下:(不错!......不需要告诉哪一个是哪个......)

enter image description here enter image description here

这是我使用的代码:

function onOpen() {
  var menuEntries = [ {name: "test method 1", functionName: "getFirstEmptyRow"},
                      {name: "test method 2 (array)", functionName: "getFirstEmptyRowUsingArray"}
                     ];
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  sh.addMenu("run tests",menuEntries);
}

function getFirstEmptyRow() {
  var time = new Date().getTime();
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var ran = spr.getRange('A:A');
  for (var i= ran.getLastRow(); i>0; i--){
    if(ran.getCell(i,1).getValue()){
      break;
    }
  }
  Browser.msgBox('lastRow = '+Number(i+1)+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function getFirstEmptyRowUsingArray() {
  var time = new Date().getTime();
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  var data = ss.getDataRange().getValues();
  for(var n =data.length ; n<0 ;  n--){
    if(data[n][0]!=''){n++;break}
  }
  Browser.msgBox('lastRow = '+n+'  duration = '+Number(new Date().getTime()-time)+' mS');
}

function fillSheet(){
  var sh = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sh.getActiveSheet();
  for(var r=1;r<1000;++r){
    ss.appendRow(['filling values',r,'not important']);
  }
}

the test spreadsheet亲自尝试: - )


编辑:

根据Mogsdad的评论,我应该提到这些函数名称确实是一个糟糕的选择......它应该是getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow()这样的东西不是很优雅(是吗?)但更准确和连贯的是什么它实际上会回来。

评论:

无论如何,我的观点是要显示两种方法的执行速度,它显然是这样做的(不是吗?; - )

答案 4 :(得分:4)

我知道这是一个老线程,这里有一些非常聪明的方法。

我使用脚本

var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;

如果我需要第一个完全空行。

如果我需要列中的第一个空单元格,我会执行以下操作。

  • 我的第一行通常是标题行。
  • 我的第二行是一个隐藏的行,每个单元格都有公式

    =COUNTA(A3:A)
    

    A替换为列字母。

  • 我的脚本只读取此值。与脚本方法相比,这种更新速度非常快。

有一次这不起作用,那就是我允许空单元格拆分列。我还没有需要修复此问题,我怀疑可能是COUNTIF,或者是组合函数或其他许多内置函数之一。

编辑: COUNTA确实可以处理范围内的空白单元格,因此对“一次不起作用”的担忧并不是真正令人担忧的问题。 (这可能是“新表格”的新行为。)

答案 5 :(得分:3)

为什么不使用appendRow

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);

答案 6 :(得分:2)

实际上getValues是一个不错的选择,但你可以使用.length函数来获取最后一行。

 function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var array = spr.getDataRange().getValues();
  ct = array.length + 1
  return (ct);
}

答案 7 :(得分:1)

我有类似的问题。现在它是一个包含数百行的表,我期待它可以增长到数千行。 (我还没有看到Google电子表格是否会处理数万行,但最终我会到达那里。)

这就是我正在做的事情。

  1. 向前走过数百个列,当我在空行时停止。
  2. 向后退一列十位,寻找第一个非空行。
  3. 在列中向前一步,寻找第一个空行。
  4. 返回结果。
  5. 这当然取决于是否有连续的内容。那里不能有任何随机的空白行。或者至少,如果你这样做,结果将是次优的。如果你认为它很重要,你可以调整增量。这些对我有用,我发现50步和100步之间的持续时间差异可以忽略不计。

    function lastValueRow() {
      var ss = SpreadsheetApp.getActiveSpreadsheet();
      var r = ss.getRange('A1:A');
      // Step forwards by hundreds
      for (var i = 0; r.getCell(i,1).getValue() > 1; i += 100) { }
      // Step backwards by tens
      for ( ; r.getCell(i,1).getValue() > 1; i -= 10) { }
      // Step forwards by ones
      for ( ; r.getCell(i,1).getValue() == 0; i--) { }
      return i;
    }
    

    这比从顶部检查每个单元要快得多。如果您碰巧有其他一些列可以扩展您的工作表,那么它也可能比从底部检查每个单元格更快。

答案 8 :(得分:0)

我在电子表格上保留了一份额外的“维护”表,我保存了这些数据。

要获取范围的下一个自由行,我只需检查相关的单元格。我可以立即得到这个值,因为在数据发生变化时会发现找到值的工作。

单元格中的公式通常类似于:

=QUERY(someSheet!A10:H5010, 
    "select min(A) where A > " & A9 & " and B is null and D is null and H < 1")

A9中的值可以定期设置到接近“足够”的某一行。

警告:我从未检查过这是否适用于大型数据集。

答案 9 :(得分:0)

最后我得到了一个单线解决方案。

var sheet = SpreadsheetApp.getActiveSpreadsheet();
var lastEmptyOnColumnB = sheet.getRange("B1:B"+sheet.getLastRow()).getValues().join(",").replace(/,,/g, '').split(",").length;

它适用于我。

答案 10 :(得分:0)

我调整了提供的代码ghoti,以便搜索空单元格。比较值不适用于带有文本的列(或者我无法弄清楚如何),而是使用isBlank()。请注意,该值被否定! (在变量r的前面)向前看时,因为你希望我增加直到找到空白。当你找到一个非空白的单元格(!删除)时,你想要停止减少我的工作量。然后,将纸张向下放回第一个空白处。

function findRow_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.setActiveSheet(ss.getSheetByName("DAT Tracking"));
  var r = ss.getRange('C:C');
  // Step forwards by hundreds
  for (var i = 2; !r.getCell(i,1).isBlank(); i += 100) { }
  // Step backwards by tens
  for ( ; r.getCell(i,1).isBlank(); i -= 10) { }
  // Step forwards by ones
  for ( ; !r.getCell(i,1).isBlank(); i++) { }
  return i;

答案 11 :(得分:0)

只是我的两分钱,但我一直这样做。我只是将数据写入表格的TOP。它的日期已经逆转(最新的),但我仍然能够做到我想要的。以下代码在过去三年中一直存储从房地产经纪人网站上搜集过的数据。

var theSheet = SpreadsheetApp.openById(zSheetId).getSheetByName('Sheet1');
theSheet.insertRowBefore(1).getRange("A2:L2").setValues( [ zPriceData ] );

刮刀功能的这一部分在#2上方插入一行并在那里写入数据。第一行是标题,所以我不接触它。我还没有及时计划,但我唯一遇到的问题是网站何时发生变化。

答案 12 :(得分:0)

使用indexOf是实现此目的的方法之一:

function firstEmptyRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getActiveSheet();
  var rangevalues = sh.getRange(1,1,sh.getLastRow(),1).getValues(); // Column A:A is taken
  var dat = rangevalues.reduce(function (a,b){ return a.concat(b)},[]); // 
 2D array is reduced to 1D//
  // Array.prototype.push.apply might be faster, but unable to get it to work//
  var fner = 1+dat.indexOf('');//Get indexOf First empty row
  return(fner);
  }

答案 13 :(得分:0)

以下是代码应执行的操作的列表:

  • 如果没有空单元格,请给出正确答案
  • 要快
  • 返回正确的行号-而不是数组的索引号
  • 即使工作表标签中的其他列有更多行包含数据,也要获取正确的空单元格行数
  • 具有好的变量名
  • 回答原始问题
  • 避免不必要的数据处理
  • 提供有关代码功能的注释说明
  • 足够通用,以适应读者的情况

此解决方案使用数组方法some,该数组方法在条件为true时将停止迭代循环。这样可以避免浪费时间遍历数组的每个元素,而是使用数组方法而不是forwhile循环。

some方法仅返回true或false,但是有一种捕获索引号的方法,因为当条件为true时,某些方法会停止循环。

将索引号分配给数组函数外部范围内的变量。这不会减慢处理速度。

代码:

function getFirstEmptyCellIn_A_Column(po) {
  var foundEmptyCell,rng,sh,ss,values,x;

  /*
    po.sheetTabName - The name of the sheet tab to get
    po.ssID - the file ID of the spreadsheet
    po.getActive - boolean - true - get the active spreadsheet - 
  */

  /*  Ive tested the code for speed using many different ways to do this and using array.some
    is the fastest way - when array.some finds the first true statement it stops iterating -
  */

  if (po.getActive || ! po.ssID) {
    ss =  SpreadsheetApp.getActiveSpreadsheet();
  } else {
    ss = SpreadsheetApp.openById(po.ssID);
  }

  sh = ss.getSheetByName(po.sheetTabName);
  rng = sh.getRange('A:A');//This is the fastest - Its faster than getting the last row and getting a
  //specific range that goes only to the last row

  values = rng.getValues(); // get all the data in the column - This is a 2D array

  x = 0;//Set counter to zero - this is outside of the scope of the array function but still accessible to it

  foundEmptyCell = values.some(function(e,i){
    //Logger.log(i)
    //Logger.log(e[0])
    //Logger.log(e[0] == "")

    x = i;//Set the value every time - its faster than first testing for a reason to set the value
    return e[0] == "";//The first time that this is true it stops looping
  });

  //Logger.log('x + 1: ' + (x + 1))//x is the index of the value in the array - which is one less than the row number
  //Logger.log('foundEmptyCell: ' + foundEmptyCell)

  return foundEmptyCell ? x + 1 : false;
}

function testMycode() {

  getFirstEmptyCellIn_A_Column({"sheetTabName":"Put Sheet tab name here","ssID":"Put your ss file ID here"})

}

答案 14 :(得分:0)

对于特定的列,我已经经历了太多的最后一行的这些实现。许多解决方案都可以使用,但是对于大型或多个数据集来说速度很慢。我的一个用例要求我检查多个电子表格中特定列的最后一行。我发现,将整个列作为一个范围,然后对其进行迭代太慢了,将其中一些加在一起会使脚本变慢。

我的“ hack”就是这个公式:

<circle>
  • 示例:将其添加到单元格A1中,以查找列A中的最后一行。可以添加到任何位置,只需确保根据公式放在哪一行的末尾来管理“ -1”即可。您还可以将其放置在另一个列中,而不是要尝试计数的列中,并且不需要管理-1。您也可以从起始行开始计数,例如“ C16:C”-将从C16开始计数值

  • 该公式可靠地给了我最后一行,包括数据集中间的空白

  • 要在我的GS代码中使用此值,我只是从A1中读取单元格值。我了解Google很清楚,电子表格功能(例如读/写)很繁琐(耗时),但是根据我的经验(对于大型数据集),这比列数最后一行方法要快得多

  • 为提高效率,我在col中获取了最后一行,然后将其保存为全局变量,并在代码中递增以跟踪应更新的行。每次循环需要进行更新时都读取单元格会导致效率低下。读取一次,迭代该值,A1单元格公式(如上)将“存储”更新的值,以供下次函数运行时使用

  • 如果数据打开了过滤器,这也将起作用。实际的最后一行保持不变

请告诉我这是否对您有帮助!如果遇到任何问题,我会对此答案发表评论。

答案 15 :(得分:0)

这是我在stackOverflow上的第一篇文章,我希望能满足你所​​有的网络礼仪需求,所以请善待我。

注意事项

我认为在列中找到第一个空白单元格的最快方法(无论如何我无法运行性能检查)是让 Google 引擎自己执行顺序任务;它只是更有效率。从程序员的角度来看,这意味着不使用任何类型的迭代/循环,即 FOR、WHILE 等(顺便说一句,这与数据库引擎上的编程方法相同 - 任何活动不应使用循环查找信息。)

想法

  1. 一路DOWN,找到工作表最后一行的单元格(考虑所有列),
  2. 从那里,UP 找到指定列中包含数据的第一个单元格(选择该列),
  3. 向下移动一个单元格以找到一个空闲位置。

以下函数仅在一个命令中完成此操作(忽略 var 声明,这里只是为了提高可读性):

代码

function lastCell() {    
  var workSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var lastRow = workSheet.getLastRow();
  var columnToSearch = 1; //index of the column to search. 1 is 'A'.

  workSheet.getRange(lastRow, columnToSearch).activateAsCurrentCell().
    getNextDataCell(SpreadsheetApp.Direction.UP).activate();
  workSheet.getCurrentCell().offset(1, 0).activate(); // shift one cell down to find a free cell
}