Exporte o Google Forms + faça o download do Google Script via API REST (Python)



Tínhamos dois formulários do Google, 75 perguntas em cada um, 5 usuários corporativos que editaram ativamente esses formulários e também um script do Google exportando o formulário para JSON. Não que seja difícil executá-lo todas as vezes com as mãos, mas depois que você começar a automatizar seu trabalho, siga esse hobby até o fim.

Na documentação oficial, o diabo quebrará a perna; portanto, sob o gato, veremos mais de perto o download remoto e o lançamento do Script do Google Apps por meio da API REST usando Python.

1. Introdução


No Doctor Near, estamos desenvolvendo uma plataforma para bots de bate-papo, na qual os formulários do Google são usados ​​para descrever cenários. Dessa forma, quero obter um JSON dos formulários com o clique de um botão que contém nós (pontos do formulário) e metadados para eles (transições entre nós, tipos de nós, seu nome). Parece que o desejo é simples, mas o Google não suporta essa funcionalidade e você precisa montar esse "exportador" com suas próprias mãos. Considere as etapas para criá-lo.

PASSO 1. Script do Google Apps


O Google forneceu a capacidade de interagir com seus serviços (Planilhas, Documentos, Formulários) por meio do Script do Google Apps - scripts escritos em google script (.gs). Este artigo não fornece uma análise da linguagem de script do Google, portanto, darei um exemplo de um script pronto que cria JSON a partir de um formulário existente do Google. O código de usuário Steven Schmatz foi retirado do github como base, pelo qual eu expresso minha gratidão a ele.

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


O que acontece no código:

  • Função getFormMetadata - retorna JSON com metadados do formulário
  • função itemToObject - converte o objeto form.item em JSON com os campos obrigatórios
  • função sendEmail - envia um arquivo JSON para o correio especificado em texto
  • função principal - retorna o JSON resultante
  • a variável form_url na função principal é o endereço do nosso formulário do Google

PASSO 2. Testando o script


No momento, o desempenho do script pode ser verificado da seguinte forma:

  1. crie seu próprio projeto de script de aplicativo
  2. copie o código nele
  3. em vez de <YOUR_FORM_URL>, substituímos o endereço do nosso formulário pelo formulário docs.google.com/forms/d/FORM_IDENTIFICATOR/edit
  4. descomente a chamada para sendEmail na função principal
  5. em vez de <YOUR_EMAIL>, substituímos o endereço de email para o qual queremos receber JSON
  6. salve o projeto
  7. execute a função principal
  8. se esta for a primeira execução do script, o sistema informará sobre a necessidade de conceder permissão ao script para enviar e-mail a partir do seu endereço. Não tenha medo. Este é o procedimento padrão necessário para testar o script. Acesse "Permissões de revisão" -> selecione sua conta -> "Avançado" -> "Vá para o projeto PROJECT_NAME (não seguro)" -> "Permitir"
  9. esperando o script dar certo
  10. procure na caixa de correio e veja o arquivo JSON em forma de texto

Tudo ficaria bem, mas o uso adicional dos dados obtidos envolve cópia manual do correio, processamento deste texto (em python, por exemplo) e salvamento do arquivo resultante. Não parece muito pronto para produção. Automatizamos o lançamento desse script e obtemos seu resultado por meio da API de scripts do Google Apps, mas primeiro configuramos nosso projeto do Google de acordo.

Atenção: Para a conveniência de entender o que está acontecendo, abaixo vou me referir apenas a duas páginas, portanto, é recomendável abri-las nas guias adjacentes:

  1. Página Script / Edição de Script - “Página 1”
  2. Página do Google Cloud Platform - "Página 2"

PASSO 3. Configure o Google Cloud Platform


Vamos para o Google Cloud Platform (página 2), criamos um novo projeto. É necessário criar um novo projeto, porque o status padrão do projeto é Padrão e o Standart é necessário para nossos propósitos. Mais detalhes podem ser encontrados aqui (ponto 3).

