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

Попробуем с нуля написать бота, который забирает вопросы квиза с вариантами ответов из таблицы и записывает ответы назад.

Ниже представлен скрин из чата с ботом.

Дисклеймер: если вы здесь в первый раз, пожалуйста ознакомьтесь с первым постом, где более детально разобраны скрипты в гугл таблицах (тыц).


Начнем с создания контейнера и написания скрипта в нем. Создаем новый Spreadsheet.

В качестве забираемых значений укажу 4 вопроса на листе Questions.

И варианты ответов для каждого вопроса на листе Answers. Данные из двух таблиц связаны по ИД – первая колонка.

В таблицу Users буду вносить ответы от пользователей, но об этом позже.

Переходим к скрипту. Укажу в качестве глобальных переменных следующие значения:

const doc = SpreadsheetApp.getActive();
const questionsSheet = doc.getSheetByName("Questions");
const answersSheet = doc.getSheetByName("Answers");
const usersSheet = doc.getSheetByName("Users");

Так как скрипт мы разворачиваем в том же документе (контейнере), то нам достаточно использовать метод getActive(). Если вы ссылаетесь на другой Spreadsheet, то можно на него сослаться методами .openByUrl() или .openById().

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

Теперь попробуем забрать значения из таблицы и вывести в логере. Следующая функция забирает значения из таблицы и возвращает их в виде массива.

function sendQuestions() {
  const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues()

  Logger.log(questionsArr)
}

Запустить функцию можно из меню, выбрав имя функции и кликнув Run

Меню
Меню

Результатом выполнения функции будет массив в логере.

Логер
Логер

Разберем строку в функции по частям.

const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues()

Здесь мы объявляем переменную для записи в нее возвращаемого массива. Далее указываем лист, из которого будем забирать значения questionsSheet. Для листа выбираем метод .getRange() и указываем ячейки, к которым функция должна обратиться.

Перевожу написанное в скобках метода getRange на понятный язык:

getRange(номер строки начала диапазона
         , номер столбца начала диапазона
         , номер строки конца диапазона
         , номер столбца конца диапазона)

Методами листа .getLastRow() и .getLastColumn() получаем номер последней строки и столбца, которые будут равны 4 и 2 соответственно. При этом методы возвращают последние столбец и строку, в которых указано значение (ячейка не пуста) или есть валидация.

Границы заданного диапазона
Границы заданного диапазона

В то же время можно указать в скобках questionsSheet.getRange("A1:B4").

Возвращаемся к функции и к методу диапазона .getValues(). Здесь обратим внимание на то, что существует также метод .getValue(), применять который следует, если мы обращаемся к одной ячейке, например questionsSheet.getRange(3,4) или questionsSheet.getRange("D3").

getValues() при этом возвращает двумерный массив, а getValue() - значение.

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

Итак, функция вернула двумерный массив, соответственно мы можем продолжить работу с массивом и его методами.

По задумке бот отправляет все вопросы одновременно с кнопками. Сначала попробуем просто отправить вопросы без вариантов ответов.

Обратимся к функции send().

function send(msg, chat_id) {
  const payload = {
    'method': 'sendMessage',
    'chat_id': String(chat_id),
    'text': msg,
    'parse_mode': 'HTML'
  }
  const data = {
    'method': 'post',
    'payload': payload,
    'muteHttpExceptions': true
  }
    UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);
}

В функцию мы передаем текст отправляемого ботом сообщения и ид чата, в который это сообщение будет отправлено. Внутри функции объявляем объект payload и указываем передаваемые параметры. Я использую только необходимые в рамках задачи параметры (method, chat_id, text, parse_mode), но их может быть и больше (ссылка на апи телеграмма для метода sendMessage).

В переменную data передаем payload (передаваемые параметры для метода апи "post") и указываем сам метод post.

В строке UrlFetchApp мы обращаемся к АПИ телеги по ссылке (таким же образом можно обратиться к любому другому доступному АПИ). В этой же строке мы передаем значение переменной token и data. Про token чуть позже.

Вернемся к функции sendQuestions() и вызовем из нее send() для каждого элемента массива questionsArr.

