it-swarm.com.ru

Более быстрый способ найти первую пустую строку

Я создал скрипт, который каждые несколько часов добавляет новую строку в электронную таблицу Служб 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 строк он становится очень медленным, даже десять секунд .... Я беспокоюсь, что при достижении тысяч строк он будет слишком медленным, возможно, из-за тайм-аута или хуже .... Есть ли способ лучше?

29
Omiod

В блоге 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 строк вместо того, чтобы захватывать весь столбец.

42
Don Kirkby

Этот вопрос теперь имеет более 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 строками по 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;
}
34
Mogsdad

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

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

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

21
Peter Herrmann

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

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

Итак, я написал этот небольшой тестовый код в электронной таблице с 1000 строками, и вот результаты: (неплохо! ... не нужно указывать, какой из них какой ...)

enter image description hereenter 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']);
  }
}

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


Правка :

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

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

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

8
Serge insas

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

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

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

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

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

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

    =COUNTA(A3:A)
    

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

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

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

EDIT:COUNTA справляется с пустыми ячейками в диапазоне, поэтому проблема «один раз это не работает» на самом деле не является проблемой. (Это может быть новое поведение с «новыми листами».)

4
Niccolo

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

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

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

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

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

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

1
ghoti

На своих электронных таблицах я храню дополнительный лист «технического обслуживания», где храню такие данные.

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

Формула в ячейке обычно выглядит примерно так:

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

Значение в A9 может периодически устанавливаться для некоторой строки, которая близка к «достаточной» до конца.

Предостережение : Я никогда не проверял, подходит ли это для больших массивов данных. 

0
Martin Bramwell

Использование indexOf - один из способов добиться этого:

 function 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)}, []); // 
 2D массив уменьшен до 1D //
 // Array.prototype.Push.apply может быть быстрее, но не может заставить его работать //
 var fner = 1 + dat.indexOf (''); // Получить indexOf Первая пустая строка 
 Возвращение (fner); 
 } 
0
TheMaster

Наконец-то я получил единственное решение для этого.

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

Он отлично работает для меня.

0
Hari Das

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

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

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

0
HardScale

Я подправил код, предоставленный ghoti, чтобы он искал пустую ячейку. Сравнение значений не работает для столбца с текстом (или я не могу понять, как), вместо этого я использовал isBlank (). Обратите внимание, что значение отрицается с! (перед переменной r) при просмотре вперед, так как вы хотите увеличить i, пока не найдете пробел. Работая на листе на десять, вы хотите прекратить уменьшать значение 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;
0
Richard Rasch