Google script

Google script

Доброго времени суток, дорогие читатели!

Как некоторые могут помнить, у нас была одна статья на тему программирования в Google таблицах. Она была больше как эксперимент, который показал, что в общем-то это направление довольно популярно (да-да, про эксель я не забыл, он тоже будет… рано или поздно, но будет).

Я не стал включать ту статью в этот, пока еще начинающийся, цикл, а решил начать его с самого нуля. Цель его довольно простая — показать, что всё не так уж и сложно, если знать некоторые особенности программирования в целом. Поэтому я постараюсь не столько расписать примеры, сколько объяснить как именно придти к такому решению.

Тем, кто уже знаком с такими понятиями, как: функция, переменная, оператор, массив и тому подобное; будет (по всей видимости) скучновато, так что в таком случае рекомендую просмотреть статью по диагонали, вдруг что интересное для себя найдете.

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

С чего начинается любой скрипт? Очевидно, что с какого-то кода, но с какого? Вполне может так быть, что у вас уже появляются такие мысли при виде этого, а точнее того, белого экрана. Но это нормально. Когда мало опыта и много непонятного, хочется все бросить и лечь спать. Но не отчаивайтесь! И отойдите от кровати! Если я не буду писать это в некотором ироничном виде, то от моей заунывной статьи вы уже на третьей строчке усне..

Пишем и используем Google Scripts: Line Zero, — Beginning

Так вот, с чего нам начать? Я сейчас не имею в виду алгоритм действий, который вы держите в голове, или блок-схемы на листочке, нет. Нам следует начать с… объявления функции.

Что же это такое? Если говорить простым языком, то функция — это одна из множества позиций в торговом (вендинговом) автомате (ваш скрипт). Вы выбираете позицию автомата (указываете функцию), скармливаете ему деньги (параметры функции) и получаете какой-то предмет канцерогенной индустрии (какой-то результат). При этом вы можете создать таких позиций (функций) бесчисленное множество.

Объявление функции имеет определенный формат вида:

function FUNCTIONNAME () { };

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

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

Итак, вы определились с вашей первой функцией, теперь что? А теперь нужно объявить переменные. Для вашей позиции в торговом автомате переменными могут быть: номер ряда, номер колонки, картинка позиции, её цена.

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

var variablename;

И ой, она уже объявлена. А для чего это нужно?

Когда компилятор (такая штука, которая превращает удобочитаемый код для нас в машинный набор нулей и единиц для компьютера) так вот, когда компилятор начинает последовательно читать ваш код (а делает это он либо при сохранении вами кода, либо при запуске) то встречая неизвестные ему названия он первым и последним делом ищет где вы объявили ему (рассказали что это такое и что с этим делать) эти названия.

Грубо говоря, если вы напишете e = m * c ^2 без объявления переменных, то компилятор перед запуском кода выдаст ошибку о том, что он вообще не имеет ни малейшего понятия кто такие e, m, с и что им всем от него нужно. А вот если вы напишете:

var e = 0, m = 1, c = 2;

То он вам посчитает безо всякого труда, потому что он уже знает что e, m, c, — это числа, имеющие значения 0, 1, 2 соответственно.

Однако, если вы напишете:

var e = «энергия», m = «масса», c = 300000;

То он вам, конечно, посчитает выражение e = m * c ^2, но его ответом будет e = 2, потому что у вас несогласованны типы (m — это строка, c — это число) и он просто отбросит строки (VBA, например, здесь выдаст ошибку что у вас неправильные типы данных).

Если же вы объявите переменную, но не напишете чему она равна, то компилятор будет считать, что значение не определено (undefined).

Что касается самих выражений, здесь не совсем обычная математика (а кто говорил, что будет легко?). Если там выражение e = m*c^2 будет эквивалентно выражению m*c^2 = e, то здесь не так. Здесь вообще оператор под названием «равно» выглядит иначе и обозначается как «==». А то, что написано выше, это не «е равно эм * це квадрат», это читается как «е присвоить эм*це квадрат» и данный оператор называется оператором присваивания. Вот этот момент очень важен и здесь не следует путать.

Давайте подробнее остановимся на операторах. Есть несколько категорий:

Пишем и используем Google Script: Line 1, — I Don’t get it

Итак, в общем-то с основами разобрались. Теперь немного практики. Задача:

У нас есть некоторый набор значений в ячейках (cкажем, три ряда по четыре колонки случайных чисел), нам надо отдельно вывести сумму каждого ряда и его среднее значение (учитывая что мы не знаем о встроенных функциях, а пишем сами).

Таблица для нашего примера:

A B C D
1 2 3 4 1
2 -4 12 3 5
3 1 1 2 5

Итак, с чего начать? Нет, не с написания кода, сначала надо прикинуть план действий (хотя бы очень грубо).

  1. Как посчитать сумму? Очевидно, сложить каждое значение в ряду. Окей, кажется несложным.
  2. Как получить среднее значение? Можно сложить каждое значение ряда и поделить на кол-во значений. Но зачем заново считать, если мы сумму уже посчитали на предыдущем шаге? Окей, тоже легко.
  3. Остался один вопрос, — как получить значение из ячейки и как его потом туда записать? А вот тут-то и начинаются сложности.

Путем нехитрого гугления мы достаточно быстро выясним как вытащить значение ячейки из таблицы. Но какой смысл копировать код, ничего в нем не понимая? Правильно, никакого. С точки зрения.. ммм.. гугла таблица представляется собой объект (класс), состоящий из множества подклассов (типа матрешки). Иерархия там примерно такая:

  • SpreadsheetApp, — самый верхний уровень, указывает, что мы хотим работать с таблицами (создавать, удалять, изменять и вообще делать с ними все, что можно. По ссылке можно изучить все свойства и методы этого класса. Т.е. все, что с ним можно сделать);
  • Spreadsheet, — класс, который отвечает за действия с самим листом таблицы (копирование, переименование, защита, сокрытие, права доступа и тп);
  • Sheet, — класс, который отвечает за действия на каком-то листе таблицы ( добавление ряда, добавление графиков, определение размера таблицы и многие другие). Во многих случаях можно использовать его вместо класса Spreasheet;
  • Range, — класс, который отвечает за действия с каким-то определенным диапазоном ячеек на листе (включая выбор диапазона).

Таким образом, мы можем получить значение как одной ячейки, так и диапазона ячеек. Воспользуемся вторым вариантом. Для этого пишем соотв. код:

Создаем нашу функцию:

function myfunction(){ };

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

var sheet = SpreadsheetApp.getActiveSheet(); // Ссылаемся на текущий лист в соответствии с иерархией. В переменной sheet будет ссылка на наш лист

Да, можно вставлять комментарии, которые компилятор будет игнорировать (и подсветит оранжевым). Для этого перед словом/фразой/строчкой достаточно добавить два слеша // и вся оставшаяся строка будет закомментирована. Если же надо закомментировать много строк, то лучше воспользоваться вторым вариантом — поставить в начале комментария символы /*, а в конце */. Т.е. Комментарий целиком будет выглядеть примерно так:

/*————- —Комментарий— —————*/

Теперь достанем наши значения из ячеек:

var dataArray = sheet.getRange(«A1:D3»).getValues(); // Получаем значения ячеек A1:D3 в переменную dataArray

При этом особенностью метода getValues является то, что на выходе мы получаем матрицу (двумерный массив или по простому — таблицу), где первый разряд означает ряды, второй — колонки. В нашем примере, например, элемент массива dataArray будет означать… нет, не нулевой ряд, а первый ряд и вторая колонка (т.к. нумерация ячеек идет с единицы, а нумерация массивов всегда идет с нуля).

Теперь мы получили значения наших ячеек и можем с ними работать. Как мы будем суммировать? Самое простое — явно:

var sum1 = dataArray + dataArray + dataArray + dataArray; var sum2 = dataArray + dataArray + dataArray + dataArray; var sum3 = dataArray + dataArray + dataArray + dataArray;

