Exportez Google Forms + téléchargez Google Script via l'API REST (Python)



Nous avions deux formulaires Google, 75 questions chacun, 5 utilisateurs professionnels qui ont activement édité ces formulaires, ainsi qu'un script Google exportant le formulaire vers JSON. Non pas qu'il serait difficile de l'exécuter à chaque fois avec vos mains, mais une fois que vous commencez à automatiser votre travail, passez à ce passe-temps jusqu'à la fin.

Dans la documentation officielle, le diable se cassera la jambe, donc sous le chat, nous allons regarder de plus près le téléchargement et le lancement à distance de Google Apps Script via l'API REST en utilisant Python.

Présentation


Dans Doctor Near, nous développons une plateforme pour les robots de chat, dans laquelle les formulaires Google sont utilisés pour décrire des scénarios. En conséquence, je veux obtenir un JSON des formulaires au clic d'un bouton qui contient des nœuds (points de formulaire) et des métadonnées pour eux (transitions entre les nœuds, types de nœuds, leur nom). Il semblerait que le désir soit simple, mais Google ne prend pas en charge cette fonctionnalité et vous devez assembler cet "exportateur" de vos propres mains. Considérez les étapes pour le créer.

ÉTAPE 1. Script Google Apps


Google a fourni la possibilité d'interagir avec ses services (Sheets, Docs, Forms) via Google Apps Script - des scripts écrits en google script (.gs). Cet article ne fournit pas d'analyse du langage de script Google, je vais donc donner un exemple de script terminé qui crée JSON à partir d'un formulaire Google existant. Le code utilisateur Steven Schmatz a été pris comme base sur le github, pour lequel je lui exprime ma gratitude.

Code de script
// 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();}); } 


Que se passe-t-il dans le code:

  • Fonction getFormMetadata - renvoie JSON avec les métadonnées du formulaire
  • Fonction itemToObject - convertit l'objet form.item en JSON avec les champs obligatoires
  • Fonction sendEmail - envoie un fichier JSON au courrier spécifié en texte
  • fonction principale - retourne le JSON résultant
  • la variable form_url dans la fonction principale est l'adresse de notre formulaire google

ÉTAPE 2. Test du script


Pour le moment, les performances du script peuvent être vérifiées comme suit:

  1. créez votre propre projet de script d'application
  2. copier le code dedans
  3. au lieu de <YOUR_FORM_URL>, nous substituons l'adresse de notre formulaire au formulaire docs.google.com/forms/d/FORM_IDENTIFICATOR/edit
  4. décommenter l'appel à sendEmail dans la fonction principale
  5. au lieu de <YOUR_EMAIL>, nous substituons l'adresse e-mail à laquelle nous voulons recevoir JSON
  6. enregistrer le projet
  7. exécuter la fonction principale
  8. s'il s'agit de la première exécution du script, le système vous informera de la nécessité de donner au script l'autorisation d'envoyer des e-mails à partir de votre adresse. N'ayez pas peur. Il s'agit de la procédure standard requise pour tester le script. Passez par "Examiner les autorisations" -> sélectionnez votre compte -> "Avancé" -> "Accédez au projet PROJECT_NAME (dangereux)" -> "Autoriser"
  9. en attendant que le script fonctionne
  10. regardez dans la boîte aux lettres et voyez le fichier JSON sous forme de texte

Tout irait bien, mais une utilisation ultérieure des données obtenues implique une copie manuelle à partir du courrier, le traitement de ce texte (en python, par exemple) et l'enregistrement du fichier résultant. Cela ne semble pas trop prêt pour la production. Nous automatisons le lancement de ce script et obtenons son résultat via l'API de script Google Apps, mais nous configurons d'abord notre projet Google en conséquence.

Attention: Pour la commodité de comprendre ce qui se passe, ci-dessous je ne ferai référence qu'à deux pages, il est donc recommandé de les ouvrir dans les onglets adjacents:

  1. Page Script / Édition de script - «Page 1»
  2. Page Google Cloud Platform - «Page 2»

ÉTAPE 3. Configurer Google Cloud Platform


Nous allons sur Google Cloud Platform (page 2), créons un nouveau projet. Il est nécessaire de créer un nouveau projet, car par défaut, l'état du projet est Par défaut et Standart est requis pour nos besoins. Plus de détails peuvent être trouvés ici (point 3).

