Last active
June 23, 2026 01:30
-
-
Save erikson1970/a870bde4f8afb4106db0b895a8e20957 to your computer and use it in GitHub Desktop.
Sentence Generator in Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python3 | |
| import argparse | |
| from zlib import decompress | |
| from base64 import b64decode | |
| from random import randint | |
| from random import seed | |
| try: | |
| import argcomplete | |
| except ImportError: | |
| argcomplete = None | |
| def _version(): | |
| return "0.2" | |
| timefmt = "%j:%H:%M:%S.%f " | |
| def create_parser(description): | |
| p = argparse.ArgumentParser(description=description) | |
| p.add_argument( | |
| "-l", | |
| "--length", | |
| type=int, | |
| default=80, | |
| help="max char length to send [80]", | |
| ) | |
| p.add_argument( | |
| "-a", | |
| "--actors", | |
| type=int, | |
| default=3, | |
| help="max number of actors [5]", | |
| ) | |
| p.add_argument( | |
| "-c", | |
| "--count", | |
| type=int, | |
| default=1, | |
| help="number of sentences to return[1]", | |
| ) | |
| p.add_argument( | |
| "-f", | |
| "--foods", | |
| type=int, | |
| default=2, | |
| help="max number of foods [2]", | |
| ) | |
| p.add_argument( | |
| "-v", | |
| "--verbs", | |
| type=int, | |
| default=2, | |
| help="max number of verbs [2]", | |
| ) | |
| p.add_argument( | |
| "-s", | |
| "--spaces", | |
| type=str, | |
| default=" ", | |
| help="replace spaces with char [ None ]", | |
| ) | |
| p.add_argument( | |
| "-d", | |
| "--seed", | |
| type=int, | |
| default=-9999, | |
| help="Set a seed for random number generation", | |
| ) | |
| if argcomplete: | |
| argcomplete.autocomplete(p) | |
| return p | |
| class DocumentGenerator: | |
| wordsStore = [] | |
| theSeed = -9999 | |
| def __init__(self): | |
| self.wordsStore = [ | |
| ii.split(" ") | |
| for ii in decompress( | |
| b64decode( | |
| """eJw9VMuS3DYMvOsr8Cuz6y07W2PHFVflkBtEYSR6SIIGyXnk69OQ1qk5aFR | |
| oNYBGAyc2LfSWMxOnmbtpa8RdaOaC33SaJdGfKd6iA1JcuatRUL3KQnPicJ3F7Amc8caZTjfHV | |
| Q5Ms8a0g4Z8YBYA/mgMygRU4aBlYbpYdJgJLxcbsQNnkQv90Lp51tJRjKcL+MtJR5XplOTBZRG | |
| jrwdEULNRFfGUYTvylcXkTq8bW9KOnhyXtKI5HevWgYtoCbi+aXnSKUtyNsu8IKDUNIHMuHzUb | |
| 05MbzdJz4KalhVvYYBnGEAo3Po2jE4zZIrpf8SmtaKqxXjV8tFic6YvbBWPWfiGB+LlSpe4Tqf | |
| RevSpxPSkOTZMqMWdY1Vtv9V8YDJvKf4LPfvmJJ5K7o4yhkYvUn5yBs8J7OAZlwt7U12THJCjl | |
| pcEdaH2Bb0H7tQyozqwDL7x9GK8JHkiE2YWTO/UN1cVWmlB1Cf1FXp5kQubT2E1HWWha7zHI8M | |
| rJ5npR8AcpHdadKXOrSPHdeRfg3fEDZX+HQPshTqEVxTZKspJkpHolfMiaGUPJqnbbgvxeMziY | |
| XEjfzYOQhd90CUxRl6QIz0hCyCbRfReXfjXLakcYkIXa50y3MQWi0yfuEQoC8aIjuGdHeJEwKy | |
| K+C0u9B3Uu5VQHjReqUF/YAq0i9OnZ4Isf0UXLkUf3x3Loz6bIqEfiTC7n7zRmQFGkeUBBxxOq | |
| Vz5ydNbR5d0do/imV1c2M6wekC0Fn+b6TPP5iV/U2PSig0emT6GWIXDNn2RAgf8oyjHF8GoA3R | |
| EbcJGcsAeoYoaV1qwBDDMHrUWs6v/jj2n08COPqnCPm6TeYegEa41iUN0hqNLQU9VLQyPwQvwS | |
| k0jA5CleTdPMnwOYW+ye6BqFtixYIOnd90w5OWwExqQ6rfGd/rXiCU4oGH4cKNwwSFq14idCc4 | |
| F1laP1Xgfu2LfGOciwkb41gwCQbv9JDVM4JDuHHGSzqMwQZXiW9H3uPH9oDrr6qcIm7A3Bnd2j | |
| c37wlgA7Yy5+LGYziNwoxfDafTz4APnGWuXBHcKQM04ndN3cfnPAq47kEEhbI4PAO5+wfat+g8 | |
| M6Ats""" | |
| ) | |
| ) | |
| .decode("utf-8") | |
| .split("\n") | |
| ] | |
| self.wordCnt = len(self.wordsStore) - 1 | |
| def setSeed(self, seedMe): | |
| self.theSeed = seedMe | |
| if self.theSeed != -9999: | |
| seed(self.theSeed) | |
| def sentence(self, actors=5, actorAnimalOdds=1, verbs=2, foods=2): | |
| def joinNice(ll, n2plus=", ", n2=" and "): | |
| """Join a list of words with ',' and/or 'and' depending on number of items in list""" | |
| return ( | |
| "{}{}{}{}".format(n2plus.join(ll[:-1]), n2plus, n2[1:], ll[-1]) | |
| if len(ll) > 2 | |
| else ("{}{}{}".format(ll[0], n2, ll[1]) if len(ll) > 1 else ll[0]) | |
| ) | |
| # results = "{} the {} {} {}.".format( | |
| results = "{} {} {}.".format( | |
| joinNice( | |
| [ | |
| self.wordsStore[randint(0, self.wordCnt)][randint(0, 1)] | |
| + ( | |
| " the " + self.wordsStore[randint(0, self.wordCnt)][2] | |
| if randint(0, actorAnimalOdds) > 0 | |
| else "" | |
| ) | |
| for ii in range(randint(1, actors)) | |
| ] | |
| ), | |
| joinNice( | |
| [ | |
| self.wordsStore[randint(0, self.wordCnt)][3] | |
| for _ in range(randint(1, verbs)) | |
| ] | |
| ), | |
| joinNice( | |
| [ | |
| "a " + self.wordsStore[randint(0, self.wordCnt)][4] | |
| for _ in range(randint(1, foods)) | |
| ] | |
| ), | |
| ) | |
| return results | |
| def getter( | |
| count=1, | |
| maxLen=400, | |
| maxTries=400, | |
| actors=5, | |
| actorAnimalOdds=1, | |
| verbs=2, | |
| foods=2, | |
| mySeed=-9999, | |
| ): | |
| gen = DocumentGenerator() | |
| gen.setSeed(mySeed) | |
| while count > 0: | |
| thisSent = (maxLen + 1) * "x" | |
| while len(thisSent) > maxLen and maxTries > 0: | |
| thisSent = gen.sentence( | |
| actors=actors, actorAnimalOdds=actorAnimalOdds, verbs=verbs, foods=foods | |
| ) | |
| maxTries -= 1 | |
| count -= 1 | |
| yield thisSent | |
| def _main(): | |
| fmt = "Sentence Generator Sender v{0:s}: " | |
| description = fmt.format(_version()) | |
| maxLen = 60 | |
| p = create_parser(description) | |
| args = p.parse_args() | |
| for i in getter( | |
| count=args.count, | |
| maxLen=args.length, | |
| maxTries=400, | |
| actors=args.actors, | |
| actorAnimalOdds=1, | |
| verbs=args.verbs, | |
| foods=args.foods, | |
| mySeed=args.seed, | |
| ): | |
| if args.spaces == " ": | |
| print(i) | |
| else: | |
| print(i.replace(" ", args.spaces)) | |
| if __name__ == "__main__": | |
| _main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| hotel_wifi_keepalive.py | |
| Low-rate HTTP keep-alive script for hotel Wi-Fi sessions. | |
| Purpose: | |
| Generate ordinary-looking, low-volume GET traffic at Poisson-distributed | |
| intervals so the hotel captive portal does not mark the connection idle. | |
| Usage: | |
| python hotel_wifi_keepalive.py | |
| Optional: | |
| Put your genSentence.py in the same directory. If it exposes a usable | |
| sentence-generation function, this script will try to use it. Otherwise it | |
| falls back to its own small phrase generator. | |
| Stop: | |
| Ctrl+C | |
| """ | |
| import random | |
| import time | |
| import string | |
| import sys | |
| from urllib.parse import urlencode | |
| from datetime import datetime | |
| try: | |
| import requests | |
| except ImportError: | |
| print("This script requires requests. Install with:") | |
| print(" python -m pip install requests") | |
| sys.exit(1) | |
| # --------------------------------------------------------------------- | |
| # Timing model | |
| # --------------------------------------------------------------------- | |
| MEAN_INTERVAL_SECONDS = 55.0 # average time between requests | |
| MIN_INTERVAL_SECONDS = 18.0 # prevent unrealistically rapid bursts | |
| MAX_INTERVAL_SECONDS = 180.0 # prevent very long idle gaps | |
| REQUEST_TIMEOUT_SECONDS = 12 | |
| MAX_BYTES_TO_READ = 64_000 # keeps bandwidth low while still doing a GET | |
| # --------------------------------------------------------------------- | |
| # Query phrase generation | |
| # --------------------------------------------------------------------- | |
| def fallback_sentence(): | |
| """ | |
| Built-in plausible query generator. | |
| These are intentionally boring, normal-looking web searches. | |
| The goal is not to impersonate a browser session perfectly, just to avoid | |
| nonsense query strings like adf83ksdf. | |
| """ | |
| topics = [ | |
| "weather forecast", | |
| "local restaurants", | |
| "coffee nearby", | |
| "news headlines", | |
| "python datetime example", | |
| "map directions", | |
| "flight status", | |
| "traffic update", | |
| "best breakfast nearby", | |
| "public radio schedule", | |
| "hotel checkout time", | |
| "nearby grocery store", | |
| "machine learning tutorial", | |
| "radio astronomy arrays", | |
| "matlab signal processing", | |
| "local events this week", | |
| "gas station nearby", | |
| "pharmacy hours", | |
| "wikipedia radar", | |
| "airport parking", | |
| "train schedule", | |
| "package tracking", | |
| "technical documentation", | |
| "linux command line tips", | |
| "restaurants open now", | |
| "walking directions", | |
| "coffee shop hours", | |
| "electronics store nearby", | |
| "book reviews", | |
| "blueberry muffin recipe", | |
| ] | |
| modifiers = [ | |
| "", | |
| "near me", | |
| "today", | |
| "this week", | |
| "example", | |
| "tutorial", | |
| "overview", | |
| "explained", | |
| "hours", | |
| "map", | |
| "latest", | |
| ] | |
| base = random.choice(topics) | |
| mod = random.choice(modifiers) | |
| if mod: | |
| return f"{base} {mod}" | |
| return base | |
| def try_external_sentence_generator(): | |
| """ | |
| Try to use a local genSentence.py if present. | |
| Since I don't know the exact API of your gist, this tries a few common | |
| function names. If none work, it returns None. | |
| """ | |
| try: | |
| import genSentence | |
| except Exception: | |
| return None | |
| candidate_function_names = [ | |
| "gen_sentence", | |
| "genSentence", | |
| "sentence", | |
| "make_sentence", | |
| "generate_sentence", | |
| "generate", | |
| "main", | |
| ] | |
| for name in candidate_function_names: | |
| fn = getattr(genSentence, name, None) | |
| if callable(fn): | |
| try: | |
| value = fn() | |
| if isinstance(value, str) and len(value.strip()) > 0: | |
| return value.strip() | |
| except Exception: | |
| pass | |
| return None | |
| def get_query_phrase(): | |
| """ | |
| Get a plausible search phrase. | |
| Prefer the user-provided generator if available; otherwise use fallback. | |
| Then clean it up into something search-engine-friendly. | |
| """ | |
| phrase = try_external_sentence_generator() | |
| if not phrase: | |
| phrase = fallback_sentence() | |
| # Convert to a reasonable query phrase. | |
| phrase = phrase.lower() | |
| phrase = phrase.replace("\n", " ") | |
| phrase = phrase.translate(str.maketrans("", "", string.punctuation.replace("-", ""))) | |
| phrase = " ".join(phrase.split()) | |
| # Keep searches short-ish. | |
| words = phrase.split() | |
| if len(words) > 8: | |
| words = words[: random.randint(3, 8)] | |
| return " ".join(words) | |
| # --------------------------------------------------------------------- | |
| # Realistic-ish endpoint selection | |
| # --------------------------------------------------------------------- | |
| def cache_buster(): | |
| """ | |
| A benign cache buster. Named parameters vary by endpoint below. | |
| """ | |
| return str(int(time.time())) + str(random.randint(100, 999)) | |
| def build_request(): | |
| """ | |
| Return a tuple: | |
| method, url, params, headers | |
| These are real public sites and the query params match the kind of params | |
| those sites normally accept. | |
| """ | |
| q = get_query_phrase() | |
| endpoints = [ | |
| { | |
| "name": "Bing search", | |
| "url": "https://www.bing.com/search", | |
| "params": lambda: { | |
| "q": q, | |
| "form": random.choice(["QBLH", "QBRE", "QBLH"]), | |
| }, | |
| }, | |
| { | |
| "name": "DuckDuckGo search", | |
| "url": "https://duckduckgo.com/html/", | |
| "params": lambda: { | |
| "q": q, | |
| }, | |
| }, | |
| { | |
| "name": "Wikipedia search", | |
| "url": "https://en.wikipedia.org/w/index.php", | |
| "params": lambda: { | |
| "search": q, | |
| "title": "Special:Search", | |
| "fulltext": "1", | |
| }, | |
| }, | |
| { | |
| "name": "Wiktionary search", | |
| "url": "https://en.wiktionary.org/w/index.php", | |
| "params": lambda: { | |
| "search": random.choice(q.split()) if q.split() else "example", | |
| "title": "Special:Search", | |
| }, | |
| }, | |
| { | |
| "name": "Python docs search", | |
| "url": "https://docs.python.org/3/search.html", | |
| "params": lambda: { | |
| "q": random.choice([ | |
| "datetime", | |
| "requests", | |
| "json", | |
| "argparse", | |
| "subprocess", | |
| "pathlib", | |
| "random", | |
| "logging", | |
| ]), | |
| "check_keywords": "yes", | |
| "area": "default", | |
| }, | |
| }, | |
| { | |
| "name": "NOAA search", | |
| "url": "https://search.usa.gov/search", | |
| "params": lambda: { | |
| "affiliate": "nws.noaa.gov", | |
| "query": random.choice([ | |
| "forecast", | |
| "radar", | |
| "current conditions", | |
| "weather alerts", | |
| "hourly weather", | |
| ]), | |
| }, | |
| }, | |
| { | |
| "name": "Internet Archive search", | |
| "url": "https://archive.org/search", | |
| "params": lambda: { | |
| "query": random.choice([ | |
| "radio", | |
| "radar", | |
| "engineering", | |
| "mathematics", | |
| "astronomy", | |
| "history", | |
| "signal processing", | |
| ]), | |
| }, | |
| }, | |
| ] | |
| ep = random.choice(endpoints) | |
| params = ep["params"]() | |
| # Mild page variation / cache avoidance, but not nonsense. | |
| if random.random() < 0.30: | |
| params["_"] = cache_buster() | |
| # Use one ordinary UA rather than rotating aggressively. | |
| # If you want, replace this with your actual browser's UA string. | |
| headers = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/124.0 Safari/537.36" | |
| ), | |
| "Accept": ( | |
| "text/html,application/xhtml+xml,application/xml;" | |
| "q=0.9,image/avif,image/webp,*/*;q=0.8" | |
| ), | |
| "Accept-Language": "en-US,en;q=0.9", | |
| "Connection": "close", | |
| } | |
| return ep["name"], ep["url"], params, headers | |
| # --------------------------------------------------------------------- | |
| # HTTP request handling | |
| # --------------------------------------------------------------------- | |
| def perform_get(session): | |
| name, url, params, headers = build_request() | |
| started = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| try: | |
| with session.get( | |
| url, | |
| params=params, | |
| headers=headers, | |
| timeout=REQUEST_TIMEOUT_SECONDS, | |
| stream=True, | |
| allow_redirects=True, | |
| ) as response: | |
| # Read a bounded amount so this creates real download activity | |
| # without wasting bandwidth. | |
| bytes_read = 0 | |
| for chunk in response.iter_content(chunk_size=8192): | |
| if not chunk: | |
| break | |
| bytes_read += len(chunk) | |
| if bytes_read >= MAX_BYTES_TO_READ: | |
| break | |
| final_url = response.url | |
| status = response.status_code | |
| # Possible captive portal hint: | |
| captive_hint = "" | |
| content_type = response.headers.get("Content-Type", "") | |
| if status in (301, 302, 303, 307, 308): | |
| captive_hint = " redirect" | |
| elif "text/html" in content_type and response.url != final_url: | |
| captive_hint = " possible_redirect" | |
| print( | |
| f"[{started}] {name:22s} " | |
| f"status={status:<3d} bytes={bytes_read:<6d} " | |
| f"query={params} {captive_hint}" | |
| ) | |
| except requests.exceptions.SSLError as e: | |
| print(f"[{started}] SSL error: {e}") | |
| except requests.exceptions.ConnectTimeout: | |
| print(f"[{started}] Connection timeout") | |
| except requests.exceptions.ReadTimeout: | |
| print(f"[{started}] Read timeout") | |
| except requests.exceptions.RequestException as e: | |
| print(f"[{started}] Request error: {e}") | |
| def poisson_sleep_seconds(): | |
| """ | |
| For a Poisson process, inter-arrival times are exponentially distributed. | |
| random.expovariate(lambda) expects lambda = events per second. | |
| If the desired mean interval is T seconds, lambda = 1/T. | |
| """ | |
| raw = random.expovariate(1.0 / MEAN_INTERVAL_SECONDS) | |
| # Clamp to keep behavior practical for hotel Wi-Fi. | |
| return max(MIN_INTERVAL_SECONDS, min(MAX_INTERVAL_SECONDS, raw)) | |
| def main(): | |
| print("Hotel Wi-Fi keep-alive running.") | |
| print("Press Ctrl+C to stop.") | |
| print(f"Mean interval: {MEAN_INTERVAL_SECONDS:.1f} seconds") | |
| print(f"Interval clamp: {MIN_INTERVAL_SECONDS:.1f} to {MAX_INTERVAL_SECONDS:.1f} seconds") | |
| print() | |
| session = requests.Session() | |
| while True: | |
| perform_get(session) | |
| sleep_s = poisson_sleep_seconds() | |
| print(f"Sleeping {sleep_s:.1f} seconds\n") | |
| time.sleep(sleep_s) | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |
| print("\nStopped.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment