reddit-universal-scraper/main.py

190 lines
6.6 KiB
Python

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)