web-dev-qa-db-ja.com

最初の空の行を見つけるより速い方法

数時間ごとに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行に達すると、10秒でも非常に遅くなります。数千の行に到達すると、非常に遅くなり、タイムアウトになるか、さらに悪化するのではないかと心配しています。もっと良い方法はありますか?

31
Omiod

Google Apps Scriptブログには スプレッドシート操作の最適化 に関する投稿があり、実際に速度を上げることができる読み取りと書き込みのバッチ処理について説明しました。 100行のスプレッドシートでコードを試しましたが、約7秒かかりました。 Range.getValues() を使用すると、バッチバージョンに1秒かかります。

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行のチャンクでデータを取得する必要がある場合があります。

45
Don Kirkby

この質問には12Kビュー以上ありました-新しいシートのパフォーマンス特性は セルジュは最初のテストを実行しました

朗報:パフォーマンスは全体的にはるかに優れています!

最速:

最初のテストと同様に、シートのデータを一度だけ読み取ってからアレイを操作すると、パフォーマンスが大幅に向上しました。興味深いことに、ドンの元の機能は、Sergeがテストした修正版よりもはるかに優れていました。 (whileは、論理的ではないforよりも速いようです。)

サンプルデータの平均実行時間は、ちょうど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行x 3列(Sergeのテスト関数で満たされた)のスプレッドシートで50回以上の繰り返しを要約したものです。

関数名は、以下のスクリプトのコードと一致します。

screenshot

「最初の空行」

最初の質問は、最初の空の行を見つけることでした。以前のスクリプトはどれも実際にそれを実現していません。多くの場合、1つの列のみをチェックします。つまり、偽陽性の結果が出る可能性があります。その他は、すべてのデータに続く最初の行のみを検出します。つまり、連続していないデータの空の行が失われます。

以下は、仕様を満たす関数です。これはテストに含まれており、超高速の単一列チェッカーよりも低速でしたが、かなりの68msで入っており、正解には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;

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

21
Peter Herrmann

この古い投稿を5kビューで見る最初に 'best answer'をチェックし、その内容...これは確かに非常に遅いプロセスでした!ドン・カークビーの答えを見たとき、私は気分が良くなりました。アレイのアプローチは確かにはるかに効率的です!

しかし、どれほど効率的ですか?

だから私はこの小さなテストコードを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']);
  }
}
_

テストスプレッドシート 自分で試してみてください:-)


編集:

モグスダッドのコメントに続いて、これらの関数名は本当に悪い選択であることに言及する必要があります...それは、getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow()のようなものである必要があります。実際に戻ります。

コメント:

とにかく、私のポイントは両方のアプローチの実行速度を示すことでした、そして明らかにそれをしました(そうではなかったのですか?;-)

8
Serge insas

これは古いスレッドであり、非常に巧妙なアプローチがいくつかあることは知っています。

スクリプトを使用します

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

最初の完全に空の行が必要な場合。

列の最初の空のセルが必要な場合は、次を実行します。

  • 通常、最初の行はタイトル行です。
  • 2番目の行は非表示の行で、各セルには式があります

    =COUNTA(A3:A)
    

    Aは列の文字に置き換えられます。

  • 私のスクリプトはこの値を読み取るだけです。これは、スクリプトアプローチと比較して非常に迅速に更新されます。

これが機能しない場合があります。空のセルで列を分割できるようにする場合です。私はまだこれを修正する必要はありません。COUNTIF、または結合された関数、または他の多くの組み込み関数のいずれかから派生したものと思われます。

EDIT:COUNTA は範囲内の空白セルに対処するため、「1回限りこれは機能しません」は実際には問題ではありません。 (これは、「新しいシート」の新しい動作です。)

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. 最初の空でない行を探して、列を10ずつ後退します。
  3. 列を1つずつ進めて、最初の空の行を探します。
  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;
}

これは、上からevery cellを調べるよりもはるかに高速です。また、ワークシートを拡張する他の列がある場合は、下からすべてのセルを検査するよりも高速です。

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を使用することは、これを達成する方法の1つです。

 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( ''); // Get IndexOf最初の空行
 return(fner); 
} 
0
TheMaster

最終的に私はそれのための単一行ソリューションを得ました。

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

それは私のためにうまく機能します。

0
Hari Das

ちょうど2セントですが、私はいつもこれをしています。データをシートのTOPに書き込むだけです。日付が逆になっています(一番上にあります)が、それでも自分のやりたいことをすることができます。以下のコードは、過去3年間にわたって不動産業者のサイトから取得したデータを保存しています。

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

スクレーパー関数のこのチャンクは、#2の上に行を挿入し、そこにデータを書き込みます。最初の行はヘッダーなので、それに触れません。タイミングは決めていませんが、問題が発生するのはサイトが変更されたときだけです。

0
HardScale

空のセルを検索するように、ゴティが提供するコードを微調整しました。 isBlank()を使用する代わりに、テキストのある列では値の比較が機能しませんでした(または、その方法がわかりませんでした)。値は! (変数rの前)空白が見つかるまでiを増やしたいので、楽しみにしているとき。空白でない(!が削除された)セルが見つかった場合、iの減少を停止するためにシートを10枚処理します。次に、シートを1つ下の最初のブランクに戻します。

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