SpreadsheetApp: читаем и пишем

Три объекта — книга, лист, диапазон — и одно правило, которое отличает скрипт на две секунды от скрипта на четыре минуты.

Range (диапазон) — прямоугольный кусок листа: одна ячейка, строка, столбец или блок. Всё чтение и вся запись в Apps Script идут через диапазоны, и именно от их числа зависит скорость скрипта.

Иерархия: книга → лист → диапазон

SpreadsheetApp — не сама таблица, а служба таблиц, точка входа. Дальше идут три уровня, и путать их нельзя.

// 1. Книга (Spreadsheet) — весь файл целиком
const kniga = SpreadsheetApp.getActiveSpreadsheet();

// 2. Лист (Sheet) — одна вкладка внизу
const list = kniga.getSheetByName('Заявки');

// 3. Диапазон (Range) — прямоугольник ячеек
const diapazon = list.getRange('B2:D10');

Что здесь важно:

  • getActiveSpreadsheet() — книга, к которой привязан скрипт. Для отдельного проекта или чужого файла берут SpreadsheetApp.openById('1AbC...'), где идентификатор — длинный кусок из адресной строки таблицы.
  • getSheetByName('Заявки') — лист по имени на ярлычке. Если такого листа нет, метод вернёт null, а не бросит ошибку. Упадёт уже следующая строка — с загадочным TypeError: Cannot read properties of null. Имя чувствительно к регистру и к лишним пробелам.
  • getRange(...) — прямоугольник ячеек. Задать его можно двумя способами.

Два способа адресовать диапазон

list.getRange('C5');          // одна ячейка, привычная A1-нотация
list.getRange('A1:D100');     // блок
list.getRange('B:B');         // весь столбец B

list.getRange(5, 3);          // строка 5, столбец 3 — та же самая C5
list.getRange(2, 1, 99, 4);   // от A2: 99 строк, 4 столбца — тот же A2:D100

Форма getRange(строка, столбец, сколькоСтрок, сколькоСтолбцов) незаменима, когда границы вычисляются в коде. Нумерация здесь начинается с единицы: getRange(1, 1) — это A1. Запомните контраст: сам лист считается с 1, а массивы значений, которые вы вот-вот получите, — с нуля.

Чтение: getValue и getValues

const odno = list.getRange('B2').getValue();        // одно значение: 'Иванов'
const mnogo = list.getRange('A2:C4').getValues();   // двумерный массив

getValues() всегда возвращает массив массивов — «строки, а в каждой строке ячейки», даже если диапазон состоит из одной колонки или одной ячейки.

// mnogo === [
//   ['2026-07-01', 'Иванов',  12000],
//   ['2026-07-01', 'Петрова',  8500],
//   ['2026-07-02', 'Иванов',  15000]
// ]

const summaVtoroy = mnogo[1][2];   // 8500 — нумерация с НУЛЯ

Отсюда типовой шаблон обхода таблицы с шапкой.

function itogiPoMenedzheram() {
  const list = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Заявки');
  const stroki = list.getDataRange().getValues();   // всё заполненное — ОДНИМ вызовом

  const itogi = {};
  for (let i = 1; i < stroki.length; i++) {         // i = 1 — пропускаем шапку
    const menedzher = stroki[i][1];
    const summa = Number(stroki[i][2]) || 0;
    itogi[menedzher] = (itogi[menedzher] || 0) + summa;
  }

  console.log(itogi);   // { Иванов: 27000, Петрова: 8500 }
}

getDataRange() — удобное сокращение: «диапазон от A1 до последней заполненной строки и столбца». Не нужно ни getLastRow(), ни getLastColumn(), ни риска промахнуться на строку.

Запись: setValue и setValues

list.getRange('E1').setValue('Итого');

list.getRange('A10:C11').setValues([
  ['2026-07-03', 'Сидоров', 9000],
  ['2026-07-03', 'Иванов',  4000]
]);

У setValues() железное требование: размер массива обязан в точности совпадать с размером диапазона. A10:C11 — это две строки по три столбца, значит нужен массив из двух подмассивов по три элемента. Не совпало — получите The number of rows in the data does not match the number of rows in the range. Это ошибка номер один при первой записи.

Надёжнее не хардкодить границы, а вычислять их от самих данных:

const dannye = [
  ['2026-07-03', 'Сидоров', 9000],
  ['2026-07-03', 'Иванов',  4000]
];

list
  .getRange(list.getLastRow() + 1, 1, dannye.length, dannye[0].length)
  .setValues(dannye);

Такой код дописывает блок под существующие данные и никогда не разъезжается по размерности. Для одной-единственной строки есть ещё короче: list.appendRow(['2026-07-03', 'Сидоров', 9000]).

Главное правило скорости: диапазоном, а не по ячейке

Вот два скрипта, которые делают ровно одно и то же — записывают 1000 чисел в столбец A.

Плохо — тысяча отдельных обращений к сервису:

function medlenno() {
  const list = SpreadsheetApp.getActiveSheet();

  for (let i = 1; i <= 1000; i++) {
    list.getRange(i, 1).setValue(i * 2);   // 1000 сетевых вызовов
  }
}

Хорошо — один вызов:

function bystro() {
  const list = SpreadsheetApp.getActiveSheet();

  const dannye = [];
  for (let i = 1; i <= 1000; i++) {
    dannye.push([i * 2]);   // готовим массив в памяти — это практически бесплатно
  }

  list.getRange(1, 1, dannye.length, 1).setValues(dannye);   // 1 сетевой вызов
}

Разница не в процентах, а в разы: первый вариант тянется десятки секунд, а на пяти-десяти тысячах строк упирается в шестиминутный лимит и падает. Второй отрабатывает меньше чем за секунду. Причина ровно та, о которой шла речь в первом уроке: каждый getValue и setValue — это сетевой вызов от сервера Apps Script к серверу Таблиц. Сам цикл в JavaScript почти ничего не стоит; стоит поход по сети.

Отсюда универсальный рецепт любого скрипта для таблиц:

  1. Прочитать всё нужное одним getValues().
  2. Посчитать в обычном JavaScript, на массивах в памяти — здесь можно всё и это быстро.
  3. Записать результат одним setValues().

«Прочитал — посчитал — записал». Почти любой медленный скрипт лечится приведением к этой схеме.

А если ячейки нужно красить по одной?

Форматирование подчиняется тому же закону. У диапазона есть множественные версии почти всех методов: setBackgrounds(), setFontColors(), setNumberFormats() — они принимают такой же двумерный массив.

const stroki = list.getDataRange().getValues();

const cveta = stroki.map(stroka => {
  const prosrocheno = stroka[3] === 'просрочено';
  return [prosrocheno ? '#ffd6d6' : '#ffffff'];
});

list.getRange(1, 5, cveta.length, 1).setBackgrounds(cveta);

Одно чтение, один map в памяти, одна запись — вместо тысячи setBackground() в цикле.

Как это работает

  • Скрипт и таблица — разные сервисы Google, между ними сеть. Объект Range — это не данные, а адрес: пока вы не вызвали getValues(), ничего ещё не прочитано.
  • Apps Script копит операции записи в буфер и отправляет их пачкой. Поэтому лист иногда «обновляется не сразу». Сбросить буфер немедленно можно вызовом SpreadsheetApp.flush() — он нужен редко: например, когда вы записали значение и следующей же строкой хотите прочитать пересчитанную формулу.
  • getValues() возвращает типизированные значения, а не строки: даты приходят объектами Date, числа — number, пустая ячейка — пустой строкой. Если нужна ровно та строка, которую видит пользователь, со всем форматированием, есть getDisplayValues().
  • getLastRow() — последняя строка с данными на всём листе, а не в вашем столбце. Если справа висит подпись в 500-й строке, метод вернёт 500, даже когда полезных данных всего двадцать строк.

Частые ошибки

  • Цикл с getValue() или setValue() внутри. Самая дорогая ошибка платформы. Выносите чтение и запись за пределы цикла — всегда.
  • Cannot read properties of null сразу после getSheetByName. Листа с таким именем нет: опечатка, другой регистр, лишний пробел на ярлычке. Страхуйтесь: if (!list) throw new Error('Нет листа Заявки');
  • Путаница в нумерации. getRange считает с единицы, массив от getValues — с нуля. Пятая строка листа — это stroki[4].
  • setValues с одномерным массивом. setValues([1, 2, 3]) упадёт. Нужен двумерный: [[1, 2, 3]] для строки или [[1], [2], [3]] для столбца.
  • getRange('A2:A' + list.getLastRow()) на пустом листе. getLastRow() вернёт 0, получится строка 'A2:A0' — невалидный диапазон и ошибка. Проверяйте, что данные вообще есть.
  • Сравнение дат как строк. В ячейке лежит Date, поэтому stroki[i][0] === '2026-07-01' никогда не окажется истиной. Приводите явно: Utilities.formatDate(stroki[i][0], 'Europe/Moscow', 'yyyy-MM-dd').

Итоги

  • Иерархия: SpreadsheetApp → книга (getActiveSpreadsheet или openById) → лист (getSheetByName) → диапазон (getRange).
  • getValues() и setValues() работают с двумерными массивами; размер массива обязан совпадать с размером диапазона.
  • getDataRange() — самый быстрый способ забрать все данные листа одним вызовом.
  • getRange нумерует с единицы, массивы — с нуля. Половина ошибок новичка растёт отсюда.
  • Главное правило: прочитал одним вызовом — посчитал в памяти — записал одним вызовом. Цикл с setValue внутри — прямая дорога к шестиминутному таймауту.
Проверьте себя
1. Почему цикл, который тысячу раз вызывает getRange(i, 1).setValue(x), работает в десятки раз медленнее одного setValues()?
AJavaScript плохо оптимизирует длинные циклы for
BКаждый setValue — это отдельный сетевой вызов к сервису Таблиц, и тысяча ячеек означает тысячу вызовов
CsetValue пересчитывает все формулы книги после каждой ячейки, а setValues — нет
DGoogle искусственно замедляет одиночные записи, чтобы стимулировать переход на Workspace
2. Какой массив нужно передать в setValues(), чтобы записать числа 1, 2 и 3 в три ячейки одного столбца?
A[1, 2, 3]
B[[1, 2, 3]]
C[[1], [2], [3]]
D{ 1: 1, 2: 2, 3: 3 }