Да, не очень красиво, но работает. Кстати проверить результат можно несколькими способами:

  1. Записать в журнал. Для этого добавить после var sum3 строчку Logger.log («\nСумма 1 ряда: «+sum1+»\nСумма 2 ряда: «+sum2+»\nСумма 3 ряда: «+sum3);
    Где \n — это распространенное обозначение перевода строки (Enter). После этого в том же редакторе скриптов переходим в пункт меню «Вид — Журналы»:
  2. Вывести оповещение в браузере: Browser.msgBox(«Суммы трех рядов: «+sum1+» «+sum2+» «+sum3);
    При выполнении скрипта перейдите на ваш лист и увидите всплывающее окно
  3. Поставить Breakpoint (1) напротив var sum3, после чего нажать не на старт, а на иконку с жуком (2, означает debug). У вас напротив переменной sum3 (4) будет написано «undefined» (на скрине этот этап пропущен). Это потому, что скрипт сейчас остановился перед этой строкой и он еще не отработал её. Для того, чтобы он сделал следующее действие, нажмите на кнопку со стрелкой (3) и вуаля (4), переменная посчитана (наиболее удобный способ отладки и поиска косяков). Но это сработает, если у вас в функции код на этой строке не заканчивается (я обычно добавляю функцию Logger.log и ставлю Breakpoint на ней:

У кого не получилось, код целиком ниже:

function myfunction(){ var sheet = SpreadsheetApp.getActiveSheet(); // Ссылаемся на текущий лист таблицы в соответствии с иерархией. В переменной sheet будет ссылка на наш лист /*————- —Комментарий— —————*/ var dataArray = sheet.getRange(«A1:D3»).getValues(); // Получаем значения ячеек A1:D3 в переменную dataArray var sum1 = dataArray +dataArray + dataArray + dataArray; var sum2 = dataArray +dataArray + dataArray + dataArray; var sum3 = dataArray +dataArray + dataArray + dataArray; Logger.log («\nСумма 1 ряда: «+sum1+»\nСумма 2 ряда: «+sum2+»\nСумма 3 ряда: «+sum3); };

Итак, первый пункт выполнен. Теперь приступаем ко второму. Учитывая, что мы (теперь) знаем сумму ряда и знали с самого начала кол-во чисел в ряду, добавляем еще три переменные и проверяем (значком {B} я обозначаю место, где можно поставить Breakpoint для быстрого просмотра результатов):

var avg1 = sum1/4; var avg2 = sum2/4; var avg3 = sum3/4; {B}Logger.log();

Что ж. Пункт 2 выполнен. осталось это дело сохранить, например в ячейках E, F. Мы уже знаем как достать данные из ячеек, а вот как их туда поместить? В общем-то тут никакой магии и нет, раз есть функция get, то можно догадаться, что есть функция и set.

При этом, мы должны указать данные точно в таком же формате, как мы их получили. Присваивание значений массивам в общем-то достаточно простая задача. Допустим у нас есть значения 1, 2, 3, 4, 5 и нам нужен массив. Объявляем:

var array1 = ; // если нам явно нужны числа var array2 = ; // Если нам неважно числа это или символы

И всё 🙂

Однако это массив с одним разрядом (одномерный), нам же нужен двумерный, как сделать его? Для этого используется конструкция вида ], как ниже:

var array2d = , , ];

Выглядит криповато, но массив можно записать еще таким образом:

var array2d = , , ];

А это уже другое дело! Хотя тем, кто на этих массивах уже не первую собаку съел, первый способ может быть удобней, т.к. быстрее.

Теперь же осталось дело за малым. Записать сумму и среднее для каждого ряда:

sheet.getRange(«E1:F3»).setValues(, , ]);

Запускаем и смотрим что у нас происходит на листе:

Послесловие

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

Теперь вы имеете представление что такое функция, переменная, зачем они нужны, какие операторы бывают и что делают, а также усвоили (я надеюсь) базовые навыки по работе с таблицами.

В следующей серии поговорим о том, как можно улучшить код, дабы не плодить кучу переменных и иметь возможность работать с таблицей значений переменной длинны (т.е. когда мы заранее не знаем сколько рядов и строк нам надо). А также немножко разобьем все это дело на несколько функций. Stay Tunned!

P.S. За существование оной статьи отдельное спасибо другу проекта и члену нашей команды под ником «barn4k».
P.S.2: Часть 2 живет .


У нас было две гугл-формы, 75 вопросов в каждой, 5 бизнес-пользователей, которые активно правили эти формы, а еще гугл-скрипт, экспортирующий форму в JSON. Не то что бы его было сложно каждый раз запускать руками, но раз начал автоматизировать свою работу, то иди в этом увлечении до конца.
В официальной документации сам черт ногу сломит, поэтому под катом мы подробно рассмотрим удаленную загрузку и запуск Google Apps Script через REST API, используя Python.

