├── requirements.txt ├── setup.py ├── LICENSE ├── examples ├── multipage.py └── all_in_one.py ├── generator.py ├── main.py ├── .gitignore ├── README.md └── form.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import setuptools 3 | 4 | # Ensure the correct version of Python is being used 5 | if not sys.version_info >= (3, 6): 6 | sys.exit("Sorry, Python >= 3.6 is required") 7 | 8 | # Read the requirements from the requirements.txt file 9 | with open('requirements.txt') as f: 10 | required = f.read().splitlines() 11 | 12 | setuptools.setup( 13 | name="googleform-toolkit", 14 | version="0.0.1", 15 | author="Tien-Thanh Nguyen-Dang", 16 | author_email="ndtthanh214@gmail.com", 17 | description="Toolkit to automate filling and submitting Google Form", 18 | long_description="Toolkit to automate filling and submitting Google Form", 19 | long_description_content_type="text", 20 | license='LICENSE.txt', 21 | packages=setuptools.find_packages(), 22 | install_requires=required, 23 | classifiers=['Operating System :: POSIX', ], 24 | python_requires='>=3.6', 25 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tien-Thanh Nguyen-Dang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/multipage.py: -------------------------------------------------------------------------------- 1 | """ Multiple pages Google Form Example 2 | just add the field "pageHistory" to the request body 3 | """ 4 | import requests 5 | 6 | URL = "https://docs.google.com/forms/d/e/1FAIpQLSezUGYpq5iV9fUXymNoGzogcZgAqHuNBY-dOLR6LSDy2yub1g/formResponse" 7 | 8 | data = { 9 | # Your name (required) 10 | # Option: any text 11 | "entry.1715763968": "Your name", 12 | # Your age 13 | # Option: any text 14 | "entry.2032428014": "22", 15 | # None: Quiz 1 (required) 16 | # Options: ['A', 'B', 'C', 'D'] 17 | "entry.192506880": "A", 18 | # None: Quiz 2 (required) 19 | # Options: ['A', 'B', 'C', 'D'] 20 | "entry.250707008": "B", 21 | # None: Quiz 3 (required) 22 | # Options: ['A', 'B', 'C', 'D'] 23 | "entry.434270429": "C", 24 | # None: Quiz 4 (required) 25 | # Options: ['A', 'B', 'C', 'D'] 26 | "entry.1803402697": "D", 27 | # Do something? (required) 28 | # Options: ['Option 1', 'Option 2', 'Option 3'] 29 | "entry.368358396": "Option 1", 30 | # Page History 31 | # Options: from 0 to (number of page - 1) 32 | "pageHistory": "0,1,2,3,4", 33 | } 34 | 35 | res = requests.post(URL, data=data, timeout=5) 36 | if res.status_code == 200: 37 | print("Successfully submitted the form") 38 | else: 39 | print("Error! Can't submit form", res.status_code) 40 | 41 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | """ Generator module for generating form request data """ 2 | import json 3 | 4 | 5 | def generate_form_request_dict(entries, with_comment: bool = True): 6 | """ Generate a dict of form request data from entries """ 7 | result = "{\n" 8 | entry_id = 0 9 | for entry in entries: 10 | if with_comment: 11 | # gen name of entry 12 | result += f" # {entry['container_name']}{(': ' + entry['name']) if entry.get('name') else ''}{' (required)' * entry['required']}\n" 13 | # gen all options (if any) 14 | if entry['options']: 15 | result += f" # Options: {entry['options']}\n" 16 | else: 17 | result += f" # Option: {get_form_type_value_rule(entry['type'])}\n" 18 | # gen entry id 19 | entry_id += 1 20 | default_value = entry.get("default_value", "") 21 | default_value = json.dumps(default_value, ensure_ascii=False) 22 | 23 | if entry.get("type") == "required": 24 | result += f' "{entry["id"]}": {default_value}' 25 | else: 26 | result += f' "entry.{entry["id"]}": {default_value}' 27 | result += f"{(entry_id < len(entries)) * ','}\n" 28 | # remove the last comma 29 | result += "}" 30 | return result 31 | 32 | def get_form_type_value_rule(type_id): 33 | ''' ------ TYPE ID ------ 34 | 0: Short answer 35 | 1: Paragraph 36 | 2: Multiple choice 37 | 3: Dropdown 38 | 4: Checkboxes 39 | 5: Linear scale 40 | 7: Grid choice 41 | 9: Date 42 | 10: Time 43 | ''' 44 | if type_id == 9: 45 | return "YYYY-MM-DD" 46 | if type_id == 10: 47 | return "HH:MM (24h format)" 48 | return "any text" 49 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import datetime 3 | import json 4 | import random 5 | 6 | import requests 7 | 8 | import form 9 | 10 | 11 | def fill_random_value(type_id, entry_id, options, required = False, entry_name = ''): 12 | ''' Fill random value for a form entry 13 | Customize your own fill_algorithm here 14 | Note: please follow this func signature to use as fill_algorithm in form.get_form_submit_request ''' 15 | # Customize for specific entry_id 16 | if entry_id == 'emailAddress': 17 | return 'your_email@gmail.com' 18 | if entry_name == "Short answer": 19 | return 'Random answer!' 20 | # Random value for each type 21 | if type_id in [0, 1]: # Short answer and Paragraph 22 | return '' if not required else 'Ok!' 23 | if type_id == 2: # Multiple choice 24 | return random.choice(options) 25 | if type_id == 3: # Dropdown 26 | return random.choice(options) 27 | if type_id == 4: # Checkboxes 28 | return random.sample(options, k=random.randint(1, len(options))) 29 | if type_id == 5: # Linear scale 30 | return random.choice(options) 31 | if type_id == 7: # Grid choice 32 | return random.choice(options) 33 | if type_id == 9: # Date 34 | return datetime.date.today().strftime('%Y-%m-%d') 35 | if type_id == 10: # Time 36 | return datetime.datetime.now().strftime('%H:%M') 37 | return '' 38 | 39 | def generate_request_body(url: str, only_required = False): 40 | ''' Generate random request body data ''' 41 | data = form.get_form_submit_request( 42 | url, 43 | only_required = only_required, 44 | fill_algorithm = fill_random_value, 45 | output = "return", 46 | with_comment = False 47 | ) 48 | data = json.loads(data) 49 | # you can also override some values here 50 | return data 51 | 52 | def submit(url: str, data: any): 53 | ''' Submit form to url with data ''' 54 | url = form.get_form_response_url(url) 55 | print("Submitting to", url) 56 | print("Data:", data, flush = True) 57 | 58 | res = requests.post(url, data=data, timeout=5) 59 | if res.status_code != 200: 60 | print("Error! Can't submit form", res.status_code) 61 | 62 | def main(url, only_required = False): 63 | try: 64 | payload = generate_request_body(url, only_required = only_required) 65 | submit(url, payload) 66 | print("Done!!!") 67 | except Exception as e: 68 | print("Error!", e) 69 | 70 | if __name__ == '__main__': 71 | parser = argparse.ArgumentParser(description='Submit google form with custom data') 72 | parser.add_argument('url', help='Google Form URL') 73 | parser.add_argument('-r', '--required', action='store_true', help='Only include required fields') 74 | args = parser.parse_args() 75 | main(args.url, args.required) 76 | -------------------------------------------------------------------------------- /examples/all_in_one.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import requests 4 | 5 | URL = "https://docs.google.com/forms/u/0/d/e/1FAIpQLSdwcwvrOeBG200L0tCSUHc1MLebycACWIi3qw0UBK31GE26Yg/formResponse" 6 | 7 | def get_gmt_time(delta = 7): 8 | ''' get local time Vietnam (+7) 9 | help run correctly on any server''' 10 | date = datetime.datetime.now() 11 | tz = datetime.timezone(datetime.timedelta(hours = delta)) 12 | return date.astimezone(tz) 13 | 14 | start_day = datetime.datetime(2021, 8, 10, tzinfo = datetime.timezone(datetime.timedelta(hours = 7))) 15 | today = get_gmt_time() 16 | names = ["Jug", "Hex", "John", "Anna"] 17 | 18 | def get_name_by_day(): 19 | return names[(today - start_day).days % 4] 20 | 21 | def fill_form(): 22 | name = get_name_by_day() 23 | date, hour = str(get_gmt_time()).split(' ') 24 | date = date.split('-') 25 | hour = hour.split(':') 26 | if (int(hour[0]) < 10): 27 | hour[0] = hour[0][1:] 28 | value = { 29 | # Name (required) 30 | # Option: any text 31 | "entry.2112281434": name, 32 | # Where (required) 33 | # Options: ['Ahihi', 'New York', 'Sài Gòn', 'Paris'] 34 | "entry.1600556346": "Sài Gòn", 35 | # Date 36 | # "entry.77071893_year": date[0], 37 | # "entry.77071893_month": date[1], 38 | # "entry.77071893_day": date[2], 39 | # Date (YYYY-MM-DD) 40 | "entry.77071893": date[0] + '-' + date[1] + '-' + date[2], 41 | # Time (HH:MM (24h format)) 42 | "entry.1734133505": hour[0] + ':' + hour[1], 43 | # Hour 44 | "entry.855769839": hour[0] + 'h', 45 | # Checkbox 46 | "entry.819260047": ["Cà phê", "Bể bơi"], 47 | # One choice 48 | "entry.1682233942": "Okay", 49 | # Grid Choice: Question 2 50 | # Options: ['A', 'B', 'C'] 51 | "entry.505915866": "A", 52 | # Grid checkbox: Question A 53 | # Options: ['1', '2', '3'] 54 | "entry.1795513578": "2", 55 | # Grid checkbox: Question B 56 | # Options: ['1', '2', '3'] 57 | "entry.1615989655": "3", 58 | # Page History 59 | # Options: from 0 to (number of page - 1) 60 | "pageHistory": "0,1", 61 | # Email address 62 | "emailAddress": "test_mail@gmail.com", 63 | } 64 | print(value, flush = True) 65 | return value 66 | 67 | 68 | def submit(url, data): 69 | ''' Submit form to url with data ''' 70 | try: 71 | res = requests.post(url, data=data, timeout=5) 72 | if res.status_code != 200: 73 | raise Exception("Error! Can't submit form", res.status_code) 74 | return True 75 | except Exception as e: 76 | print("Error!", e) 77 | return False 78 | 79 | # ---------------------------------------------------------------------- 80 | if __name__ == "__main__": 81 | print("Running script...", flush = True) 82 | submit(URL, fill_form()) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | **/data 9 | **/outputs 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 112 | .pdm.toml 113 | .pdm-python 114 | .pdm-build/ 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Google Form AutoFill and Submit 2 | Vietnamese version [here](https://tienthanh214.github.io/computer%20science/autofill-and-submit-ggform/) 3 | 4 | Someone send us a Google-form, and we need to fill it everyday or maybe every hour to report something. It seems to be boring, so I just think to write a script to automate the process using **Python 3** 5 | 6 | This is a simple and lightweight script to automatically fill and submit a Google form with random data. The script is customizable, allowing you to fill the form with the data you need. 7 | 8 | It's also include a request body *generator* for those who prefer to manually input data, you can simply copy and paste a Google form URL, eliminating the need for manual inspection. 9 | 10 | *This document will guide you through the process of creating a Python script to automatically fill and submit a Google form.* 11 | 12 | ## Table of Contents 13 | - [🚀 Google Form AutoFill and Submit](#-google-form-autofill-and-submit) 14 | - [Table of Contents](#table-of-contents) 15 | - [Features](#features) 16 | - [Prerequisites](#prerequisites) 17 | - [Getting Started](#getting-started) 18 | - [Access and get the URL](#access-and-get-the-url) 19 | - [Extract information](#extract-information) 20 | - [Automatically](#automatically) 21 | - [Manually](#manually) 22 | - [Note](#note) 23 | - [Write the Python script](#write-the-python-script) 24 | - [Fill form](#fill-form) 25 | - [Submit form](#submit-form) 26 | - [AutoFill and Submit](#autofill-and-submit) 27 | - [Run the Script](#run-the-script) 28 | - [Customize the Script](#customize-the-script) 29 | - [Limitations](#limitations) 30 | 31 | ## Features 32 | - [x] Supports multiple pages Google forms 33 | - [x] Supports Google Forms that collect email addresses (Responder input) 34 | - [x] Automatically generates the request body using the `form.py` script 35 | - [x] Auto-fills the form with random values (customizable) and submits it 36 | 37 | ## Prerequisites 38 | - Python 3.x 39 | - `requests` library (`pip install requests` or `pip install -r requirements.txt`) 40 | 41 | # Getting Started 42 | If you only want to fill and submit a Google form with random data, or customize the script to fill the form with the data you need, you can skip to the [AutoFill and Submit](#autofill-and-submit) section. 43 | 44 | Below are the steps to create a Python script to fill and submit a Google form. If you want to skip the manual inspection step, you can use the `form.py` script to automatically generate the request body (as described in the [Extract information Automatically](#automatically) section). 45 | 46 | ## Access and get the URL 47 | The URL of the Google form will look like this: 48 | ``` 49 | https://docs.google.com/forms/d/e/form-index/viewform 50 | ``` 51 | Just copy it and replace **viewform** to **formResponse** 52 | ``` 53 | https://docs.google.com/forms/d/e/form-index/formResponse 54 | ``` 55 | 56 | ## Extract information 57 | ### Automatically 58 | Just copy the Google form URL and run [form.py](form.py) script. The script will return a *dictionary* which contains the name attributes of each input element and the data you need to fill out. 59 | ```bash 60 | python form.py 61 | ``` 62 | The result will be printed to the console (by default), or saved to a file if the `-o` option is used. 63 | 64 | For more information, please use the help command 65 | ```bash 66 | python form.py -h 67 | ``` 68 | 69 | Example: 70 | ```bash 71 | python form.py 'https://docs.google.com/forms/u/0/d/e/1FAIpQLSdwcwvrOeBG200L0tCSUHc1MLebycACWIi3qw0UBK31GE26Yg/formResponse' -o results.txt 72 | ``` 73 | 74 | ### Manually 75 | Open the Google form, then open DevTools (inspect) for inspecting the input element. 76 | 77 | Each of the input elements which we need to fill data has format: ```name = "entry.id"``` 78 | 79 | Try to fill each input box to know its id 80 | 81 | ### Note 82 | - If the form requires email, please add the `emailAddress` field 83 | - For multiple pages forms, please add the `pageHistory` field with a comma-separated list of page numbers (starting from 0). For example, a 4-page form would have `"pageHistory": "0,1,2,3"` 84 | 85 | ## Write the Python script 86 | 87 | ### Fill form 88 | Create a dictionary in which keys are the name attributes of each input element, and values are the data you need to fill out 89 | 90 | ```py 91 | # example 92 | def fill_form(): 93 | name = get_name_by_day() 94 | date, hour = str(get_gmt_time()).split(' ') 95 | date = date.split('-') 96 | hour = hour.split(':') 97 | if (int(hour[0]) < 10): 98 | hour[0] = hour[0][1:] 99 | 100 | value = { 101 | # Text 102 | "entry.2112281434": name, 103 | # Dropdown menu 104 | "entry.1600556346": "Sài Gòn", 105 | # Date 106 | "entry.77071893_year": date[0], 107 | "entry.77071893_month": date[1], 108 | "entry.77071893_day": date[2], 109 | # Hour 110 | "entry.855769839": hour[0] + 'h', 111 | # Checkbox 112 | "entry.819260047": ["Cafe", "Fences"], 113 | # One choice 114 | "entry.1682233942": "Okay" 115 | } 116 | print(value, flush = True) 117 | return value 118 | ``` 119 | 120 | ### Submit form 121 | Just use POST method in ```requests``` 122 | ```python 123 | def submit(url, data): 124 | try: 125 | requests.post(url, data = data) 126 | print("Submitted successfully!") 127 | except: 128 | print("Error!") 129 | 130 | submit(url, fill_form()) 131 | ``` 132 | Done!!! 133 | 134 | # AutoFill and Submit 135 | ## Run the Script 136 | Run the `main.py` script with the Google form URL as an argument to automatically fill and submit the form with ***random data*** 137 | ```bash 138 | python main.py 139 | ``` 140 | For example: 141 | ```bash 142 | python main.py 'https://docs.google.com/forms/u/0/d/e/1FAIpQLSdwcwvrOeBG200L0tCSUHc1MLebycACWIi3qw0UBK31GE26Yg/viewform' 143 | ``` 144 | Use `-r`/`--required` to only fill the form with required fields 145 | ```bash 146 | python main.py 'https://docs.google.com/forms/u/0/d/e/1FAIpQLSdwcwvrOeBG200L0tCSUHc1MLebycACWIi3qw0UBK31GE26Yg/viewform' -r 147 | ``` 148 | ## Customize the Script 149 | The [main.py](main.py) script is a simple example of how to use the `form.py` script to automatically fill and submit a Google form. You can customize the `fill_form` function to fill the form with the data you need. 150 | 151 | # Limitations 152 | Please note that this script currently operates only with Google Forms that do not require user authentication. 153 | -------------------------------------------------------------------------------- /form.py: -------------------------------------------------------------------------------- 1 | """ Get entries from form 2 | Version 2: 3 | - support submit almost all types of google form fields 4 | - only support single page form 5 | - not support upload file (because it's required to login) 6 | Date: 2023-12-17 7 | """ 8 | 9 | import argparse 10 | import json 11 | import re 12 | 13 | import requests 14 | 15 | import generator 16 | 17 | # constants 18 | ALL_DATA_FIELDS = "FB_PUBLIC_LOAD_DATA_" 19 | FORM_SESSION_TYPE_ID = 8 20 | ANY_TEXT_FIELD = "ANY TEXT!!" 21 | 22 | """ --------- Helper functions --------- """ 23 | 24 | def get_form_response_url(url: str): 25 | ''' Convert form url to form response url ''' 26 | url = url.replace('/viewform', '/formResponse') 27 | if not url.endswith('/formResponse'): 28 | if not url.endswith('/'): 29 | url += '/' 30 | url += 'formResponse' 31 | return url 32 | 33 | def extract_script_variables(name: str, html: str): 34 | pattern = re.compile(r'var\s' + name + r'\s*=\s*(\[[\s\S]*?\]);') 35 | match = pattern.search(html) 36 | if not match: 37 | return None 38 | value_str = match.group(1) 39 | try: 40 | return json.loads(value_str) 41 | except json.JSONDecodeError: 42 | print("JSON parsing failed, trying ast.literal_eval") 43 | try: 44 | import ast 45 | return ast.literal_eval(value_str) 46 | except: 47 | print("ast.literal_eval also failed") 48 | return None 49 | 50 | def get_fb_public_load_data(url: str): 51 | """ Get form data from a google form url """ 52 | try: 53 | response = requests.get(url, timeout=10) 54 | response.raise_for_status() # Raise an exception for bad status codes 55 | print(f"Successfully fetched URL: {url} - Status Code: {response.status_code}") 56 | data = extract_script_variables(ALL_DATA_FIELDS, response.text) 57 | if not data: 58 | print(f"Failed to extract {ALL_DATA_FIELDS} from the response.") 59 | return data 60 | except requests.RequestException as e: 61 | print(f"Error fetching URL: {e}") 62 | return None 63 | 64 | # ------ MAIN LOGIC ------ # 65 | 66 | def parse_form_entries(url: str, only_required = False): 67 | """ 68 | In window.FB_PUBLIC_LOAD_DATA_ (as v) 69 | - v[1][1] is the form entries array 70 | - for x in v[1][1]: 71 | x[0] is the entry id of the entry container 72 | x[1] is the entry name (*) 73 | x[3] is the entry type 74 | x[4] is the array of entry (usually length of 1, but can be more if Grid Choice, Linear Scale) 75 | x[4][0] is the entry id (we only need this to make request) (*) 76 | x[4][1] is the array of entry value (if null then text) 77 | x[4][1][i][0] is the i-th entry value option (*) 78 | x[4][2] field required (1 if required, 0 if not) (*) 79 | x[4][3] name of Grid Choice, Linear Scale (in array) 80 | - v[1][10][6]: determine the email field if the form request email 81 | 1: Do not collect email 82 | 2: required checkbox, get verified email 83 | 3: required responder input 84 | """ 85 | url = get_form_response_url(url) 86 | 87 | v = get_fb_public_load_data(url) 88 | if not v or not v[1] or not v[1][1]: 89 | print("Error! Can't get form entries. Login may be required.") 90 | return None 91 | 92 | def parse_entry(entry): 93 | entry_name = entry[1] 94 | entry_type_id = entry[3] 95 | result = [] 96 | for sub_entry in entry[4]: 97 | info = { 98 | "id": sub_entry[0], 99 | "container_name": entry_name, 100 | "type": entry_type_id, 101 | "required": sub_entry[2] == 1, 102 | "name": ' - '.join(sub_entry[3]) if (len(sub_entry) > 3 and sub_entry[3]) else None, 103 | "options": [(x[0] or ANY_TEXT_FIELD) for x in sub_entry[1]] if sub_entry[1] else None, 104 | } 105 | if only_required and not info['required']: 106 | continue 107 | result.append(info) 108 | return result 109 | 110 | parsed_entries = [] 111 | page_count = 0 112 | for entry in v[1][1]: 113 | if entry[3] == FORM_SESSION_TYPE_ID: 114 | page_count += 1 115 | continue 116 | parsed_entries += parse_entry(entry) 117 | 118 | # Collect email addresses 119 | if v[1][10][6] > 1: 120 | parsed_entries.append({ 121 | "id": "emailAddress", 122 | "container_name": "Email Address", 123 | "type": "required", 124 | "required": True, 125 | "options": "email address", 126 | }) 127 | if page_count > 0: 128 | parsed_entries.append({ 129 | "id": "pageHistory", 130 | "container_name": "Page History", 131 | "type": "required", 132 | "required": False, 133 | "options": "from 0 to (number of page - 1)", 134 | "default_value": ','.join(map(str,range(page_count + 1))) 135 | }) 136 | 137 | return parsed_entries 138 | 139 | def fill_form_entries(entries, fill_algorithm): 140 | """ Fill form entries with fill_algorithm """ 141 | for entry in entries: 142 | if entry.get('default_value'): 143 | continue 144 | # remove ANY_TEXT_FIELD from options to prevent choosing it 145 | options = (entry['options'] or [])[::] 146 | if ANY_TEXT_FIELD in options: 147 | options.remove(ANY_TEXT_FIELD) 148 | 149 | entry['default_value'] = fill_algorithm(entry['type'], entry['id'], options, 150 | required = entry['required'], entry_name = entry['container_name']) 151 | return entries 152 | 153 | # ------ OUTPUT ------ # 154 | def get_form_submit_request( 155 | url: str, 156 | output = "console", 157 | only_required = False, 158 | with_comment = True, 159 | fill_algorithm = None, 160 | ): 161 | ''' Get form request body data ''' 162 | entries = parse_form_entries(url, only_required = only_required) 163 | if fill_algorithm: 164 | entries = fill_form_entries(entries, fill_algorithm) 165 | if not entries: 166 | return None 167 | result = generator.generate_form_request_dict(entries, with_comment) 168 | if output == "console": 169 | print(result) 170 | elif output == "return": 171 | return result 172 | else: 173 | # save as file 174 | with open(output, "w", encoding="utf-8") as f: 175 | f.write(result) 176 | print(f"Saved to {output}", flush = True) 177 | f.close() 178 | return None 179 | 180 | 181 | 182 | if __name__ == "__main__": 183 | parser = argparse.ArgumentParser(description="Google Form Autofill and Submit") 184 | parser.add_argument("url", help="Google Form URL") 185 | parser.add_argument("-o", "--output", default="console", help="Output file path (default: console)") 186 | parser.add_argument("-r", "--required", action="store_true", help="Only include required fields") 187 | parser.add_argument("-c", "--no-comment", action="store_true", help="Don't include explain comment for each field") 188 | args = parser.parse_args() 189 | get_form_submit_request(args.url, args.output, args.required, not args.no_comment) --------------------------------------------------------------------------------