Retornamos à página 2, acessamos a guia "API e serviços" e, em seguida, "Janela OAuth Access Request". Defina o Tipo de usuário como "Externo".

Na janela exibida, preencha o "Nome do aplicativo".

Abra a página inicial no Google Cloud Platform. No bloco "Informações do projeto", copie o número do projeto.

Vá para a página 1. Abra o script criado anteriormente. Na janela de edição de script aberta, vá para "Recursos" -> "Projeto Cloud Platform". No campo "Alterar projeto", insira o número do projeto copiado anteriormente. Agora este script está associado ao projeto criado.

PASSO 4. API REST do Python


É hora de automatizar o script usando a API REST . Python foi usado como a linguagem.

Logon na API de scripts do Google Apps


O código deve ter acesso ao projeto, portanto, o primeiro e muito importante procedimento é o login na API de scripts do Google Apps. Abra a página 2 -> "API e serviços" -> "Credenciais" -> "Criar credenciais" -> "OAuth Client Identifier" -> "Outros tipos". Chamamos o nosso identificador, vá para ele. Sendo na guia "Credenciais", selecione "Baixar arquivo JSON". Isso carregará o arquivo de chave para acesso do código ao projeto no Google. Colocamos esse arquivo na pasta de credenciais.

Agora você precisa dar permissão para usar a API (no nosso caso, a API de script de aplicativos) como parte deste projeto. Para fazer isso, vá para "API e serviços" -> "Biblioteca" -> digite "API de script de aplicativos" na pesquisa e clique em "Ativar".

Os aplicativos que interagem com o Google têm várias permissões que o usuário deve conceder ao iniciá-lo. Essa cópia depende das funções usadas por um script específico e você pode descobrir isso indo para a página 1 na janela de edição de script em “Arquivo” -> “Propriedades do projeto” -> “Escopos”. Essas permissões devem ser mantidas para referência futura no código.

Nesse caso, a função de login ficará assim:

 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 

Esse bloco de código é o procedimento padrão para começar a usar o Google App Script.
Usamos um token de autenticação e, ao fazer login, crie um novo token ou use um já existente.