Введение

У нас в «Доктор рядом» разрабатывается платформа для чат-ботов, в которой для описания сценариев используются гугл-формы. Соответственно, хочется из форм по нажатию кнопки получать JSON, содержащий ноды (пункты формы) и метаданные к ним (переходы между нодами, типы нод, их название). Казалось бы, желание простое, но гугл не поддерживает данный функционал и приходится собирать этот «экспортер» своими руками. Рассмотрим по шагам процесс его создания.

ШАГ 1. Google Apps Script

Google предусмотрел возможность взаимодействия со своими сервисами (Sheets, Docs, Forms) через Google Apps Script — скрипты, написанные на языке google script (.gs). Данная статья не предусматривает разбора языка google script, поэтому приведу пример готового скрипта, создающего JSON из имеющейся гугл-формы. За основу был взят с гитхаба код пользователя Steven Schmatz, за что выражаю ему свою благодарность.
Код скрипта // Steven Schmatz // Humanitas Labs // 13 October, 2016. // Roman Shekhovtsov // dr-telemed.ru // Autumn 2019 // Nikita Orekhov // dr-telemed.ru // Autumn 2019 /** * Converts the given form URL into a JSON object. */ function main() { form_url = «<YOUR_FORM_URL>» var form = FormApp.openByUrl(form_url); var items = form.getItems(); var result = { «metadata»: getFormMetadata(form), «items»: items.map(itemToObject), «count»: items.length }; // sendEmail(«<YOUR_EMAIL>», result) return result; } /** If we want to receive data by email * Sends JSON as text to recipient email * @param recipient: String * @param result: JSON */ function sendEmail(recipient, json_file){ var subject = «google form json import» var body = JSON.stringify(json_file); Logger.log(body); MailApp.sendEmail(recipient, subject, body); } /** * Returns the form metadata object for the given Form object. * @param form: Form * @returns (Object) object of form metadata. */ function getFormMetadata(form) { return { «title»: form.getTitle(), «id»: form.getId(), «description»: form.getDescription(), «publishedUrl»: form.getPublishedUrl(), «editorEmails»: form.getEditors().map(function(user) { return user.getEmail() }), «count»: form.getItems().length, «confirmationMessage»: form.getConfirmationMessage(), «customClosedFormMessage»: form.getCustomClosedFormMessage() }; } /** * Returns an Object for a given Item. * @param item: Item * @returns (Object) object for the given item. */ function itemToObject(item) { var data = {}; data.type = item.getType().toString(); // Downcast items to access type-specific properties var itemTypeConstructorName = snakeCaseToCamelCase(«AS_» + item.getType().toString() + «_ITEM»); var typedItem = item(); // Keys with a prefix of «get» have «get» stripped var getKeysRaw = Object.keys(typedItem).filter(function(s) {return s.indexOf(«get») == 0}); getKeysRaw.map(function(getKey) { var propName = getKey.toLowerCase() + getKey.substr(4); // Image data, choices, and type come in the form of objects / enums if (.indexOf(propName) != -1) {return}; // Skip feedback-related keys if («getFeedbackForIncorrect».equals(getKey) || «getFeedbackForCorrect».equals(getKey) || «getGeneralFeedback».equals(getKey)) {return}; var propValue = typedItem(); data = propValue; }); // Bool keys are included as-is var boolKeys = Object.keys(typedItem).filter(function(s) { return (s.indexOf(«is») == 0) || (s.indexOf(«has») == 0) || (s.indexOf(«includes») == 0); }); boolKeys.map(function(boolKey) { var propName = boolKey; var propValue = typedItem(); data = propValue; }); // Handle image data and list choices switch (item.getType()) { case FormApp.ItemType.LIST: case FormApp.ItemType.CHECKBOX: data.choices = typedItem.getChoices().map(function(choice) { return choice.getValue() }); case FormApp.ItemType.MULTIPLE_CHOICE: data.choices = typedItem.getChoices().map(function(choice) { gotoPage = choice.getGotoPage() if (gotoPage == null) return choice.getValue() else return { «value»: choice.getValue(), «gotoPage»:choice.getGotoPage().getId() }; }); break; case FormApp.ItemType.IMAGE: data.alignment = typedItem.getAlignment().toString(); if (item.getType() == FormApp.ItemType.VIDEO) { return; } var imageBlob = typedItem.getImage(); data.imageBlob = { «dataAsString»: «», //imageBlob.getDataAsString(), — BLOB too big «name»: imageBlob.getName(), «isGoogleType»: imageBlob.isGoogleType() }; break; case FormApp.ItemType.PAGE_BREAK: data.pageNavigationType = typedItem.getPageNavigationType().toString(); break; default: break; } // Have to do this because for some reason Google Scripts API doesn’t have a // native VIDEO type if (item.getType().toString() === «VIDEO») { data.alignment = typedItem.getAlignment().toString(); } return data; } /** * Converts a SNAKE_CASE string to a camelCase string. * @param s: string in snake_case * @returns (string) the camelCase version of that string */ function snakeCaseToCamelCase(s) { return s.toLowerCase().replace(/(\_\w)/g, function(m) {return m.toUpperCase();}); }
Что происходит в коде:

  • функция getFormMetadata — возвращает JSON с метаданными формы
  • функция itemToObject — преобразует объект form.item в JSON с требуемыми полями
  • функция sendEmail — отправляет текстом JSON файл на указанную почту
  • функция main — возвращает итоговый JSON
  • переменная form_url в функции main — адрес нашей гугл-формы

