Exporte Google Forms + descargue Google Script a través de REST API (Python)



Teníamos dos formularios de Google, 75 preguntas en cada uno, 5 usuarios comerciales que editaron activamente estos formularios, y también un script de Google que exportaba el formulario a JSON. No es que sea difícil ejecutarlo cada vez con las manos, pero una vez que comience a automatizar su trabajo, continúe en este pasatiempo hasta el final.

En la documentación oficial, el diablo se romperá la pierna, así que debajo del gato veremos más de cerca la descarga remota y el lanzamiento de Google Apps Script a través de la API REST usando Python.

Introduccion


En Doctor Near, estamos desarrollando una plataforma para bots de chat, en la que los formularios de Google se utilizan para describir escenarios. En consecuencia, quiero obtener un JSON de los formularios con solo hacer clic en un botón que contiene nodos (puntos de formulario) y metadatos (transiciones entre nodos, tipos de nodos, su nombre). Parece que el deseo es simple, pero Google no es compatible con esta funcionalidad y tiene que armar este "exportador" con sus propias manos. Considere los pasos para crearlo.

PASO 1. Script de Google Apps


Google ha proporcionado la capacidad de interactuar con sus servicios (Hojas de cálculo, Documentos, Formularios) a través de Google Apps Script: scripts escritos en google script (.gs). Este artículo no proporciona un análisis del lenguaje de script de google, por lo que daré un ejemplo de un script listo que crea JSON a partir de un formulario de Google existente. El código de usuario Steven Schmatz fue tomado del github como base, por lo que le expreso mi gratitud.

Código 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();}); } 


Lo que sucede en el código:

  • Función getFormMetadata : devuelve JSON con metadatos de formulario
  • Función itemToObject : convierte el objeto form.item a JSON con los campos obligatorios
  • Función sendEmail : envía un archivo JSON al correo especificado en texto
  • función principal : devuelve el JSON resultante
  • la variable form_url en la función principal es la dirección de nuestro formulario de google

PASO 2. Probar el guión


Por el momento, el rendimiento del script se puede verificar de la siguiente manera:

  1. crea tu propio proyecto de script de aplicación
  2. copia el código en él
  3. en lugar de <YOUR_FORM_URL>, sustituimos la dirección de nuestro formulario del formulario docs.google.com/forms/d/FORM_IDENTIFICATOR/edit
  4. descomentar la llamada a sendEmail en la función principal
  5. en lugar de <YOUR_EMAIL> sustituimos la dirección de correo electrónico a la que queremos recibir JSON
  6. guardar el proyecto
  7. ejecuta la función principal
  8. Si esta es la primera ejecución de la secuencia de comandos, el sistema le informará sobre la necesidad de dar permiso a la secuencia de comandos para enviar correos electrónicos desde su dirección. No tengas miedo. Este es el procedimiento estándar requerido para probar el script. Vaya a “Revisar permisos” -> seleccione su cuenta -> “Avanzado” -> “Ir al proyecto PROJECT_NAME (inseguro)” -> “Permitir”
  9. esperando que el script funcione
  10. mire en el buzón y vea el archivo JSON en forma de texto

Todo estaría bien, pero el uso adicional de los datos obtenidos implica la copia manual del correo, el procesamiento de este texto (en Python, por ejemplo) y el almacenamiento del archivo resultante. No suena demasiado listo para la producción. Automatizamos el lanzamiento de este script y obtenemos su resultado a través de la API de Google Apps Script, pero primero configuramos nuestro proyecto de Google en consecuencia.

Atención: para la conveniencia de comprender lo que está sucediendo, a continuación me referiré solo a dos páginas, por lo tanto, se recomienda abrirlas en pestañas adyacentes:

  1. Guión / Página de edición de guiones - “Página 1”
  2. Página de Google Cloud Platform - "Página 2"

PASO 3. Configure Google Cloud Platform


Vamos a Google Cloud Platform (página 2), creamos un nuevo proyecto. Es necesario crear un nuevo proyecto, porque el estado predeterminado del proyecto es Predeterminado, y Standart es necesario para nuestros propósitos. Más detalles se pueden encontrar aquí (punto 3).