Por conveniência, foi criado um arquivo de configuração JSON, com o seguinte formato:

 { "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: o token é criado para autenticação com um escopo específico de permissões. Em outras palavras, ao alterar o escopo das permissões, você deve excluir o token e criar um novo no logon.

Código de script de atualização remota


Agora vamos aprender como atualizar remotamente o código do script, executar esse código e obter o resultado. De fato, além do código que executamos no editor do Google, também há um arquivo de manifesto que contém os direitos de inicialização, configurações de implantação etc. Mais informações sobre sua estrutura podem ser encontradas aqui .
Para ver o arquivo de manifesto padrão criado pelo Google para o seu script, acesse o editor de scripts em "Visualizar" -> "Mostrar arquivo de manifesto". O manifesto aparecerá na lista de arquivos relacionados a este script.

Discurso sobre o manifesto não foi sem motivo: a atualização remota de scripts requer o download do código dos arquivos (* .gs) e do manifesto (appscript.json).

Primeiro, leia o código do arquivo .gs que queremos implantar:

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

Agora copie o manifesto gerado automaticamente e modifique-o um pouco para nossos propósitos. A documentação descreve de maneira bastante abrangente a estrutura do arquivo de manifesto, portanto não vou me debruçar sobre esse ponto. Para que o script funcione, você precisa adicionar a seção "ExecutionApi" ao manifesto padrão, necessário para executar remotamente o script por meio da API. Nesta seção, indicamos o círculo de pessoas que têm a capacidade de executá-lo. Eu permiti o lançamento para todos que passaram pela autorização, o que corresponde ao identificador "ANYONE":

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

O corpo da solicitação de atualização deve conter uma matriz de arquivos com a seguinte estrutura:

  • nome : nome do arquivo a ser criado no servidor, sem extensão
  • tipo : tipo de arquivo (JSON para manifesto, SERVER_JS para .gs)
  • fonte : código do arquivo

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

Por fim, a própria solicitação de atualização deve conter o corpo (solicitação descrita acima) e o ID do script. O último pode ser obtido acessando "Arquivo" -> "Propriedades do projeto" no editor de script e copiando o "ID do script":

 script_id = 'qwertyuiopQWERTYUIOPasdfghjkl123456789zxcvbnmASDFGHJKL54' 

Para o objeto de serviço obtido como resultado do login, obtemos o campo projects () e chamamos o método updateContent (), após o qual chamamos o método execute () para o objeto HttpRequest recebido:

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

No entanto, por enquanto, a execução do código resultará em um erro:

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

Como você pode ver, não há permissões suficientes no osprey de autenticação, conforme indicado anteriormente. Voltamos à documentação oficial da API, a saber, o método updateContent , que usamos para atualizar remotamente o script. A documentação diz que o uso desse método requer o acesso ao script.projects:

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

Adicione-o ao nosso arquivo de configuração na seção SCOPES. Como escrevi acima, ao alterar o osprey, você precisa excluir o token gerado automaticamente.

Ótimo! No momento, aprendemos a atualizar remotamente o script do Google. Resta executá-lo e obter o resultado da execução.

Execução de script


A solicitação de ativação de script contém scriptID e corpo com a seguinte estrutura:

  • function : nome da função que queremos executar
  • parameters : (opcional) um conjunto de parâmetros de um tipo primitivo (string, array ...) transmitidos para a função
  • sessionState : (opcional) é necessário apenas para aplicativos Android
  • devMode : (opcional) Verdadeiro se o usuário for o proprietário do script e, em seguida, a versão mais recente será lançada que a que foi implantada usando a API de scripts do Google Apps. (por padrão - Falso)

Para não costurar o URL do formulário do Google no script, passaremos form_url para a função principal como argumento.

Atenção Quando testamos o script, a função principal não aceitou nada; portanto, alteraremos as primeiras linhas de código no arquivo .gs da seguinte maneira:

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

Como nosso aplicativo não é para Android e somos os proprietários do script, o corpo ficará assim:

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

Execute o script e escreva o resultado da execução na variável resp:

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

Salve resp em um arquivo com a formatação 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) 

Atenção Devido ao fato de a solicitação script.run () aguardar o resultado pelo soquete, quando o tempo limite for excedido pelo tempo de execução, ocorrerá um erro do seguinte tipo:

 socket.timeout: The read operation timed out 

Para evitar esse comportamento, recomendo que, no início do programa, defina um limite no tempo de soquete aberto, obviamente suficiente para aguardar a conclusão da execução do script. No meu caso, 120 segundos são suficientes:

 import socket socket.setdefaulttimeout(120) 

Voila! Um pipeline conveniente para atualização remota e lançamento de scripts do Google está pronto. O código completo adaptado para o lançamento a partir do terminal é fornecido no meu github .

Além disso, darei o código das principais funções abaixo

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 começar, você precisa colocar o arquivo JSON com as chaves de acesso do Google na pasta credenciais e a configuração JSON no mesmo diretório que os scripts.

Então, se queremos atualizar remotamente o script, na chamada de terminal:

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

Nesse caso:

  • config_file_name - nome do arquivo JSON de configuração
  • script_id - ID do script
  • script_file_name - o nome do arquivo .gs que será carregado no google

Para executar o script, chame:

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

Nesse caso:

  • config_file_name - nome do arquivo JSON de configuração
  • result_file_name - o nome do arquivo JSON no qual o formulário será descarregado
  • script_id - ID do script
  • google_form_url - URL do formulário do Google

Obrigado por sua atenção, aguardando suas sugestões e comentários :)

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


All Articles