ШАГ 2. Тестирование скрипта

В данный момент работоспособность скрипта можно проверить следующим образом:

  1. создаем свой App Script проект
  2. копируем в него код
  3. вместо <YOUR_FORM_URL> подставляем адрес своей формы вида docs.google.com/forms/d/FORM_IDENTIFICATOR/edit
  4. раскомментируем вызов функции sendEmail в функции main
  5. вместо <YOUR_EMAIL> подставляем адрес своей электронной почты, на которую хотим получить JSON
  6. сохраняем проект
  7. запускаем функцию main
  8. если это первый запуск скрипта, то система сообщит о необходимости дать скрипту разрешение отправлять email с вашего адреса. Не стоит пугаться. Это стандартная процедура, необходимая для тестирования скрипта. Проходим «Review permissions» -> выбираем свой аккаунт -> «Advanced» -> «Go to PROJECT_NAME project (unsafe)» -> «Allow»
  9. ждем отработки скрипта
  10. смотрим в почтовый ящик и видим JSON файл в текстовом виде

Все бы ничего, но дальнейшее использование полученных данных предполагает ручное копирование из почты, обработку этого текста (в питоне, например) и сохранение итогового файла. Звучит не слишком production-ready. Автоматизируем запуск этого скрипта и получение его результата через Google Apps Script API, однако прежде настроим наш гугл-проект соответствующим образом.
Внимание: Для удобства понимания происходящего, ниже я буду ссылаться только на две страницы, поэтому рекомендуется открыть их в соседних вкладках:

  1. Страница скриптов / редактирования скрипта — «страница 1»
  2. Страница Google Cloud Platform — «страница 2»

ШАГ 3. Настройка Google Cloud Platform

Заходим в Google Cloud Platform (страница 2), создаем новый проект. Необходимо создать новый проект, ибо по умолчанию статус проекта — Default, а для наших целей требуется Standart. Подробнее можно почитать (пункт 3).
Возвращаемся на страницу 2, переходим во вкладку «API и сервисы», затем «Окно запроса доступа OAuth». Ставим User Type «Внешний».
В появившемся окне заполняем «Название приложения».
Открываем главную страницу в Google Cloud Platform. Из блока «Информация о проекте» копируем номер проекта.
Переходим на страницу 1. Открываем ранее созданный скрипт. В открывшемся окне редактирования скрипта идем в «Resources» -> «Cloud Platform project». В поле «Change project» вводим скопированный ранее номер проекта. Теперь этот скрипт ассоциирован с созданным проектом.

ШАГ 4. Python REST API

Пришло время автоматизировать работу скрипта с помощью REST API. В качестве языка был использован Python.

Логин в Apps Script API

Код должен иметь доступ к проекту, поэтому первая и очень важная процедура — логин в Apps Script API. Открываем страницу 2 -> «API и сервисы» -> «Учетные данные» -> «Создать учетные данные» -> «Идентификатор клиента OAuth» -> «Другие типы». Называем наш идентификатор, переходим в него. Находясь во вкладке «Учетные данные», выбираем «Скачать файл JSON». При этом будет загружен файл ключей для доступа из кода к проекту в гугле. Помещаем данный файл в папку «credentials».

