Skip to content

Instantly share code, notes, and snippets.

@erikson1970
Last active June 23, 2026 01:30
Show Gist options
  • Select an option

  • Save erikson1970/a870bde4f8afb4106db0b895a8e20957 to your computer and use it in GitHub Desktop.

Select an option

Save erikson1970/a870bde4f8afb4106db0b895a8e20957 to your computer and use it in GitHub Desktop.
Sentence Generator in Python
#!/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()
#!/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