Google Forms exportieren + Google Script über REST-API (Python) herunterladen



Wir hatten zwei Google-Formulare mit jeweils 75 Fragen, fünf Geschäftsbenutzer, die diese Formulare aktiv bearbeitet haben, sowie ein Google-Skript, mit dem das Formular in JSON exportiert wurde. Nicht, dass es schwierig wäre, es jedes Mal mit den Händen auszuführen, aber wenn Sie einmal damit begonnen haben, Ihre Arbeit zu automatisieren, gehen Sie in diesem Hobby zu Ende.

In der offiziellen Dokumentation wird sich der Teufel das Bein brechen, daher werden wir uns unter der Katze den Remote-Download und das Starten von Google Apps Script über die REST-API mit Python genauer ansehen.

Einleitung


In Doctor Near entwickeln wir eine Plattform für Chat-Bots, in der Google-Formulare zur Beschreibung von Szenarien verwendet werden. Dementsprechend möchte ich von den Formularen auf Knopfdruck einen JSON erhalten, der Knoten (Formpunkte) und Metadaten zu ihnen (Übergänge zwischen Knoten, Knotentypen, deren Namen) enthält. Es scheint, dass der Wunsch einfach ist, aber Google unterstützt diese Funktionalität nicht und Sie müssen diesen "Exporteur" mit Ihren eigenen Händen zusammenbauen. Betrachten Sie die Schritte, um es zu erstellen.

SCHRITT 1. Google Apps Script


Google bietet die Möglichkeit, mit seinen Diensten (Tabellen, Dokumente, Formulare) über Google Apps Script zu interagieren - Skripte, die in Google Script (.gs) geschrieben sind. Dieser Artikel enthält keine Analyse der Google-Skriptsprache. Ich werde daher ein Beispiel für ein fertiges Skript geben, mit dem JSON aus einem vorhandenen Google-Formular erstellt wird. Der Benutzercode Steven Schmatz wurde aus dem Github als Grundlage genommen, wofür ich ihm meinen Dank aussprechen möchte.

Skriptcode
// 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[itemTypeConstructorName](); // 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[3].toLowerCase() + getKey.substr(4); // Image data, choices, and type come in the form of objects / enums if (["image", "choices", "type", "alignment"].indexOf(propName) != -1) {return}; // Skip feedback-related keys if ("getFeedbackForIncorrect".equals(getKey) || "getFeedbackForCorrect".equals(getKey) || "getGeneralFeedback".equals(getKey)) {return}; var propValue = typedItem[getKey](); data[propName] = 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[boolKey](); data[propName] = 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[1].toUpperCase();}); } 


Was passiert im Code:

  • getFormMetadata- Funktion - Gibt JSON mit Formularmetadaten zurück
  • itemToObject- Funktion - konvertiert das form.item- Objekt mit den erforderlichen Feldern in JSON
  • sendEmail- Funktion - sendet eine JSON-Datei an die angegebene E-Mail im Text
  • Hauptfunktion - gibt den resultierenden JSON zurück
  • Die Variable form_url in der Hauptfunktion ist die Adresse unseres Google-Formulars

SCHRITT 2. Das Skript testen


Im Moment kann die Leistung des Skripts wie folgt überprüft werden:

  1. Erstellen Sie Ihr eigenes App-Skript-Projekt
  2. Kopieren Sie den Code hinein
  3. Anstelle von <YOUR_FORM_URL> ersetzen wir die Adresse unseres Formulars aus dem Formular docs.google.com/forms/d/FORM_IDENTIFICATOR/edit
  4. Kommentieren Sie den Aufruf von sendEmail in der Hauptfunktion aus
  5. Anstelle von <YOUR_EMAIL> ersetzen wir die E-Mail-Adresse, an die JSON gesendet werden soll
  6. Speichern Sie das Projekt
  7. Führen Sie die Hauptfunktion aus
  8. Wenn dies die erste Ausführung des Skripts ist, werden Sie vom System über die Notwendigkeit informiert, dem Skript die Berechtigung zum Senden von E-Mails von Ihrer Adresse aus zu erteilen. Hab keine Angst. Dies ist die Standardprozedur, die zum Testen des Skripts erforderlich ist. Gehe durch "Berechtigungen überprüfen" -> wähle dein Konto aus -> "Erweitert" -> "Gehe zu PROJECT_NAME-Projekt (unsicher)" -> "Zulassen"
  9. Warten auf das Skript zu arbeiten
  10. Schauen Sie in das Postfach und sehen Sie die JSON-Datei in Textform