Теперь необходимо дать разрешение на использование API (в нашем случае Apps Script API) в рамках данного проекта. Для этого переходим в «API и сервисы» -> «Библиотека» -> набираем в поиске «Apps Script API» и нажимаем «Включить».
Приложения, взаимодействующие с гуглом, имеют скоп разрешений, которые пользователь должен дать при его запуске. Этот скоп зависит от функций, используемых конкретным скриптом и узнать его можно, пройдя на странице 1 в окне редактирования скрипта в «File» -> «Project Properties» -> «Scopes». Указанные разрешения следует сохранить для дальнейшего использования в коде.
В таком случае функция логина будет выглядеть следующим образом:
import pickle import os.path from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request def login(config): try: creds = None # The file token.pickle stores the user’s access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. token_file = config + config credentials_file = config + config if os.path.exists(token_file): with open(token_file, ‘rb’) as token: creds = pickle.load(token) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(credentials_file, config) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(token_file, ‘wb’) as token: pickle.dump(creds, token) service = build(‘script’, ‘v1’, credentials=creds) pprint(‘Login successful’) return service except Exception as e: pprint(f’Login failure: {e}’) return None
Данный блок кода — стандартная процедура для начала работы с Google App Script.
Мы используем аутентификационный токен и, осуществляя login, либо создаем новый токен, либо используем уже существующий.
Для удобства был создан конфигурационный JSON файл, имеющий следующий вид:
{ «SCOPES»: , «credentials_path»: «credentials/», «credentials_file»: «google_test_project.json», «token_file»: «token.pickle» }
Важно: токен создается для аутентификации с конкретным скопом разрешений. Иными словами, при изменении скопа разрешений, следует удалить токен и при логине создать новый.

Удаленное обновление кода скрипта

Теперь научимся удаленно обновлять код скрипта, затем запускать этот код и получать результат. На самом деле, помимо того кода, что мы запускаем в гугловском редакторе, есть еще манифест файл, в котором указаны права запуска, настройки деплоймента и т.д. Подробнее ознакомиться с его структурой можно по .
Чтобы посмотреть на дефолтный манифест файл, созданный гуглом к вашему скрипту, пройдите в редакторе скрипта в «View» -> «Show manifest file». Манифест появится в списке файлов, относящихся к данному скрипту.
Речь о манифесте зашла неспроста: для удаленного обновления скрипта требуется загрузка кода обоих файлов (*.gs) и манифеста (appscript.json).
Для начала читаем код .gs файла, который мы хотим задеплоить:
with open(‘export-google-form.gs’, ‘r’) as f: sample_code = f.read()
Теперь скопируем автоматически сгенерированный манифест и немного модифицируем его под наши цели. Документация достаточно исчерпывающе описывает структуру манифест файла, поэтому останавливаться на этом моменте я не буду. Для работы скрипта необходимо добавить в дефолтный манифест секцию «executionApi», которая требуется для удаленного запуска скрипта через API. В данной секции указываем круг лиц, имеющих возможность запускать его. Я разрешил запуск для всех, кто прошел авторизацию, что соответствует идентификатору «ANYONE»:
MANIFEST = »’ { «timeZone»: «America/New_York», «exceptionLogging»: «STACKDRIVER», «executionApi»: { «access»: «ANYONE» } } »’.strip()
Тело запроса на обновление должно содержать в себе массив файлов со следующей структурой:

  • name: имя файла, который будет создан на сервере, без расширения
  • type: тип файла (JSON для манифеста, SERVER_JS для .gs)
  • source: код файла