Чтобы обратиться к каждому элементу можно воспользоваться циклами типа for или while. Но я предпочитаю использовать методы массивов и стрелочные функции, которые умещаются в одну строку.

Таким образом, вместо конструкции

for (let i=0; i<questionsArr.length; i++) {
    send(questionsArr[i][1],chat_id)
}

я могу написать

questionsArr.forEach(e => send(e[1],chat_id))

Итак, в качестве текста сообщения я забираю из вложенного массива элемент на позиции [1] (на позиции 0 указан ид вопроса).

Нам не достает только чат ид, который я укажу в качестве аргумента функции sendQuestions() и получу следующее:

function sendQuestions(chat_id) {
  const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();

  Logger.log(questionsArr);

  questionsArr.forEach(e => send(e[1],chat_id));
}

Наконец, функцию sendQuestions(chat_id) мы вызовем, когда пользователь запустит бота по команде /start. Пропишем эту логику.

Сначала добавлю функцию, которая парсит возвращаемый из телеграма json пакет.

function doPost(e) {
  const update = JSON.parse(e.postData.contents);
  let msgData = {}
  if (update.hasOwnProperty('message')) {
    msgData = {
      id         : update.message.message_id,
      chat_id    : update.message.chat.id,
      user_name  : update.message.from.username,
      text       : update.message.text,
      is_msg     : true
    };
  }
}

Результатом функции является объект msgData с ключами id, chat_id, user_name, text, is_msg. Возвращаемые ключи вы можете определить сами, для простоты я указала только те, которые нужны для задачи.

Здесь же я передам в функцию отправки вопросов значение ключа chat_id.

function doPost(e) {
  const update = JSON.parse(e.postData.contents);
  let msgData = {}
  if (update.hasOwnProperty('message')) {
    msgData = {
      id         : update.message.message_id,
      chat_id    : update.message.chat.id,
      user_name  : update.message.from.username,
      text       : update.message.text,
      is_msg     : true
    };
  }
  sendQuestions(msgData.chat_id);
}

Как создать бота и получить токен уже описывалось здесь. Берем этот токен и записываем в глобальную переменную token. Далее деплоим приложение (смотреть ссылку выше) и записываем URL веб приложения в переменную appLink.

И наконец функция api_connector() для установки веб хука.

function api_connector() {
  const appLink = "Ваш URL";
  UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+ appLink); 
}

В этой функции мы опять же отправляем запрос на сервер по адресу https://api.telegram.org и дополнительно указываем метод и/или параметры. В данном случае используется метод setWebhook и параметр url, куда мы передаем значение переменной appLink.

Запускаем эту функцию по кнопке Run.

После успешной установки вебхука запускаем бота в телеграме. В ответ получаем вопросы

Весь код целиком:

const doc = SpreadsheetApp.getActive();
const questionsSheet = doc.getSheetByName("Questions");
const answersSheet = doc.getSheetByName("Answers");
const usersSheet = doc.getSheetByName("Users");

const token = "Ваш токен"

function api_connector () {
  const App_link = "Ваш URL";
  UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+App_link); 
}


function doPost(e) {
  const update = JSON.parse(e.postData.contents);
  let msgData = {}
  if (update.hasOwnProperty('message')) {
    msgData = {
      id         : update.message.message_id,
      chat_id    : update.message.chat.id,
      user_name  : update.message.from.username,
      text       : update.message.text,
      is_msg     : true
    };
  }
  
  sendQuestions(msgData.chat_id);
}

function send(msg, chat_id) {
  const payload = {
    'method': 'sendMessage',
    'chat_id': String(chat_id),
    'text': msg,
    'parse_mode': 'HTML'
  }
  const data = {
    'method': 'post',
    'payload': payload,
    'muteHttpExceptions': true
  }
    UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);
}


function sendQuestions(chat_id) {
  const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();

  Logger.log(questionsArr)

  questionsArr.forEach(e => send(e[1],chat_id))

}

Продолжение следует...

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

Как всегда, рада обратной связи, дайте знать, если пишу слишком сложно\ неразборчиво\ без деталей..

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

Продолжение постараюсь подготовить в разумные сроки и показать, как массивы с ответами на вопросы превратить в кнопки. Будет интересно! ????

Комментарии (12)


  1. SWATOPLUS
    00.00.0000 00:00

    А можно код рабочего приложения? Можно без вашей бизнес логики.


    1. Nadjuscha Автор
      00.00.0000 00:00

      добавила код в конец статьи


  1. jonic
    00.00.0000 00:00
    +5

    Давайте будем честны, любая штука может быть базой данных такого рода.


  1. Robastik
    00.00.0000 00:00

    гугл таблицы в качестве базы данных

    В чем профит?

    Сам сторонник извращаться с Excel, но все же какой-то смысл должен быть. Оно же медленно очень, Google Sheets API. По причине то, чего нельзя открыто называть.

    Если привлекает бесплатность, то посмотрите в сторону [NoSQL]https://github.com/RomainVialard/FirebaseApp от того же гугла. То есть код как прежде в Google Script, только в standalone script, а данные в Куфдешьу DB. Сильно быстрее и примерно также наглядно.


  1. Robastik
    00.00.0000 00:00

    гугл таблицы в качестве базы данных

    В чем профит?

    Сам сторонник извращаться с Excel, но все же какой-то смысл должен быть. Оно же медленно очень, Google Sheets API. По причине того, чего нельзя открыто называть.

    Если привлекает бесплатность, то посмотрите в сторону NoSQL от того же гугла. То есть код как прежде в Google Script, только в standalone script, а данные в Realtime DB. Сильно быстрее и примерно также наглядно.


    1. Nadjuscha Автор
      00.00.0000 00:00

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


    1. SWATOPLUS
      00.00.0000 00:00
      +1

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


      1. Robastik
        00.00.0000 00:00

        Чем бот лучше, чем Google Forms? Тем, что в формах валидация из коробки, а боту еще надо

        докинуть базовую валидацию ?

        И если уж

        заполняют, находясь на улице

        , то почему бы им не делать все голосом через гарнитуру? Google Speech вроде для этого делали.


  1. jankri
    00.00.0000 00:00
    +1

    Можно немного проще:

    const questionsArr = questionsSheet.getDataRange().getValues();


  1. chipmunk1
    00.00.0000 00:00

    Хахаа не только я такой извращенец)

    Тоже делал бота с БД в Гугл таблицах, и зашел довольно далеко. Исходя из контекста бот берет определенную строку с конфигурацией, которая определяет как набор данных так и алгоритм.

    Главный минус который у меня всплыл со временем - это постоянно растущая сложность кода, который работает с гугл таблицами, со временем к нему очень сложно возвращаться даже через пару дней, тк апи таблиц, скажем, не самое интуитивное :)


  1. Klochko
    00.00.0000 00:00

    Как-то игрался в такие игры, но перестал. Оказалось, что у AppScript есть определенное время выполнения скрипта. И если это время будет превышено, то скрипт просто завершится ошибкой. Но узнал я об этом, когда после передачи данных в 1200 строк обнаружил в таблице всего 700. Благо было откуда ещё раз запросить эти данные и перелить на БД. После того случая больше я в таблицах ничего не храню.

    Плюс если в таблице много данных, я имею ввиду прям много, то и время поиска и обработки может оказаться слишком высоким и опять же скрипт упадёт. Сейчас достаточно недорогих VPS на котором можно за недорого развернуть и бода и БД которая будет в сотни раз быстрее таблиц.


    1. Robastik
      00.00.0000 00:00

      Сейчас достаточно недорогих VPS на котором можно за недорого развернуть и бода и БД которая будет в сотни раз быстрее таблиц.

      Зачем городить свою инфраструктуру, если есть готовые БД на любой вкус в Google Cloud databases с приличным администрированием и масштабированием? Ведь цена/качество у них явно лучше, чем админ явно невысокого уровня в

      достаточно недорогих VPS ?

      П.С.

      определенное время выполнения скрипта

      после передачи данных в 1200 строк обнаружил в таблице всего 700

      Вместо одного потока на 1200 строк в этом случае делают 30 потоков по 40 строк.