it-swarm.com.de

Schnellere Möglichkeit, die erste leere Zeile zu finden

Ich habe ein Skript erstellt, das alle paar Stunden einer Google Apps-Tabelle eine neue Zeile hinzufügt.

Mit dieser Funktion habe ich die erste leere Zeile gefunden:

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

Es funktioniert gut, aber wenn man ungefähr 100 Reihen erreicht, wird es wirklich langsam, sogar zehn Sekunden ... Ich bin besorgt, dass es beim Erreichen von Tausenden von Reihen zu langsam sein wird, vielleicht in eine Zeitüberschreitung oder noch schlimmer geht. Gibt es einen besseren Weg?

29
Omiod

Das Google Apps Script-Blog enthielt einen Beitrag zu zur Optimierung von Tabellenkalkulationen , in dem es um das Stapeln von Lese- und Schreibvorgängen ging, die die Dinge wirklich beschleunigen könnten. Ich habe Ihren Code in einer Tabelle mit 100 Zeilen ausprobiert, und der Vorgang dauerte etwa sieben Sekunden. Bei Verwendung von Range.getValues() dauert die Stapelversion eine Sekunde.

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

Wenn die Kalkulationstabelle groß genug wird, müssen Sie die Daten möglicherweise in Schritten von 100 oder 1000 Zeilen erfassen, anstatt die gesamte Spalte zu erfassen.

42
Don Kirkby

Diese Frage hatte jetzt mehr als 12K Views - also ist es Zeit für ein Update, da die Leistungsmerkmale von New Sheets anders sind als bei Serge führte seine ersten Tests durch .

Gute Nachrichten: Die Leistung ist auf der ganzen Linie viel besser!

Am schnellsten:

Wie im ersten Test hatte das einmalige Lesen der Daten des Blattes und das anschließende Arbeiten mit dem Array einen enormen Leistungsvorteil. Interessanterweise funktionierte Dons ursprüngliche Funktion viel besser als die von Serge getestete modifizierte Version. (Es scheint, dass while schneller ist als for, was nicht logisch ist.)

Die durchschnittliche Ausführungszeit für die Beispieldaten beträgt nur 38ms, weniger als die vorherigen 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);
}

Testergebnisse:

Hier sind die Ergebnisse, zusammengefasst über 50 Iterationen in einem Arbeitsblatt mit 100 Zeilen x 3 Spalten (gefüllt mit der Testfunktion von Serge).

Die Funktionsnamen stimmen mit dem Code im folgenden Skript überein. 

screenshot

"Erste leere Reihe"

Die ursprüngliche Anfrage bestand darin, die erste leere Zeile zu finden. Keines der vorherigen Skripte liefert das tatsächlich. Viele prüfen nur eine Spalte, was bedeutet, dass sie falsch positive Ergebnisse liefern können. Andere finden nur die erste Zeile, die auf alle Daten folgt, was bedeutet, dass leere Zeilen in nicht zusammenhängenden Daten übersehen werden.

Hier ist eine Funktion, die die Spezifikation erfüllt. Es wurde in die Tests einbezogen und war zwar langsamer als der blitzschnelle einspaltige Checker, erreichte jedoch beachtliche 68 ms, eine Prämie von 50% für eine korrekte Antwort!