request = { ‘files’: }
Наконец, сам запрос на обновление должен содержать в себе тело (request, описанный выше) и ID скрипта. Последнее можно получить, пройдя в редакторе скрипта в «File» -> «Project Properties» и скопировав «Script ID»:
script_id = ‘qwertyuiopQWERTYUIOPasdfghjkl123456789zxcvbnmASDFGHJKL54’
У объекта service, полученного в результате логина, получаем поле projects() и вызываем метод updateContent(), после чего у полученного объекта HttpRequest вызываем метод execute():
service.projects().updateContent( body=request, scriptId=script_id ).execute()
Однако, на данный момент запуск кода приведет к ошибке:
«error»: { «code»: 403, «message»: «Request had insufficient authentication scopes.», «status»: «PERMISSION_DENIED» }
Как видим, не хватает разрешений в аутентификационном скопе, который мы указали ранее. Обратимся к официальной документации по API, а именно к методу updateContent, который мы использовали для удаленного обновления скрипта. Документация гласит, что использование этого метода требует включения разрешения доступа к script.projects:
https://www.googleapis.com/auth/script.projects
Добавим его в нашу конфигурационный файл в секцию SCOPES. Как я уже писал выше, при изменении скопа, необходимо удалять автоматически создающийся токен.
Отлично! На данный момент мы научились удаленно обновлять гугл скрипт. Осталось его запустить и получить результат выполнения.

Запуск скрипта

Запрос на запуск скрипта содержит scriptID и body со следующей структурой:

  • function: имя функции, которую мы хотим запускать
  • parameters: (optional) набор параметров примитивного типа (string, array …), передаваемых в функцию
  • sessionState: (optional) требуется только для Android приложений
  • devMode: (optional) True в случае если юзер является владельцем скрипта и тогда будет запущена самая свежая версия, нежели та, что задеплоена с помощью Apps Script API. (по дефолту — False)

Для того чтобы не зашивать URL гугл-формы в скрипте, будем передавать form_url в функцию main как аргумент.

Внимание. Когда мы тестировали скрипт, то функция main ничего не принимала, поэтому изменим первые строки кода в .gs файле следующим образом:
function main(form_url) { var form = FormApp.openByUrl(form_url); …….
Так как наше приложение не под Android и мы являемся владельцами скрипта, то в итоге body будет выглядеть так:
body = { «function»: «main», «devMode»: True, «parameters»: form_url }
Запустим скрипт и запишем результат выполнения в переменную resp:
resp = service.scripts().run(scriptId=script_id, body=body).execute()
Сохраним resp в файл с удобным JSON форматированием:
import json with open(‘habr_auto.json’, ‘w’, encoding=’utf-8′) as f: json.dump(resp, f, ensure_ascii=False, indent=4)
Внимание. В силу того, что script.run() запрос ждет результат через сокет, то при превышении таймаута временем выполнения, произойдет ошибка следующего типа:
socket.timeout: The read operation timed out
Для избежания такого поведения, рекомендую в начале программы установить лимит на открытое время сокета, заведомо достаточное для того чтобы он дождался окончания выполнения скрипта. В моем случае 120 секунд достаточно:
import socket socket.setdefaulttimeout(120)
Вуаля! Удобный пайплайн для удаленного обновления и запуска гугл скриптов готов. Полный код, приспособленный к запуску из терминала приведен в моем гитхабе.
Также, приведу код основных функций ниже
login.py from pprint import pprint import pickle import os.path from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request def login(config): try: creds = None # The file token.pickle stores the user’s access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. token_file = config + config credentials_file = config + config if os.path.exists(token_file): with open(token_file, ‘rb’) as token: creds = pickle.load(token) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(credentials_file, config) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(token_file, ‘wb’) as token: pickle.dump(creds, token) service = build(‘script’, ‘v1’, credentials=creds) pprint(‘Login successful’) return service except Exception as e: pprint(f’Login failure: {e}’) return None
update_script.py from pprint import pprint import json import sys from googleapiclient import errors from google_habr_login import login MANIFEST = »’ { «timeZone»: «America/New_York», «exceptionLogging»: «STACKDRIVER», «executionApi»: { «access»: «ANYONE» } } »’.strip() def update_project(service, script_id, script_file_name): # Read from file code we want to deploy with open(script_file_name, ‘r’) as f: sample_code = f.read() # Upload two files to the project request = { ‘files’: } # Update files in the project service.projects().updateContent( body=request, scriptId=script_id ).execute() pprint(‘Project was successfully updated’) def main(): try: args = sys.argv if len(args) != 4: raise TypeError(‘Wrong number of arguments. Three argument required: <config_file_name>, <script_id> and ‘ ‘<script_file_name>’) config_file_name = args script_id = args script_file_name = args with open(config_file_name, «r») as f: config = json.load(f) service = login(config) update_project(service, script_id, script_file_name) except (errors.HttpError, ) as error: # The API encountered a problem. pprint(error.content.decode(‘utf-8’)) if __name__ == ‘__main__’: main()
export_form.py from pprint import pprint import socket import json import sys from googleapiclient import errors from google_habr_login import login socket.setdefaulttimeout(120) # Get JSON, which is returned by script def get_json(service, file_name, script_id, form_url): pprint(‘Exporting form…’) body = { «function»: «main», «devMode»: True, «parameters»: form_url } # Get JSON from script resp = service.scripts().run(scriptId=script_id, body=body).execute() # Write out JSON to file with open(file_name, ‘w’, encoding=’utf-8′) as f: json.dump(resp, f, ensure_ascii=False, indent=4) pprint(‘Form was successfully exported’) def main(): try: args = sys.argv if len(args) != 5: raise TypeError(‘Wrong number of arguments. Four arguments required: <config_file_name>, ‘ ‘<result_file_name>, <script_id> and <google_form_url>’) config_file_name = args file_name = args script_id = args form_url = args with open(config_file_name, «r») as f: config = json.load(f) service = login(config) get_json(service, file_name, script_id, form_url) except (errors.HttpError, ) as error: # The API encountered a problem. pprint(error.content.decode(‘utf-8’)) if __name__ == ‘__main__’: main()
Для запуска необходимо поместить JSON файл с ключами доступа к гуглу в папку «credentials», а конфигурационный JSON в одной директории со скриптами.
Затем, если мы хотим удаленно обновить скрипт, то в терминале вызвать:
python update_script.py <config_file_name> <script_id> <script_file_name>
В данном случае:

  • config_file_name — имя конфигурационного JSON файла
  • script_id — ID скрипта
  • script_file_name — имя .gs файла, который будет залит в гугл

Для запуска скрипта надо вызвать:

Пусть нашей условной задачей будет выгрузка данных формы обратной связи в таблицу Гугл. В форме будет три поля: Имя, Телефон и сообщение. Как созздавать форму обсуждать не будем. Здесь рассмотрим создание скрипта в таблице и обработчик формы.

Заходим на Google Docs и создаем новую таблицу. Заходим в меню Инструменты/Редактор скриптов…

Вставляем следующий код

function doGet(e){ var sheet = SpreadsheetApp.openById(«1XpqAz2DB3r-hNbIIsJ7teMjyuTix6GVLZrMi4Glsz-Y»); var n=sheet.getRange(«A1»).getValue()+2; var d = new Date(); sheet.getRange(«A»+n).setValue(d); sheet.getRange(«B»+n).setValue(e.parameter.p1); sheet.getRange(«C»+n).setValue(e.parameter.p2); sheet.getRange(«D»+n).setValue(e.parameter.p3); sheet.getRange(«A1»).setValue(n-1); }

Вместо 1XpqAz2DB3r-hNbIIsJ7teMjyuTix6GVLZrMi4Glsz-Y вставляем ID созданной вами таблицы. ID таблицы можно посмотреть в адресной строке открытого документа.

Осталось только опубликовать новое веб-приложение. Для этого, выбираем в меню Публикация/Развернуть как веб-приложение. Выбираем «Как запускать приложение: от моего имени», «Кто имеет доступ к приложению: все, включая анонимных пользователей». Ждем ОК.

Далее создаем обработчик формы на нашем сайте.

jQuery(document).ready(function() { jQuery(‘#moya_forma form’).submit(function() { var Name = jQuery(«#Name»).val(); var Phone = jQuery(«#Phone»).val(); var Quession = jQuery(«#Quession»).val(); var http = new XMLHttpRequest(); var url = «https://script.google.com/macros/s/AKfycbw1fEVCr9ib6GJ9JzAD-Tmy-ZWCRNLwZhW1Q6AGsJ7IcBwhL4W-/exec»; var params = «p1=»+Name+»&p2=»+Phone+»&p3=»+Quession; http.open(«GET», url+»?»+params, true); http.onreadystatechange = function() { if(http.readyState == 4 && http.status == 200) { //alert(http.responseText); } } http.send(null); }); });

Заметьте, в переменной url мы указали ссылку на наш скрипт Гугл. Параметрами p1-p3 мы передали значения полей для ввода методом GET.

Не забудьте предварительно подключить jQuery.


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *