actorcat

News Scraper

Oct 9th, 2026
58
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.26 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Google News RSS search: enter a subject and a location, get a digest.
  4.  
  5. Usage:
  6.    python news_search.py                      (opens the input window)
  7.    python news_search.py "school board" "Phoenix Arizona" [days]
  8.    python news_search.py "school board" "Phoenix, Arizona; Tucson, Arizona" [days]
  9.  
  10. Locations are separated by semicolons. A separate HTML digest is created
  11. for each location in OUTPUT_DIR, and the pages open automatically in your browser.
  12. """
  13.  
  14. import sys
  15. import io
  16. import re
  17. import html
  18. import json
  19. import webbrowser
  20. import hashlib
  21. import urllib.request
  22. import urllib.parse
  23. from datetime import datetime
  24. from pathlib import Path
  25. from concurrent.futures import ThreadPoolExecutor, as_completed
  26.  
  27. # With a windowed (no-console) build, stdout/stderr are None.
  28. if sys.stdout is not None and sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
  29.     sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
  30. if sys.stderr is not None and sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
  31.     sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
  32.  
  33. import feedparser
  34.  
  35. OUTPUT_DIR = Path(r"C:\Users\buzzl\OneDrive\Desktop\news output")
  36. OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
  37.  
  38. # Edition of Google News (language / country). Change for non-US searches.
  39. HL, GL, CEID = 'en-US', 'US', 'US:en'
  40.  
  41.  
  42. def build_url(subject, location, days):
  43.     query = f"{subject} {location} when:{days}d"
  44.     return (
  45.         "https://news.google.com/rss/search?"
  46.         + urllib.parse.urlencode({'q': query, 'hl': HL, 'gl': GL, 'ceid': CEID})
  47.     )
  48.  
  49.  
  50. def resolve_url(url, timeout=5):
  51.     if 'news.google.com' not in url:
  52.         return url
  53.     try:
  54.         req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
  55.         with urllib.request.urlopen(req, timeout=timeout) as response:
  56.             return response.url
  57.     except Exception:
  58.         return url
  59.  
  60.  
  61. def resolve_all(urls, workers=10):
  62.     resolved = {}
  63.     with ThreadPoolExecutor(max_workers=workers) as ex:
  64.         futures = {ex.submit(resolve_url, u): u for u in urls}
  65.         for f in as_completed(futures):
  66.             u = futures[f]
  67.             try:
  68.                 resolved[u] = f.result()
  69.             except Exception:
  70.                 resolved[u] = u
  71.     return resolved
  72.  
  73.  
  74. def fetch_articles(subject, location, days):
  75.     url = build_url(subject, location, days)
  76.     print(f"Fetching: {url}")
  77.     feed = feedparser.parse(url)
  78.     entries = feed.entries
  79.     print(f"  {len(entries)} results; resolving links...")
  80.     resolved = resolve_all([e.get('link', '') for e in entries if e.get('link')])
  81.  
  82.     articles, seen = [], set()
  83.     for e in entries:
  84.         title = e.get('title', 'No title')
  85.         key = title.lower().strip()
  86.         if key in seen:
  87.             continue
  88.         seen.add(key)
  89.         link = resolved.get(e.get('link', ''), e.get('link', ''))
  90.         src = e.get('source', {})
  91.         source = src.get('title', '') if hasattr(src, 'get') else ''
  92.         articles.append({
  93.             'hash': hashlib.md5(f"{key}{link}".encode()).hexdigest()[:12],
  94.             'title': title,
  95.             'link': link,
  96.             'source': source,
  97.             'published': e.get('published', e.get('updated', '')),
  98.         })
  99.     return articles
  100.  
  101.  
  102. def build_report(subject, location, days, articles):
  103.     esc = html.escape
  104.     items = []
  105.     for a in articles:
  106.         meta = " · ".join(esc(x) for x in (a['source'], a['published']) if x)
  107.         items.append(
  108.             f'<li><a href="{esc(a["link"], quote=True)}" target="_blank" rel="noopener noreferrer">'
  109.             f'{esc(a["title"])}</a>'
  110.             + (f'<div class="meta">{meta}</div>' if meta else '')
  111.             + '</li>'
  112.         )
  113.     generated = datetime.now().strftime('%A %d %B %Y at %H:%M')
  114.     return f"""<!DOCTYPE html>
  115. <html lang="en">
  116. <head>
  117. <meta charset="utf-8">
  118. <meta name="viewport" content="width=device-width, initial-scale=1">
  119. <title>News: {esc(subject)} — {esc(location)}</title>
  120. <style>
  121.  body {{ font-family: Segoe UI, Arial, sans-serif; max-width: 820px; margin: 2rem auto; padding: 0 1rem; color: #222; }}
  122.  h1 {{ margin-bottom: .2rem; }}
  123.  .sub {{ color: #666; margin-bottom: 1.5rem; }}
  124.  ul {{ list-style: none; padding: 0; }}
  125.  li {{ padding: .8rem 0; border-bottom: 1px solid #ddd; }}
  126.  a {{ font-weight: 600; color: #0b57d0; text-decoration: none; }}
  127.  a:hover {{ text-decoration: underline; }}
  128.  .meta {{ color: #666; font-size: .85rem; margin-top: .2rem; }}
  129. </style>
  130. </head>
  131. <body>
  132. <h1>{esc(subject)} — {esc(location)}</h1>
  133. <div class="sub">Generated {esc(generated)} &middot; last {esc(str(days))} days &middot; {len(articles)} articles</div>
  134. <ul>
  135. {chr(10).join(items)}
  136. </ul>
  137. </body>
  138. </html>
  139. """
  140.  
  141.  
  142. def slug(text):
  143.     return re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-')
  144.  
  145.  
  146. def parse_locations(text):
  147.     locations = []
  148.     seen = set()
  149.     for location in text.split(';'):
  150.         location = location.strip()
  151.         key = location.casefold()
  152.         if location and key not in seen:
  153.             locations.append(location)
  154.             seen.add(key)
  155.     return locations
  156.  
  157.  
  158. def run_search(subject, locations, days, status=print):
  159.     date = datetime.now().strftime('%Y-%m-%d')
  160.     used_bases = set()
  161.     html_files = []
  162.     for i, location in enumerate(locations, 1):
  163.         status(f"[{i}/{len(locations)}] Searching {location}...")
  164.         articles = fetch_articles(subject, location, days)
  165.         base = f"news_{slug(subject)}_{slug(location)}_{date}"
  166.         unique_base = base
  167.         suffix = 2
  168.         while unique_base in used_bases:
  169.             unique_base = f"{base}_{suffix}"
  170.             suffix += 1
  171.         used_bases.add(unique_base)
  172.         html_file = OUTPUT_DIR / f"{unique_base}.html"
  173.         html_file.write_text(
  174.             build_report(subject, location, days, articles), encoding='utf-8'
  175.         )
  176.         html_files.append((html_file, len(articles)))
  177.  
  178.     for html_file, article_count in html_files:
  179.         status(f"{article_count} articles -> {html_file}")
  180.         webbrowser.open(html_file.as_uri())
  181.     return html_files
  182.  
  183.  
  184. def run_gui():
  185.     import queue
  186.     import threading
  187.     import tkinter as tk
  188.     from tkinter import messagebox
  189.  
  190.     root = tk.Tk()
  191.     root.title("News Search")
  192.     root.columnconfigure(0, weight=1)
  193.  
  194.     pad = {'padx': 12, 'sticky': 'ew'}
  195.     tk.Label(root, text="Subject").grid(row=0, column=0, pady=(12, 2), **pad)
  196.     subject_box = tk.Entry(root, width=50)
  197.     subject_box.grid(row=1, column=0, **pad)
  198.  
  199.     tk.Label(root, text="Locations (separate with semicolons)").grid(row=2, column=0, pady=(10, 2), **pad)
  200.     locations_box = tk.Entry(root, width=50)
  201.     locations_box.grid(row=3, column=0, **pad)
  202.  
  203.     tk.Label(root, text="Number of days").grid(row=4, column=0, pady=(10, 2), **pad)
  204.     days_box = tk.Entry(root, width=50)
  205.     days_box.insert(0, '7')
  206.     days_box.grid(row=5, column=0, **pad)
  207.  
  208.     status_var = tk.StringVar()
  209.     search_btn = tk.Button(root, text="Search")
  210.     search_btn.grid(row=6, column=0, pady=12)
  211.     tk.Label(root, textvariable=status_var, fg='#555', wraplength=420).grid(row=7, column=0, pady=(0, 12), **pad)
  212.  
  213.     messages = queue.Queue()
  214.  
  215.     def worker(subject, locations, days):
  216.         try:
  217.             run_search(subject, locations, days, status=lambda m: messages.put(('msg', m)))
  218.             messages.put(('done', f"Done: {len(locations)} file(s) created."))
  219.         except Exception as exc:
  220.             messages.put(('done', f"Error: {exc}"))
  221.  
  222.     def poll():
  223.         try:
  224.             while True:
  225.                 kind, text = messages.get_nowait()
  226.                 status_var.set(text)
  227.                 if kind == 'done':
  228.                     search_btn.config(state='normal')
  229.         except queue.Empty:
  230.             pass
  231.         root.after(100, poll)
  232.  
  233.     def on_search(event=None):
  234.         subject = subject_box.get().strip()
  235.         locations = parse_locations(locations_box.get())
  236.         days = days_box.get().strip() or '7'
  237.         if not subject or not locations:
  238.             messagebox.showwarning("Missing input", "Enter a subject and at least one location.")
  239.             return
  240.         if not days.isdigit() or int(days) < 1:
  241.             messagebox.showwarning("Invalid days", "Number of days must be a positive whole number.")
  242.             return
  243.         search_btn.config(state='disabled')
  244.         status_var.set("Starting...")
  245.         threading.Thread(target=worker, args=(subject, locations, days), daemon=True).start()
  246.  
  247.     search_btn.config(command=on_search)
  248.     root.bind('<Return>', on_search)
  249.     subject_box.focus_set()
  250.     poll()
  251.     root.mainloop()
  252.  
  253.  
  254. def main():
  255.     args = sys.argv[1:]
  256.     if len(args) < 2:
  257.         run_gui()
  258.         return
  259.     subject = args[0]
  260.     rest = args[1:]
  261.     days = '7'
  262.     if len(rest) > 1 and rest[-1].isdigit():
  263.         days = rest.pop()
  264.     locations = parse_locations(';'.join(rest))
  265.     if not locations:
  266.         print("Both a subject and at least one location are required.")
  267.         return
  268.     if not days.isdigit():
  269.         print("Days must be a number; using 7.")
  270.         days = '7'
  271.     run_search(subject, locations, days)
  272.  
  273.  
  274. if __name__ == '__main__':
  275.     main()
  276.  
Advertisement
Add Comment
Please, Sign In to add comment