Nous revenons à la page 2, allez dans l'onglet "API et services", puis "Fenêtre de demande d'accès OAuth". Définissez le type d'utilisateur sur "Externe".

Dans la fenêtre qui apparaît, remplissez le "Nom de l'application".

Ouvrez la page d'accueil sur Google Cloud Platform. Dans le bloc "Informations sur le projet", copiez le numéro de projet.

Allez à la page 1. Ouvrez le script créé précédemment. Dans la fenêtre d'édition de script ouverte, allez dans «Ressources» -> «Projet Cloud Platform». Dans le champ «Modifier le projet», entrez le numéro de projet précédemment copié. Maintenant, ce script est associé au projet créé.

ÉTAPE 4. API REST Python


Il est temps d'automatiser le script à l'aide de l' API REST . Python a été utilisé comme langage.

Connexion à l'API Apps Script


Le code doit avoir accès au projet, donc la première et très importante procédure est la connexion dans l'API Apps Script. Ouvrez la page 2 -> «API et services» -> «Credentials» -> «Create Credentials» -> «OAuth Client Identifier» -> «Other Types». Nous appelons notre identifiant, allez-y. Étant dans l'onglet "Informations d'identification", sélectionnez "Télécharger le fichier JSON". Cela chargera le fichier de clé pour l'accès du code au projet dans Google. Nous plaçons ce fichier dans le dossier des informations d'identification.