Alles wäre in Ordnung, aber die weitere Verwendung der erhaltenen Daten erfordert manuelles Kopieren aus der Mail, Verarbeiten dieses Texts (z. B. in Python) und Speichern der resultierenden Datei. Es klingt nicht zu produktionsreif. Wir automatisieren den Start dieses Skripts und erhalten das Ergebnis über die Google Apps-Skript-API. Zunächst richten wir jedoch unser Google-Projekt entsprechend ein.

Achtung: Um zu verstehen, was gerade passiert, werde ich im Folgenden nur auf zwei Seiten verweisen. Es wird daher empfohlen, diese in benachbarten Registerkarten zu öffnen:

  1. Skript / Skriptbearbeitungsseite - „Seite 1“
  2. Google Cloud Platform-Seite - "Seite 2"

SCHRITT 3. Google Cloud Platform konfigurieren


Wir gehen zur Google Cloud Platform (Seite 2) und erstellen ein neues Projekt. Es ist erforderlich, ein neues Projekt zu erstellen, da der Standardstatus des Projekts "Standard" lautet und für unsere Zwecke "Standart" erforderlich ist. Weitere Details finden Sie hier (Punkt 3).

Wir kehren zu Seite 2 zurück, gehen zur Registerkarte "API and Services" und dann zu "OAuth Access Request Window". Stellen Sie den Benutzertyp auf "Extern".

Geben Sie im angezeigten Fenster den "Anwendungsnamen" ein.

Öffnen Sie die Homepage auf der Google Cloud-Plattform. Kopieren Sie aus dem Block "Projektinformationen" die Projektnummer.

Gehen Sie zu Seite 1. Öffnen Sie das zuvor erstellte Skript. Gehen Sie im geöffneten Skriptbearbeitungsfenster zu „Ressourcen“ -> „Cloud Platform-Projekt“. Geben Sie im Feld „Projekt ändern“ die zuvor kopierte Projektnummer ein. Dieses Skript ist nun dem erstellten Projekt zugeordnet.

SCHRITT 4. Python-REST-API


Es ist Zeit, das Skript mithilfe der REST-API zu automatisieren. Als Sprache wurde Python verwendet.

Anmeldung für Apps Script API


Der Code muss Zugriff auf das Projekt haben, daher ist die Anmeldung in der Apps Script-API die erste und sehr wichtige Prozedur. Öffnen Sie Seite 2 -> „API and Services“ -> „Credentials“ -> „Create Credentials“ -> „OAuth Client Identifier“ -> „Other Types“. Wir nennen unseren Bezeichner, gehen Sie zu ihm. Wählen Sie auf der Registerkarte "Anmeldeinformationen" die Option "JSON-Datei herunterladen". Dadurch wird die Schlüsseldatei für den Zugriff vom Code auf das Projekt in Google geladen. Wir legen diese Datei im Anmeldeinformationsordner ab.

Jetzt müssen Sie die Berechtigung erteilen, die API (in unserem Fall die Apps Script-API) als Teil dieses Projekts zu verwenden. Gehen Sie dazu zu "API und Dienste" -> "Bibliothek" -> geben Sie "Apps Script API" in die Suche ein und klicken Sie auf "Aktivieren".

Anwendungen, die mit Google interagieren, verfügen über eine Reihe von Berechtigungen, die der Nutzer beim Starten erteilen muss. Diese Kopie hängt von den Funktionen ab, die von einem bestimmten Skript verwendet werden. Sie finden sie auf Seite 1 im Skriptbearbeitungsfenster unter „Datei“ -> „Projekteigenschaften“ -> „Bereiche“. Diese Berechtigungen sollten für die zukünftige Verwendung im Code gespeichert werden.

In diesem Fall sieht die Login-Funktion folgendermaßen aus:

 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['credentials_path'] + config['token_file'] credentials_file = config['credentials_path'] + config['credentials_file'] 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['SCOPES']) 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 

Dieser Codeblock ist das Standardverfahren für den Einstieg in Google App Script.
Wir verwenden ein Authentifizierungstoken und erstellen bei der Anmeldung entweder ein neues Token oder verwenden ein vorhandenes.

Zur Vereinfachung wurde eine JSON-Konfigurationsdatei mit der folgenden Form erstellt:

 { "SCOPES": ["https://www.googleapis.com/auth/forms", "https://www.googleapis.com/auth/script.send_mail"], "credentials_path": "credentials/", "credentials_file": "google_test_project.json", "token_file": "token.pickle" } 

Wichtig: Das Token wird für die Authentifizierung mit einem bestimmten Berechtigungsumfang erstellt. Mit anderen Worten, wenn Sie den Berechtigungsumfang ändern, sollten Sie das Token löschen und bei der Anmeldung ein neues erstellen.

Skriptcode für Remote-Aktualisierung


Jetzt lernen wir, wie man den Skriptcode remote aktualisiert, diesen Code dann ausführt und das Ergebnis erhält. Zusätzlich zu dem Code, den wir im Google Editor ausführen, gibt es auch eine Manifestdatei , die die Startrechte, Bereitstellungseinstellungen usw. enthält. Weitere Informationen zu seiner Struktur finden Sie hier .
Um die von Google für Ihr Skript erstellte Standardmanifestdatei anzuzeigen, rufen Sie den Skripteditor unter "Ansicht" -> "Manifestdatei anzeigen" auf. Das Manifest wird in der Liste der Dateien angezeigt, die sich auf dieses Skript beziehen.

Die Rede über das Manifest war nicht ohne Grund: Für die Remote-Skriptaktualisierung muss der Code sowohl der Dateien (* .gs) als auch des Manifests (appscript.json) heruntergeladen werden.

Lesen Sie zunächst den Code der .gs-Datei, die Sie bereitstellen möchten:

  with open('export-google-form.gs', 'r') as f: sample_code = f.read() 

Kopieren Sie nun das automatisch generierte Manifest und ändern Sie es ein wenig für unsere Zwecke. Die Dokumentation beschreibt die Struktur der Manifestdatei ziemlich ausführlich, so dass ich auf diesen Punkt nicht näher eingehen werde. Damit das Skript funktioniert, müssen Sie dem Standardmanifest den Abschnitt "executionApi" hinzufügen, der für die Remoteausführung des Skripts über die API erforderlich ist. In diesem Abschnitt geben wir den Personenkreis an, der in der Lage ist, das Programm auszuführen. Ich habe den Start für alle erlaubt, die die Autorisierung bestanden haben, die der Kennung "ANYONE" entspricht:

 MANIFEST = ''' { "timeZone": "America/New_York", "exceptionLogging": "STACKDRIVER", "executionApi": { "access": "ANYONE" } } '''.strip() 

Der Hauptteil der Aktualisierungsanforderung sollte ein Array von Dateien mit der folgenden Struktur enthalten:

  • name : Name der Datei, die auf dem Server erstellt werden soll, ohne Erweiterung
  • Typ : Dateityp (JSON für Manifest, SERVER_JS für .gs)
  • Quelle : Dateicode

 request = { 'files': [{ 'name': 'hello', 'type': 'SERVER_JS', 'source': sample_code }, { 'name': 'appsscript', 'type': 'JSON', 'source': MANIFEST } ] } 

Schließlich muss die Aktualisierungsanforderung selbst den Hauptteil (oben beschriebene Anforderung) und die Skript-ID enthalten. Letzteres erhalten Sie, indem Sie im Skripteditor unter "Datei" -> "Projekteigenschaften" die "Skript-ID" kopieren:

 script_id = 'qwertyuiopQWERTYUIOPasdfghjkl123456789zxcvbnmASDFGHJKL54' 

Für das Serviceobjekt, das als Ergebnis der Anmeldung abgerufen wurde, erhalten wir das Feld projects () und rufen die Methode updateContent () auf. Anschließend rufen wir die Methode execute () für das empfangene HttpRequest-Objekt auf:

 service.projects().updateContent( body=request, scriptId=script_id ).execute() 

Derzeit führt das Ausführen des Codes jedoch zu einem Fehler:

 "error": { "code": 403, "message": "Request had insufficient authentication scopes.", "status": "PERMISSION_DENIED" } 

Wie Sie sehen, gibt es nicht genügend Berechtigungen für den Authentifizierungs-Fischadler, auf den wir bereits hingewiesen haben. Wir wenden uns der offiziellen Dokumentation der API zu, der updateContent- Methode, mit der wir das Skript remote aktualisiert haben. Die Dokumentation besagt, dass für die Verwendung dieser Methode der Zugriff auf script.projects aktiviert werden muss:

 https://www.googleapis.com/auth/script.projects 

Fügen Sie es unserer Konfigurationsdatei im Bereich SCOPES hinzu. Wie ich oben geschrieben habe, ist es beim Ändern des Fischadlers erforderlich, das automatisch generierte Token zu löschen.