/**
 * 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);
}

Komplettes Skript:

Wenn Sie die Tests wiederholen oder Ihre eigene Funktion als Vergleich zum Mix hinzufügen möchten, verwenden Sie einfach das gesamte Skript und verwenden Sie es in einer Kalkulationstabelle.

/**
 * 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

Es ist bereits als getLastRow-Methode in der Tabelle enthalten.

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

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

21
Peter Herrmann

Als ich diesen alten Beitrag mit 5k-Ansichten sah, habe ich zuerst den 'best answer' überprüft und war ziemlich überrascht von dessen Inhalt ... das war ein sehr langsamer Prozess! dann fühlte ich mich besser, als ich die Antwort von Don Kirkby sah.

Aber wie viel effizienter?

Also habe ich diesen kleinen Testcode in eine Tabelle mit 1000 Zeilen geschrieben und hier sind die Ergebnisse: (nicht schlecht!

enter image description hereenter image description here

und hier ist der Code, den ich verwendet habe:

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

Und die Testkalkulationstabelle um es selbst auszuprobieren :-)


BEARBEITEN:

Nach dem Kommentar von Mogsdad sollte ich erwähnen, dass diese Funktionsnamen tatsächlich eine schlechte Wahl sind ... Es sollte etwas wie getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow() sein, das nicht sehr elegant ist (oder?), Aber genauer und kohärenter mit dem, was es tatsächlich zurückgibt.

Kommentar : 

Jedenfalls ging es mir darum, die Geschwindigkeit der Ausführung beider Ansätze zu zeigen, und das hat es offensichtlich getan (oder? ;-)

8
Serge insas

Ich weiß, dass dies ein alter Thread ist und es hier einige sehr kluge Ansätze gab.

Ich benutze das Skript

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

wenn ich die erste komplett leere reihe brauche.

Wenn ich die erste leere Zelle in einer Spalte brauche, mache ich Folgendes.

  • Meine erste Reihe ist normalerweise eine Titelzeile.
  • Meine 2. Zeile ist eine versteckte Zeile und jede Zelle hat die Formel

    =COUNTA(A3:A)
    

    Wo A durch den Spaltenbuchstaben ersetzt wird.

  • Mein Skript liest diesen Wert einfach. Dies wird im Vergleich zu Skriptansätzen ziemlich schnell aktualisiert.

Einmal funktioniert das nicht, und ich erlaube leeren Zellen, die Spalte aufzuteilen. Ich habe bisher noch keine Korrektur benötigt, ich vermute, dass eine von COUNTIF, einer kombinierten Funktion oder einer der vielen anderen eingebauten Funktionen abgeleitet ist.

EDIT:COUNTA behandelt leere Zellen innerhalb eines Bereichs, daher ist die Besorgnis über das "einmalige Funktionieren nicht möglich" nicht wirklich ein Problem. (Dies könnte ein neues Verhalten bei "neuen Sheets" sein.)

4
Niccolo

Und warum nicht appendRow verwenden?

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

GetValues ​​ist zwar eine gute Option, aber Sie können die Funktion .length verwenden, um die letzte Zeile abzurufen.

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

Ich habe ein ähnliches Problem. Im Moment ist es eine Tabelle mit vielen Hunderten von Reihen, und ich erwarte, dass sie auf viele Tausende anwachsen wird. (Ich habe nicht gesehen, ob eine Google-Tabelle Zehntausende von Zeilen verarbeiten kann, aber ich komme irgendwann dorthin.)

Hier ist was ich mache.

  1. Schritt für Schritt vorwärts durch die Spalte, halte an, wenn ich in einer leeren Reihe bin.
  2. Gehen Sie zehnmal rückwärts durch die Spalte und suchen Sie nach der ersten nicht leeren Zeile.
  3. Gehe durch die Spalte vorwärts und suche nach der ersten leeren Zeile.
  4. Gib das Ergebnis zurück.

Dies hängt natürlich von zusammenhängenden Inhalten ab. Kann keine zufälligen leeren Zeilen enthalten. Zumindest sind die Ergebnisse jedoch nicht optimal. Sie können die Inkremente anpassen, wenn Sie es für wichtig halten. Diese arbeiten für mich und ich finde, dass der Unterschied in der Dauer zwischen Schritten von 50 und Schritten von 100 vernachlässigbar ist.

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;
}

Dies ist viel schneller als das Überprüfen von jeder Zelle von oben. Wenn Sie andere Spalten haben, die Ihr Arbeitsblatt erweitern, ist es möglicherweise schneller, als jede Zelle auch von unten zu untersuchen.

1
ghoti

Ich behalte in meinen Tabellenkalkulationen ein zusätzliches "Wartungsblatt", in dem ich solche Daten aufbewahre.

Um die nächste freie Reihe eines Bereichs zu erhalten, untersuche ich nur die relevante Zelle. Ich kann den Wert sofort abrufen, da die Suche nach dem Wert geschieht, wenn die Daten geändert werden.

Die Formel in der Zelle lautet normalerweise so:

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

Der Wert in A9 kann periodisch auf eine Zeile gesetzt werden, die bis zum Ende "genug" ist.

Vorbehalt : Ich habe noch nie geprüft, ob dies für große Datensätze geeignet ist. 

0
Martin Bramwell

Mit indexOf können Sie dies erreichen:

 function firstEmptyRow () {
 var ss = SpreadsheetApp.getActiveSpreadsheet (); 
 var sh = ss.getActiveSheet (); 
 var rangevalues ​​= sh.getRange (1,1, sh.getLastRow (), 1) .getValues ​​(); // Spalte A: A wird genommen 
 var dat = rangevalues.reduce (Funktion (a, b) {return a.concat (b)}, []); // 
 2D-Array wird auf 1D //.__ reduziert. // Array.prototype.Push.apply ist möglicherweise schneller, aber nicht in der Lage, es zum Laufen zu bringen. var fner = 1 + dat.indexOf (''); // get indexOf Erste leere Zeile 
 return (fner); 
 } 
0
TheMaster

Endlich bekam ich eine einzeilige Lösung dafür.

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

Es funktioniert gut für mich.

0
Hari Das

Nur meine zwei Cents, aber ich mache das die ganze Zeit. Ich schreibe die Daten einfach auf die Oberseite des Blattes. Das Datum ist vertauscht (das neueste oben), aber ich kann immer noch das bekommen, was ich will. Der folgende Code speichert seit drei Jahren Daten, die er von einem Grundstücksmaklerstandort abruft.

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

Dieser Block der Scraper-Funktion fügt eine Zeile über # 2 ein und schreibt die Daten dort. Die erste Zeile ist die Kopfzeile, daher greife ich das nicht an. Ich habe es nicht zeitlich festgelegt, aber ich habe nur ein Problem, wenn sich die Site ändert.

0
HardScale

Ich habe den von ghoti gelieferten Code so angepasst, dass nach einer leeren Zelle gesucht wurde. Der Vergleich von Werten funktionierte nicht für eine Spalte mit Text (oder ich konnte nicht herausfinden, wie), stattdessen verwendete ich isBlank (). Beachten Sie, dass der Wert mit negiert wird! (vor der Variable r) wenn Sie nach vorne schauen, da i so lange erhöht werden soll, bis ein Leerzeichen gefunden wird. Um das Blatt um zehn aufzubereiten, möchten Sie aufhören, i zu verringern, wenn Sie eine Zelle finden, die nicht leer ist (! Entfernt). Kehren Sie dann das Blatt um eins bis zum ersten Rohling zurück.

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