Vous devez maintenant autoriser l'utilisation de l'API (dans notre cas, l'API Apps Script) dans le cadre de ce projet. Pour ce faire, allez dans «API et services» -> «Bibliothèque» -> tapez «Apps Script API» dans la recherche et cliquez sur «Activer».

Les applications qui interagissent avec Google ont un tas d'autorisations que l'utilisateur doit donner lors de son lancement. Cette copie dépend des fonctions utilisées par un script particulier et vous pouvez le découvrir en allant à la page 1 dans la fenêtre d'édition de script dans "Fichier" -> "Propriétés du projet" -> "Étendues". Ces autorisations doivent être conservées pour référence future dans le code.

Dans ce cas, la fonction de connexion ressemblera à ceci:

 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 

Ce bloc de code est la procédure standard pour démarrer avec Google App Script.
Nous utilisons un jeton d'authentification et, en nous connectant, nous créons un nouveau jeton ou nous en utilisons un existant.

Pour plus de commodité, un fichier de configuration JSON a été créé, qui a la forme suivante:

 { "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" } 

Important: le jeton est créé pour l'authentification avec une étendue d'autorisations spécifique. En d'autres termes, lorsque vous modifiez la portée des autorisations, vous devez supprimer le jeton et en créer un nouveau lors de la connexion.

Code de script de mise à jour à distance


Nous allons maintenant apprendre à mettre à jour à distance le code du script, puis à exécuter ce code et à obtenir le résultat. En fait, en plus du code que nous exécutons dans l'éditeur Google, il existe également un fichier manifeste qui contient les droits de lancement, les paramètres de déploiement, etc. Vous trouverez plus d'informations sur sa structure ici .
Pour consulter le fichier manifeste par défaut créé par Google pour votre script, accédez à l'éditeur de script dans "Affichage" -> "Afficher le fichier manifeste". Le manifeste apparaîtra dans la liste des fichiers liés à ce script.

Le discours sur le manifeste n'était pas sans raison: la mise à jour du script à distance nécessite le téléchargement du code des fichiers (* .gs) et du manifeste (appscript.json).

Tout d'abord, lisez le code du fichier .gs que nous voulons déployer:

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

Copiez maintenant le manifeste généré automatiquement et modifiez-le un peu pour nos besoins. La documentation décrit assez complètement la structure du fichier manifeste, donc je ne m'attarderai pas sur ce point. Pour que le script fonctionne, vous devez ajouter la section "executionApi" au manifeste par défaut, qui est requis pour exécuter le script à distance via l'API. Dans cette section, nous indiquons le cercle de personnes qui ont la capacité de l'exécuter. J'ai autorisé le lancement pour tous ceux qui ont passé l'autorisation, ce qui correspond à l'identifiant "N'IMPORTE QUI":

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

Le corps de la demande de mise à jour doit contenir un tableau de fichiers avec la structure suivante:

  • nom : nom du fichier à créer sur le serveur, sans extension
  • type : type de fichier (JSON pour manifeste, SERVER_JS pour .gs)
  • source : code de fichier

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

Enfin, la demande de mise à jour elle-même doit contenir le corps (demande décrite ci-dessus) et l'ID de script. Ce dernier peut être obtenu en allant dans le "Fichier" -> "Propriétés du projet" dans l'éditeur de script et en copiant le "Script ID":

 script_id = 'qwertyuiopQWERTYUIOPasdfghjkl123456789zxcvbnmASDFGHJKL54' 

Pour l'objet de service obtenu à la suite de la connexion, nous obtenons le champ projects () et appelons la méthode updateContent (), après quoi nous appelons la méthode execute () pour l'objet HttpRequest reçu:

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

Cependant, pour l'instant, l'exécution du code entraînera une erreur:

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

Comme vous pouvez le voir, il n'y a pas suffisamment d'autorisations dans le balbuzard d'authentification, que nous avons indiqué plus tôt. Nous nous tournons vers la documentation officielle de l'API, à savoir la méthode updateContent , que nous avons utilisée pour mettre à jour le script à distance. La documentation indique que l'utilisation de cette méthode nécessite l'activation de l'accès à script.projects:

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

Ajoutez-le à notre fichier de configuration dans la section SCOPES. Comme je l'ai écrit ci-dessus, lors du changement du balbuzard pêcheur, il est nécessaire de supprimer le jeton généré automatiquement.

Super! Pour le moment, nous avons appris à mettre à jour à distance le script Google. Il reste à l'exécuter et à obtenir le résultat de l'exécution.

Exécution de script


La demande de lancement de script contient le scriptID et le corps avec la structure suivante:

  • fonction : nom de la fonction que nous voulons exécuter
  • paramètres : (facultatif) un ensemble de paramètres de type primitif (chaîne, tableau ...) transmis à la fonction
  • sessionState : (facultatif) n'est requis que pour les applications Android
  • devMode : (facultatif) True si l'utilisateur est le propriétaire du script et que la dernière version sera lancée par rapport à celle qui a été déployée à l'aide de l'API Apps Script. (par défaut - Faux)

Afin de ne pas assembler l'URL du formulaire Google dans le script, nous passerons form_url à la fonction principale comme argument.

Attention Lorsque nous avons testé le script, la fonction principale n’acceptait rien. Nous allons donc modifier les premières lignes de code du fichier .gs comme suit:

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

Puisque notre application n'est pas pour Android et que nous sommes les propriétaires du script, le corps ressemblera à ceci:

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

Exécutez le script et écrivez le résultat de l'exécution dans la variable resp:

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

Enregistrez resp dans un fichier avec un formatage JSON pratique:

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

Attention Du fait que la requête script.run () attend le résultat via le socket, lorsque le délai est dépassé par le temps d'exécution, une erreur du type suivant se produit:

 socket.timeout: The read operation timed out 

Pour éviter ce problème, je recommande qu'au début du programme définissez une limite sur le temps de socket ouvert, ce qui est évidemment suffisant pour qu'il attende la fin de l'exécution du script. Dans mon cas, 120 secondes suffisent:

 import socket socket.setdefaulttimeout(120) 

Voila! Un pipeline pratique pour la mise à jour et le lancement à distance des scripts Google est prêt. Le code complet adapté au lancement depuis le terminal est donné dans mon github .

Aussi, je vais donner le code des fonctions principales ci-dessous

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() 


Pour commencer, vous devez placer le fichier JSON avec les clés d'accès Google dans le dossier des informations d'identification et la configuration JSON dans le même répertoire que les scripts.

Ensuite, si nous voulons mettre à jour le script à distance, alors dans l'appel du terminal:

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

Dans ce cas:

  • config_file_name - nom du fichier de configuration JSON
  • script_id - ID de script
  • script_file_name - le nom du fichier .gs qui sera téléchargé sur google

Pour exécuter le script, appelez:

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

Dans ce cas:

  • config_file_name - nom du fichier de configuration JSON
  • result_file_name - le nom du fichier JSON dans lequel le formulaire sera déchargé
  • script_id - ID de script
  • google_form_url - URL du formulaire Google

Merci de votre attention, en attendant vos suggestions et commentaires :)

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


All Articles