58

我编写了一个脚本,每隔几个小时就会在 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 行时,它变得非常慢,甚至十秒。我担心当达到数千行时,它会太慢,可能会超时或更糟。有没有更好的办法?

4

16 回答 16

65

这个问题现在已经有超过12K 的浏览量- 所以是时候更新了,因为 New Sheets 的性能特征与Serge 运行他的初始测试时不同。

好消息:性能全面提升!

最快的:

与第一个测试一样,只读取工作表的数据一次,然后对阵列进行操作,可以带来巨大的性能优势。有趣的是,Don 的原始功能比 Serge 测试的修改版本的性能要好得多。(看起来这while比 快for,这是不合逻辑的。)

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

// 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 行 x 3 列的电子表格中汇总了 50 次迭代(用 Serge 的测试函数填充)。

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

截屏

“第一个空行”

最初的要求是找到第一个空行。以前的脚本都没有真正实现这一点。许多人只检查一列,这意味着他们可能会给出假阳性结果。其他人只找到所有数据之后的第一行,这意味着不连续数据中的空行会丢失。

这是一个符合规范的功能。它被包含在测试中,虽然比闪电般快速的单列检查器慢,但它以可观的 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;
}
于 2014-11-27T22:46:43.760 回答
55

Google Apps 脚本博客有一篇关于优化电子表格操作的帖子,其中谈到了批量读写可以真正加快速度。我在一个有 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 行为单位抓取数据,而不是抓取整列。

于 2012-02-01T20:04:44.000 回答
36

它已经作为工作表上的 getLastRow 方法存在。

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

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

于 2012-05-12T08:29:53.977 回答
8

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

我使用脚本

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

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

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

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

    =COUNTA(A3:A)
    

    whereA被替换为列字母。

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

有一次这不起作用,那就是当我允许空单元格分解列时。我还不需要对此进行修复,我怀疑一个可能来自COUNTIF,或组合函数或许多其他内置函数之一。

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

于 2014-08-09T17:02:03.863 回答
8

看到这篇有5k 浏览量的旧帖子,我首先检查了“最佳答案”,并对它的内容感到非常惊讶……这确实是一个非常缓慢的过程!然后当我看到 Don Kirkby 的回答时,我感觉好多了,数组方法确实效率更高!

但是效率高多少?

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

在此处输入图像描述 在此处输入图像描述

这是我使用的代码:

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']);
  }
}

测试电子表格自己尝试:-)


编辑 :

在 Mogsdad 的评论之后,我应该提到这些函数名称确实是一个糟糕的选择......它应该是这样的东西getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow(),它不是很优雅(是吗?)但更准确和与它实际返回的内容一致。

评论 :

无论如何,我的意思是展示这两种方法的执行速度,它显然做到了(不是吗?;-)

于 2013-08-19T16:51:30.800 回答
5

为什么不使用appendRow

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);
于 2013-04-03T18:07:40.493 回答
2

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

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;
于 2016-01-27T20:30:03.787 回答
2

只是我的两分钱,但我一直这样做。我只是将数据写入工作表的顶部。它的日期颠倒了(最新的在上面),但我仍然可以让它做我想做的事。下面的代码在过去三年中一直在存储从房地产经纪人网站上抓取的数据。

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

刮板函数的这一部分在#2 上方插入一行并将数据写入那里。第一行是标题,所以我不碰它。我没有计时,但我唯一遇到的问题是网站更改时。

于 2017-07-15T02:56:33.323 回答
1

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

 function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var array = spr.getDataRange().getValues();
  ct = array.length + 1
  return (ct);
}
于 2012-05-11T20:07:01.393 回答
1

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

这就是我正在做的事情。

  1. 向前走数百列,当我在一个空行时停下来。
  2. 向后退十个列,寻找第一个非空行。
  3. 逐列前进,寻找第一个空行。
  4. 返回结果。

这当然取决于具有连续的内容。那里不能有任何随机的空行。或者至少,如果你这样做,结果将是次优的。如果您认为这很重要,您可以调整增量。这些对我有用,我发现 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;
}

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

于 2013-08-19T14:44:01.970 回答
1

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

函数 firstEmptyRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sh = ss.getActiveSheet();
  var rangevalues = sh.getRange(1,1,sh.getLastRow(),1).getValues(); // A:A 列被占用
  var dat = rangevalues.reduce(function (a,b){ return a.concat(b)},[]); //
 二维数组缩减为一维//
  // Array.prototype.push.apply 可能更快,但无法让它工作//
  var fner = 1+dat.indexOf('');//获取indexOf第一个空行
  返回(fner);
  }
于 2018-02-09T18:07:33.567 回答
0

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

要获得范围的下一个空闲行,我只需检查相关单元格。我可以立即获得值,因为查找值的工作发生在数据更改时。

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

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

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

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

于 2012-09-28T11:54:38.343 回答
0

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

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

这对我来说可以。

于 2013-09-13T18:08:42.047 回答
0

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

我的“hack”是这个公式:

=ROW(index(sheet!A2:A,max(row(sheet!A2:A)*(sheet!A2:A<>""))))-1
  • 示例:将此添加到单元格 A1,以查找 A 列中的最后一行。可以添加到任何地方,只需确保根据公式所在的行管理末尾的“-1”。您也可以将其放置为另一个列,而不是您要计数的列,并且您不需要管理 -1。您还可以从起始行开始计数,例如“C16:C” - 将从 C16 开始计数值

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

  • 要在我的 GS 代码中使用这个值,我只是从 A1 读取单元格值。我知道谷歌很清楚像读/写这样的电子表格功能很繁重(耗时),但这比我的经验中的列计数最后一行方法快得多(对于大型数据集)

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

  • 如果数据已打开过滤器,这也适用。实际最后一行保持不变

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

于 2020-09-29T20:22:02.207 回答
0

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

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

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

some方法仅返回 true 或 false,但有一种方法可以捕获索引号,因为 some 方法在条件为 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"})

}
于 2020-05-29T01:41:49.550 回答
0

this is my very first post on stackOverflow, I hope to meet all your netiquette needs, so please be nice to me.

considerations

I think the fastest way to find the first blank cell in a column (I couldn't run the performance checks, anyway) is to let the Google engine do sequential tasks itself; it is simply much more efficient. From a programmer's point of view, this translates into NOT using any kind of iteration/loops, i.e. FOR, WHILE, etc. (By the way, this is the same programming approach on database engines - any activity should NOT use loops to find information.)

the idea

  1. Go all way DOWN and find the cell in last row of the Sheet (considering all columns),
  2. from there, go UP find the first cell containing data in the specified column (selecting the column),
  3. shift down one cell to find a free place.

The following function does this in just one command (neglecting the var declarations, here just to improve readability):

code

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
}
于 2021-06-16T21:23:19.937 回答