import requests import pandas as pd import datetime import time import os import xml.etree.ElementTree as ET import argparse import random import sys # --- CONFIGURATION --- USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" # Public Mirrors to bypass AWS/Data Center IP Blocks # old.reddit.com works from residential IPs, mirrors for data centers MIRRORS = [ "https://old.reddit.com", "https://redlib.catsarch.com", "https://redlib.vsls.cz", "https://r.nf", "https://libreddit.northboot.xyz", "https://redlib.tux.pizza" ] SEEN_URLS = set() def get_file_path(target, type_prefix): """Generates a dynamic filename (e.g., data/r_python.csv or data/u_elonmusk.csv)""" # Ensure data directory exists if not os.path.exists("data"): os.makedirs("data") sanitized_target = target.replace("/", "_") return f"data/{type_prefix}_{sanitized_target}.csv" def load_history(filepath): """Loads existing CSV history to prevent duplicates.""" SEEN_URLS.clear() # Reset for new target if os.path.exists(filepath): try: df = pd.read_csv(filepath) for url in df['URL']: SEEN_URLS.add(str(url)) print(f"πŸ“š Loaded {len(SEEN_URLS)} existing items from {filepath}") except: pass def save_to_csv(items, filepath): """Appends new unique items to the specific CSV file.""" if not items: return 0 new_items = [i for i in items if i['URL'] not in SEEN_URLS] if new_items: df = pd.DataFrame(new_items) if os.path.exists(filepath): df.to_csv(filepath, mode='a', header=False, index=False) else: df.to_csv(filepath, index=False) for i in new_items: SEEN_URLS.add(i['URL']) print(f"βœ… Saved {len(new_items)} new items to {filepath}") return len(new_items) else: print("πŸ’€ No new unique data found.") return 0 # --- MODE 1: LIVE MONITOR (RSS) --- def run_monitor(target, is_user=False): prefix = "u" if is_user else "r" # Reddit RSS format: /r/sub/new.rss OR /user/name/submitted.rss if is_user: rss_url = f"https://www.reddit.com/user/{target}/submitted.rss?limit=100" else: rss_url = f"https://www.reddit.com/r/{target}/new.rss?limit=100" print(f"[{datetime.datetime.now()}] πŸ“‘ Checking RSS Feed for {prefix}/{target}...") try: headers = {"User-Agent": USER_AGENT} response = requests.get(rss_url, headers=headers, timeout=15) if response.status_code != 200: print(f"❌ Blocked/Error (Status {response.status_code})") return root = ET.fromstring(response.content) namespace = {'atom': 'http://www.w3.org/2005/Atom'} posts = [] for entry in root.findall('atom:entry', namespace): posts.append({ "Date": entry.find('atom:published', namespace).text, "Title": entry.find('atom:title', namespace).text, "URL": entry.find('atom:link', namespace).attrib['href'], "Source": "Monitor-RSS" }) save_to_csv(posts, get_file_path(target, prefix)) except Exception as e: print(f"❌ Monitor Error: {e}") # --- MODE 2: HISTORY SCRAPE (Mirrors) --- def run_history(target, limit, is_user=False): prefix = "u" if is_user else "r" print(f"πŸš€ Starting HISTORY scrape for {prefix}/{target} (Target: {limit})...") filepath = get_file_path(target, prefix) load_history(filepath) after = None count = 0 while count < limit: random.shuffle(MIRRORS) success = False for base_url in MIRRORS: try: # Mirror URL format differs slightly for users if is_user: path = f"/user/{target}/submitted.json" else: path = f"/r/{target}/new.json" target_url = f"{base_url}{path}?limit=100" if after: target_url += f"&after={after}" print(f"πŸ“‘ Requesting: {base_url} (Page Token: {after})") response = requests.get(target_url, headers={"User-Agent": USER_AGENT}, timeout=10) if response.status_code == 200: data = response.json() posts = [] for child in data['data']['children']: p = child['data'] date_str = datetime.datetime.fromtimestamp(p['created_utc']).isoformat() posts.append({ "Date": date_str, "Title": p['title'], "URL": p.get('url_overridden_by_dest', p.get('url')), "Source": "History-Mirror" }) saved = save_to_csv(posts, filepath) count += saved after = data['data'].get('after') if not after: print("🏁 Reached end of history.") return success = True break except: continue if not success: print("❌ All mirrors failed. Waiting 30s...") time.sleep(30) else: print(f"⏸️ Cooling down (5s)... Total: {count}") time.sleep(5) # --- CLI ARGS --- if __name__ == "__main__": parser = argparse.ArgumentParser(description="Universal Reddit Scraper (Open Source)") parser.add_argument("target", help="Name of Subreddit (e.g. 'python') or User (e.g. 'spez')") parser.add_argument("--mode", choices=["monitor", "history"], default="monitor", help="Run mode") parser.add_argument("--user", action="store_true", help="Flag if the target is a User, not a Subreddit") parser.add_argument("--limit", type=int, default=500, help="Max posts to scrape (History mode only)") args = parser.parse_args() # Pre-load history for monitor mode if args.mode == "monitor": prefix = "u" if args.user else "r" load_history(get_file_path(args.target, prefix)) print(f"πŸ€– Monitoring {prefix}/{args.target} every 5 mins...") while True: run_monitor(args.target, args.user) time.sleep(300) else: run_history(args.target, args.limit, args.user)