Tohle je vlastně jen vedlejší efekt sumarizace přes ChatGPT API – už tam je totiž dobře fungující skript co stáhne z URL jen samotný obsah.
Ukázalo se ale, že něco takového se bude hodit i samostatně – ChatGPT totiž velmi často nedokáže otevřít webovou adresu (weby ji blokují, jsou tam nesmyslné cookie lišty, atd), takže je třeba obsah z této adresy poskytnout ručně.
Jenže ukládat to jako PDF nebo to copy-paste vkládat postupně do ChatGPT není moc šikovné.
Co ale jde, je využít už hotového skriptu a jen ho osamostatnit. Ten vezme URL ve schránce/clipboard, stáhne obsah pomocí trafilatura a stažené vloží do schránky (clipboardu). Zachováno zůstalo i logování, spouštění pomocí RayCastu na jedné klávese.
Vrací čistý text, ale trafilatura umí vrátit i markdown – jen je ve volání trafilature.extract případně nutné doplnit jeden parametr navíc – output_format=“markdown“
Ještě by šlo udělat to, že sumarizační python si zavolá tento skript, aby v tom sumarizačním nemusel být znovu ten samý kód. Ale to si nechám někdy na později.
TIP: Sumarizační skript viz Jak na ChatGPT API pro souhrn článků na Internetu. Další velmi dobrý příklad včetně kompletního skriptu 💎
Funguje to?
Drtivá většina použití funguje dobře, čas od času to ale nevrátí obsah, nebo to vrátí jen nějaké zmatky – jsou weby co například generují obsah stránky až v prohlížeči na klientovi. Nebo mají nějakou zoufale šílenou paranoidní ochranu proti robotům. Případně vyžadují nějaké kliknutí (ne nutně paywall, ale může být i ten), aby se dalo dostat k obsahu.
Na podobné případy je tohle řešení krátké, ale není problém to pro tenhle účet prostě otevřít “starým způsobem”, tedy klasicky v prohlížeči a použít buď PDF nebo copy/paste.
To že to dobře funguje je věc postupného ladění pro předchozí použití – ukázalo se, že je nutné mít user-agent co opravdu vypadá jako prohlížeč (Chrome), ohlídat si aby server neposílal gzip (dalo by se řešit i rozbalením, dá se to detekovat). Jsou tam doplněné i nějak fall-back na amp verze a podobně (ale k tomu skoro nedochází).
Pro případné další ladění je tam nechané aktivní logování – v .log souboru je vidět co se dělo a do .html souboru se ukládá co vlastně přesně přišlo jako odpověď.
Samotný grabURL.py
Připomenu, že vše se odehrává ze schránky do schránky a jde o Python, který lze spouštět i z Terminalu, ale v mém případě je přes RayCast spouštěný klávesovou zkratkou.
#!/usr/bin/env python3
# @raycast.schemaVersion 1
# @raycast.title Copy Article Text from URL
# @raycast.mode compact
# @raycast.packageName Tools
# @raycast.icon 📰
import os, sys, re, subprocess, urllib.error, traceback, datetime, pathlib, urllib.request
# --- Log do stejné složky jako skript (při startu vymažeme) ---
LOG_FILE = pathlib.Path(__file__).with_suffix(".log")
try:
LOG_FILE.write_text("", encoding="utf-8")
except Exception:
pass
def log(msg: str):
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{ts}] {msg}\n")
except Exception:
pass
# --- util ---
def pbpaste() -> str:
return subprocess.check_output("pbpaste", text=True)
def pbcopy(text: str) -> None:
subprocess.run("pbcopy", text=True, input=text, check=True)
def beep(times=1):
try:
subprocess.run(["osascript", "-e", f'beep {times}'])
except Exception:
pass
def is_url(s: str) -> bool:
return bool(re.match(r'^https?://', s.strip(), re.I))
# --- HTTP fetch s fallbacky ---
def fetch_with_status(url: str, timeout=20):
import gzip
headers = {
"User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"),
"Accept": ("text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8"),
"Accept-Language": "cs-CZ,cs;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
status = getattr(resp, "status", None)
hdrs = dict(resp.headers)
log(f"HTTP status: {status}")
log(f"Content-Type: {hdrs.get('Content-Type', 'NA')}")
raw = resp.read()
# manuální gzip, pokud server neposlal Content-Encoding
if raw.startswith(b"\x1f\x8b"):
try:
raw = gzip.decompress(raw)
log("Applied manual gzip decompression")
except Exception as e:
log(f"Gzip decompress failed: {e}")
charset = "utf-8"
m = re.search(r"charset=([a-zA-Z0-9_\-]+)", hdrs.get("Content-Type", ""))
if m:
charset = m.group(1).strip()
try:
html = raw.decode(charset, errors="ignore")
except Exception:
html = raw.decode("utf-8", errors="ignore")
return status, hdrs, html
except urllib.error.HTTPError as e:
log(f"HTTPError {e.code}: {e.reason}")
return e.code, {}, None
except urllib.error.URLError as e:
log(f"URLError: {e.reason}")
return None, {}, None
except Exception as e:
log(f"FETCH EXCEPTION: {e}")
return None, {}, None
def try_fallback_urls(original_url: str):
cands = []
base = original_url[:-1] if original_url.endswith("/") else original_url
cands.extend([
base + "/amp",
base + "/amp/",
base + "?outputType=amp",
base + "?amp",
base + "/print",
base + "/print/",
])
seen, result = set(), []
for u in cands:
if u not in seen:
result.append(u); seen.add(u)
return result
# --- Extrakce článku (trafilatura) ---
def extract_article_text(url: str) -> str:
try:
import trafilatura
except Exception:
print("ERROR: chybí 'trafilatura'. Nainstaluj: python3 -m pip install --user trafilatura")
sys.exit(1)
log(f"Fetching {url}")
status, headers, html = fetch_with_status(url)
if not html or (status in (401, 403)):
log(f"Primary fetch failed or blocked (status={status}); trying fallbacks…")
for fu in try_fallback_urls(url):
log(f"Fallback fetch {fu}")
status, headers, html = fetch_with_status(fu)
if html and (status is None or 200 <= status < 300):
log(f"Fallback OK with {fu}")
break
if not html:
raise RuntimeError(f"Stahování selhalo, status={status}, hlavičky={headers}")
try:
text = trafilatura.extract(
html,
include_comments=False,
include_tables=False,
favor_recall=True,
with_metadata=False, # čistý text
)
except Exception as e:
log(f"trafilatura.extract exception: {type(e).__name__}: {e}")
raise
clean = (text or "").strip()
# minimální sanity check
if len(clean) < 200:
preview = (clean[:500] + ("…" if len(clean) > 500 else "")) if clean else "None"
log(f"Extraction too short ({len(clean)} chars). Preview:\n{preview}")
# Pro debug ulož kus HTML
debug_html = LOG_FILE.with_suffix(".debug.html")
try:
debug_html.write_text(html[:20000], encoding="utf-8", errors="ignore")
log(f"Saved first 20kB of HTML to {debug_html}")
except Exception as e:
log(f"Failed to write debug HTML: {e}")
raise RuntimeError("Extrakce nenašla dostatek článkového textu.")
# lehké vyčištění nových řádků
clean = re.sub(r'\n{3,}', '\n\n', clean).strip()
log(f"Extracted text length: {len(clean)} chars")
return clean
def main():
clip = pbpaste().strip()
if not is_url(clip):
print("ERROR: Ve schránce není URL")
beep(3); sys.exit(1)
try:
article_text = extract_article_text(clip)
except Exception as e:
log("Extract ERROR: " + str(e))
print(f"ERROR: Extrakce textu selhala: {e}")
beep(3); sys.exit(1)
# výsledek do schránky + echo do konzole
pbcopy(article_text)
print(f"OK ({len(article_text)} znaků) – text článku je ve schránce.")
log("Done OK")
beep(1)
if __name__ == "__main__":
try:
main()
except Exception as e:
log("FATAL ERROR: " + str(e))
log(traceback.format_exc())
print(f"ERROR: {e}")
beep(3)
sys.exit(1)