Regresamos a la página 2, vaya a la pestaña "API y servicios", luego a "Ventana de solicitud de acceso de OAuth". Establezca el Tipo de usuario en "Externo".

En la ventana que aparece, complete el "Nombre de la aplicación".

Abra la página de inicio en Google Cloud Platform. Desde el bloque "Información del proyecto" copie el número del proyecto.

Vaya a la página 1. Abra el script creado anteriormente. En la ventana de edición de script abierta, vaya a "Recursos" -> "Proyecto de plataforma en la nube". En el campo "Cambiar proyecto", ingrese el número de proyecto copiado previamente. Ahora este script está asociado con el proyecto creado.

PASO 4. API REST de Python


Es hora de automatizar el script usando la API REST . Python fue usado como el lenguaje.

Inicio de sesión de API de Apps Script


El código debe tener acceso al proyecto, por lo que el primer y muy importante procedimiento es el inicio de sesión en la API de Apps Script. Abra la página 2 -> “API y servicios” -> “Credenciales” -> “Crear credenciales” -> “Identificador de cliente OAuth” -> “Otros tipos”. Llamamos a nuestro identificador, ve a él. En la pestaña "Credenciales", seleccione "Descargar archivo JSON". Esto cargará el archivo de clave para acceder desde el código al proyecto en Google. Colocamos este archivo en la carpeta de credenciales.

Ahora debe dar permiso para usar la API (en nuestro caso, la API de script de aplicaciones) como parte de este proyecto. Para hacer esto, vaya a "API y servicios" -> "Biblioteca" -> escriba "API de script de aplicaciones" en la búsqueda y haga clic en "Habilitar".

Las aplicaciones que interactúan con Google tienen muchos permisos que el usuario debe otorgar al iniciarlo. Esta copia depende de las funciones utilizadas por un script en particular y puede encontrarla yendo a la página 1 en la ventana de edición del script en "Archivo" -> "Propiedades del proyecto" -> "Ámbitos". Estos permisos deben conservarse para futuras referencias en el código.

En este caso, la función de inicio de sesión se verá así:

 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 

Este bloque de código es el procedimiento estándar para comenzar con Google App Script.
Usamos un token de autenticación y, al iniciar sesión, creamos un token nuevo o usamos uno existente.

Por conveniencia, se creó un archivo de configuración JSON, que tiene la siguiente forma:

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

Importante: el token se crea para la autenticación con un alcance específico de permisos. En otras palabras, al cambiar el alcance de los permisos, debe eliminar el token y crear uno nuevo al iniciar sesión.

Código de script de actualización remota


Ahora aprenderemos cómo actualizar remotamente el código del script, luego ejecutar este código y obtener el resultado. De hecho, además del código que ejecutamos en el editor de Google, también hay un archivo de manifiesto que contiene los derechos de inicio, la configuración de implementación, etc. Más información sobre su estructura se puede encontrar aquí .
Para ver el archivo de manifiesto predeterminado creado por Google para su script, vaya al editor de script en "Ver" -> "Mostrar archivo de manifiesto". El manifiesto aparecerá en la lista de archivos relacionados con este script.

El discurso sobre el manifiesto no fue sin razón: la actualización remota de scripts requiere la descarga del código de ambos archivos (* .gs) y manifiesto (appscript.json).

Primero, lea el código del archivo .gs que queremos implementar:

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

Ahora copie el manifiesto generado automáticamente y modifíquelo un poco para nuestros propósitos. La documentación describe completamente la estructura del archivo de manifiesto, por lo que no me detendré en este punto. Para que el script funcione, debe agregar la sección "executeApi" al manifiesto predeterminado, que se requiere para ejecutar el script de forma remota a través de la API. En esta sección indicamos el círculo de personas que tienen la capacidad de ejecutarlo. Permití el lanzamiento para todos los que han aprobado la autorización, que corresponde al identificador "CUALQUIERA":

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

El cuerpo de la solicitud de actualización debe contener una matriz de archivos con la siguiente estructura:

  • nombre : nombre del archivo que se creará en el servidor, sin extensión
  • tipo : tipo de archivo (JSON para manifiesto, SERVER_JS para .gs)
  • fuente : código de archivo

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

Finalmente, la solicitud de actualización en sí misma debe contener el cuerpo (solicitud descrita anteriormente) y el ID del script. Este último puede obtenerse yendo al "Archivo" -> "Propiedades del proyecto" en el editor de script y copiando el "ID del script":

 script_id = 'qwertyuiopQWERTYUIOPasdfghjkl123456789zxcvbnmASDFGHJKL54' 

Para el objeto de servicio obtenido como resultado del inicio de sesión, obtenemos el campo projects () y llamamos al método updateContent (), después de lo cual llamamos al método execute () para el objeto HttpRequest recibido:

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

Sin embargo, por ahora, ejecutar el código dará como resultado un error:

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

Como puede ver, no hay suficientes permisos en el águila pescadora de autenticación, que indicamos anteriormente. Pasamos a la documentación oficial en la API, a saber, el método updateContent , que utilizamos para actualizar el script de forma remota. La documentación dice que usar este método requiere habilitar el acceso a script.projects:

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

Agréguelo a nuestro archivo de configuración en la sección ALCANCE. Como escribí anteriormente, al cambiar el águila pescadora, es necesario eliminar el token generado automáticamente.

Genial Por el momento, hemos aprendido a actualizar remotamente el script de Google. Queda por ejecutarlo y obtener el resultado de la ejecución.

Script ejecutado


La solicitud de inicio del script contiene scriptID y cuerpo con la siguiente estructura:

  • función : nombre de la función que queremos ejecutar
  • parámetros : (opcional) un conjunto de parámetros de un tipo primitivo (cadena, matriz ...) pasados ​​a la función
  • sessionState : (opcional) se requiere solo para aplicaciones de Android
  • devMode : (opcional) Verdadero si el usuario es el propietario del script y luego se lanzará la última versión que la que se implementó utilizando la API de Script de Apps. (por defecto - Falso)

Para no unir la URL del formulario de Google en el script, pasaremos form_url a la función principal como argumento.

Atencion Cuando probamos el script, la función principal no aceptaba nada, por lo que cambiaremos las primeras líneas de código en el archivo .gs de la siguiente manera:

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

Como nuestra aplicación no es para Android y somos los propietarios del script, el cuerpo se verá así:

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

Ejecute el script y escriba el resultado de la ejecución en la variable resp:

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

Guarde resp en un archivo con el formato JSON conveniente:

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

Atencion Debido al hecho de que la solicitud script.run () está esperando el resultado a través del socket, cuando el tiempo de ejecución excede el tiempo de espera, se producirá un error del siguiente tipo:

 socket.timeout: The read operation timed out 

Para evitar este comportamiento, recomiendo que al comienzo del programa establezca un límite en el tiempo de socket abierto, obviamente suficiente para que espere a que el script termine de ejecutarse. En mi caso, 120 segundos son suficientes:

 import socket socket.setdefaulttimeout(120) 

Voila! Ya está lista una tubería conveniente para la actualización remota y el lanzamiento de scripts de Google. El código completo adaptado para el lanzamiento desde la terminal se da en mi github .

Además, daré el código de las funciones principales a continuación

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


Para comenzar, debe colocar el archivo JSON con las claves de acceso de Google en la carpeta de credenciales y la configuración JSON en el mismo directorio que los scripts.

Luego, si queremos actualizar el script de forma remota, en la llamada del terminal:

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

En este caso:

  • config_file_name : nombre del archivo JSON de configuración
  • script_id - ID del script
  • script_file_name : el nombre del archivo .gs que se cargará en google

Para ejecutar el script, llame a:

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

En este caso:

  • config_file_name : nombre del archivo JSON de configuración
  • nombre_archivo_resultado - el nombre del archivo JSON en el que se descargará el formulario
  • script_id - ID del script
  • google_form_url - url de formulario de google

Gracias por su atención, esperando sus sugerencias y comentarios :)

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


All Articles