Großartig! Im Moment haben wir gelernt, Google-Skript aus der Ferne zu aktualisieren. Es bleibt zu laufen und das Ergebnis der Ausführung zu erhalten.

Skript ausführen


Die Skriptstartanforderung enthält die Skript- ID und den Text mit der folgenden Struktur:

  • function : Name der Funktion, die ausgeführt werden soll
  • parameters : (optional) Eine Reihe von Parametern eines primitiven Typs (Zeichenfolge, Array ...), die an die Funktion übergeben werden
  • sessionState : (optional) ist nur für Android-Anwendungen erforderlich
  • devMode : (optional) True, wenn der Benutzer der Eigentümer des Skripts ist und dann die neueste Version gestartet wird, als diejenige, die mit der Apps Script-API bereitgestellt wurde. (standardmäßig - Falsch)

Um die URL des Google-Formulars im Skript nicht zusammenzufügen, übergeben wir form_url als Argument an die Hauptfunktion .

Achtung Beim Testen des Skripts hat die Hauptfunktion nichts akzeptiert. Daher ändern wir die ersten Codezeilen in der .gs-Datei wie folgt:

 function main(form_url) { var form = FormApp.openByUrl(form_url); ....... 

Da unsere Anwendung nicht für Android ist und wir die Eigentümer des Skripts sind, sieht der Text folgendermaßen aus:

 body = { "function": "main", "devMode": True, "parameters": form_url } 

Führen Sie das Skript aus und schreiben Sie das Ergebnis der Ausführung in die entsprechende Variable:

 resp = service.scripts().run(scriptId=script_id, body=body).execute() 

Speichern bzw. in eine Datei mit praktischer JSON-Formatierung:

 import json with open('habr_auto.json', 'w', encoding='utf-8') as f: json.dump(resp['response']['result'], f, ensure_ascii=False, indent=4) 

Achtung Aufgrund der Tatsache, dass die script.run () -Anforderung über den Socket auf das Ergebnis wartet und das Timeout um die Ausführungszeit überschritten wird, tritt ein Fehler des folgenden Typs auf:

 socket.timeout: The read operation timed out 

Um dieses Verhalten zu vermeiden, empfehle ich, zu Beginn des Programms ein Limit für die Open-Socket-Zeit festzulegen, das offensichtlich ausreicht, um auf die Ausführung des Skripts zu warten. In meinem Fall reichen 120 Sekunden:

 import socket socket.setdefaulttimeout(120) 

Voila! Eine praktische Pipeline zum Remote-Aktualisieren und Starten von Google-Skripten ist verfügbar. Der vollständige Code für den Start vom Terminal ist in meinem Github angegeben .

Außerdem werde ich den Code der Hauptfunktionen unten geben

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['credentials_path'] + config['token_file'] credentials_file = config['credentials_path'] + config['credentials_file'] 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['SCOPES']) 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': [{ 'name': 'hello', 'type': 'SERVER_JS', 'source': sample_code }, { 'name': 'appsscript', 'type': 'JSON', 'source': MANIFEST } ] } # 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[1] script_id = args[2] script_file_name = args[3] 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['response']['result'], 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[1] file_name = args[2] script_id = args[3] form_url = args[4] 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() 


Zu Beginn müssen Sie die JSON-Datei mit den Google-Zugriffsschlüsseln im Ordner mit den Anmeldeinformationen und die Konfigurations-JSON im selben Verzeichnis wie die Skripts ablegen.

Wenn wir dann das Skript aus der Ferne aktualisieren möchten, rufen Sie im Terminal Folgendes auf:

 python update_script.py <config_file_name> <script_id> <script_file_name> 

In diesem Fall:

  • config_file_name - Name der Konfigurations-JSON-Datei
  • script_id - Skript-ID
  • script_file_name - Der Name der .gs-Datei, die auf Google hochgeladen wird

Rufen Sie zum Ausführen des Skripts Folgendes auf:

 python export_form.py <config_file_name> <result_file_name> <script_id> <google_form_url> 

In diesem Fall:

  • config_file_name - Name der Konfigurations-JSON-Datei
  • result_file_name - Der Name der JSON-Datei, in die das Formular entladen wird
  • script_id - Skript-ID
  • google_form_url - Google-Formular-URL

Vielen Dank für Ihre Aufmerksamkeit und das Warten auf Ihre Vorschläge und Kommentare :)

Source: https://habr.com/ru/post/de485898/


All Articles