Более быстрый способ найти первую пустую строку - PullRequest
31 голосов
/ 30 июля 2011

Я создал скрипт, который каждые несколько часов добавляет новую строку в электронную таблицу Служб Google.

Это функция, которую я сделал, чтобы найти первую пустую строку:

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

Он отлично работает, но при достижении примерно 100 строк он становится очень медленным, даже десять секунд. Я беспокоюсь, что при достижении тысяч строк это будет слишком медленно, возможно, из-за превышения времени ожидания или еще хуже. Есть ли лучший способ?

Ответы [ 13 ]

45 голосов
/ 02 февраля 2012

В блоге Google Apps Script было сообщение о , оптимизирующем операции с электронными таблицами , в котором говорилось о пакетном чтении и записи, которые действительно могли бы ускорить процесс. Я попробовал ваш код в электронной таблице из 100 строк, и это заняло около семи секунд. При использовании 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 строк вместо того, чтобы захватывать весь столбец.

34 голосов
/ 28 ноября 2014

Этот вопрос теперь имеет более 12K просмотров - поэтому пришло время для обновления, поскольку характеристики производительности Новых листов отличаются от тех, когда Серж провел свои начальные тесты .

Хорошие новости: производительность намного выше по всем направлениям!

Самый быстрый:

Как и в первом тесте, чтение данных листа только один раз, затемработа с массивом дала огромный выигрыш в производительности.Интересно, что оригинальная функция Дона работала намного лучше, чем модифицированная версия, которую тестировал Серж.(Похоже, что 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);
}

Результаты теста:

Вот результаты, обобщенные за 50 итераций в электронной таблице с 100 строками x 3 столбцами (заполненными тестовой функцией Сержа).

Имена функций соответствуют коду в приведенном ниже скрипте.

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;
}
21 голосов
/ 12 мая 2012

Он уже существует как метод getLastRow на Листе.

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

Ссылка https://developers.google.com/apps-script/class_sheet#getLastRow

8 голосов
/ 19 августа 2013

Видя этот старый пост с 5k просмотров Я впервые проверил 'лучший ответ' и был довольно удивлен его содержанием ... это действительно был очень медленный процесс! затем я почувствовал себя лучше, когда увидел ответ Дона Киркби, а массивный подход действительно намного эффективнее!

Но насколько эффективнее?

Итак, я написал этот маленький тестовый код в электронной таблице с 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']);
  }
}

И тестовая таблица , чтобы попробовать сами: -)


РЕДАКТИРОВАТЬ:

После комментария Могсдада я должен упомянуть, что эти имена функций действительно плохой выбор ... Это должно было быть что-то вроде getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow(), которое не очень элегантно (не так ли?), Но более точно и согласованно с тем, что на самом деле возвращается.

Комментарий:

Во всяком случае, моя цель была показать скорость выполнения обоих подходов, и он, очевидно, сделал это (не так ли?; -)

4 голосов
/ 09 августа 2014

Я знаю, что это старая ветка, и здесь были некоторые очень умные подходы.

Я использую скрипт

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

если мне нужен первый полностью пустой ряд.

Если мне нужна первая пустая ячейка в столбце, я делаю следующее.

  • Моя первая строка обычно является строкой заголовка.
  • Моя вторая строка является скрытой строкой, и каждая ячейка имеет формулу

    =COUNTA(A3:A)
    

    Где A заменяется буквой столбца.

  • Мой скрипт просто читает это значение. Это обновляется довольно быстро по сравнению со сценариями.

Есть один раз, когда это не работает, и это когда я позволяю пустым ячейкам разбивать столбец. Мне пока не нужно исправление, я подозреваю, что оно может быть получено из COUNTIF, или из комбинированной функции, или из множества других встроенных функций.

РЕДАКТИРОВАТЬ: COUNTA справляется с пустыми ячейками в пределах диапазона, поэтому проблема «один раз это не работает» на самом деле не является проблемой. (Это может быть новое поведение с «новыми листами».)

3 голосов
/ 03 апреля 2013

А почему бы не использовать appendRow ?

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);
2 голосов
/ 12 мая 2012

Действительно, getValues ​​- хороший вариант, но вы можете использовать функцию .length для получения последней строки.

 function getFirstEmptyRow() {
  var spr = SpreadsheetApp.getActiveSpreadsheet();
  var array = spr.getDataRange().getValues();
  ct = array.length + 1
  return (ct);
}
1 голос
/ 19 августа 2013

У меня похожая проблема. Сейчас это таблица с сотнями строк, и я ожидаю, что она вырастет до многих тысяч. (Я не видел, будет ли электронная таблица 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;
}

Это намного быстрее, чем проверка каждой ячейки сверху. И если у вас есть какие-то другие столбцы, расширяющие ваш рабочий лист, это может быть быстрее, чем проверка каждой ячейки снизу.

0 голосов
/ 09 февраля 2018

Использование 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);
  }
0 голосов
/ 15 июля 2017

Только мои два цента, но я делаю это все время.Я просто записываю данные в ТОП листа.Дата поменялась (последняя - сверху), но я все еще могу заставить ее делать то, что я хочуКод ниже хранит данные, которые он удаляет с сайта риэлтора в течение последних трех лет.

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

Этот фрагмент функции скребка вставляет строку выше # 2 и записывает туда данные.Первая строка - заголовок, поэтому я не касаюсь этого.Я не рассчитал время, но у меня возникает проблема только когда сайт меняется.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...