- Add utils/log_rotation.py: safe_imwrite() auto-rotates screenshot dirs when they exceed configured file count or size limits - Add [log_rotation] config section in params.ini (pickit/info/items max_files, max_mb, discord_notify_rotation) - Replace 40 cv2.imwrite() calls with safe_imwrite() across 13 files - Add failure reason tracking: bot.py catches run exceptions, stores reason in game_stats, Discord message includes the error - Add Discord notification when log rotation deletes old files - Prevents disk-full crashes that stopped botty on May 30
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""
|
|
Playwright-based headless scraper for d2jsp FG market forum.
|
|
Saves HTML files to data/d2jsp_pages/ for the offline scraper to process.
|
|
"""
|
|
import argparse
|
|
import http.cookiejar
|
|
import os
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
BASE_URL = "https://forums.d2jsp.org"
|
|
FORUM_PATH = "/forum.php?f=271"
|
|
|
|
BROWSER_HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
|
"Accept-Language": "en-DK,en;q=0.9,da-DK;q=0.8,da;q=0.7,en-GB;q=0.6,en-US;q=0.5",
|
|
}
|
|
|
|
|
|
def sanitize_filename(name: str) -> str:
|
|
"""Make a safe filename from a topic title."""
|
|
bad = '<>:"/\\|?*'
|
|
for c in bad:
|
|
name = name.replace(c, "_")
|
|
return name[:120]
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Scrape d2jsp FG market forum and save HTML files.")
|
|
parser.add_argument("--max-forum-pages", type=int, default=40, help="Forum pages to scan (25 topics/page)")
|
|
parser.add_argument("--max-topics", type=int, default=800, help="Max topic pages to save")
|
|
parser.add_argument("--delay-s", type=float, default=0.5, help="Delay between requests")
|
|
parser.add_argument("--out-dir", default="data/d2jsp_pages", help="Output directory for HTML files")
|
|
parser.add_argument("--cookie-path", default="cookies.txt", help="Path to Netscape cookie jar")
|
|
args = parser.parse_args()
|
|
|
|
out_dir = Path(args.out_dir)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
context = browser.new_context(
|
|
user_agent=BROWSER_HEADERS["User-Agent"],
|
|
viewport={"width": 1920, "height": 1080},
|
|
locale="en-DK",
|
|
)
|
|
|
|
# Load cookies if available
|
|
if Path(args.cookie_path).exists():
|
|
cj = http.cookiejar.MozillaCookieJar(args.cookie_path)
|
|
cj.load(ignore_discard=True, ignore_expires=True)
|
|
for cookie in cj:
|
|
context.add_cookies([{
|
|
"name": cookie.name,
|
|
"value": cookie.value,
|
|
"domain": cookie.domain,
|
|
"path": cookie.path,
|
|
}])
|
|
|
|
page = context.new_page()
|
|
|
|
# Warm up
|
|
try:
|
|
page.goto(f"{BASE_URL}/", timeout=20000, wait_until="domcontentloaded")
|
|
except Exception:
|
|
pass
|
|
|
|
topic_urls = []
|
|
for page_idx in range(args.max_forum_pages):
|
|
offset = page_idx * 25
|
|
url = f"{BASE_URL}{FORUM_PATH}&st={offset}"
|
|
try:
|
|
page.goto(url, timeout=20000, wait_until="domcontentloaded")
|
|
except Exception:
|
|
print(f"Failed to load forum page {page_idx}, stopping.")
|
|
break
|
|
|
|
# Check for 403 / Cloudflare
|
|
content = page.content()
|
|
if "Access denied" in content or "Just a moment" in content:
|
|
print("Cloudflare/403 detected, stopping forum scan.")
|
|
break
|
|
|
|
# Extract topic links using same regex as fg_market_scraper.py
|
|
links = re.findall(r'href=\"((?:/)?topic\.php\?t=\d+[^\"]*)\"', content)
|
|
seen = set()
|
|
for link in links:
|
|
# Normalize: strip leading slash, remove &v=1
|
|
norm = link.lstrip("/")
|
|
norm = re.sub(r'&v=\d+', '', norm)
|
|
if norm not in seen:
|
|
seen.add(norm)
|
|
full = f"{BASE_URL}/{norm}"
|
|
if full not in topic_urls:
|
|
topic_urls.append(full)
|
|
|
|
time.sleep(args.delay_s)
|
|
if len(topic_urls) >= args.max_topics:
|
|
break
|
|
|
|
print(f"Found {len(topic_urls)} topic URLs. Saving HTML files...")
|
|
|
|
saved = 0
|
|
skipped = 0
|
|
for i, topic_url in enumerate(topic_urls):
|
|
try:
|
|
page.goto(topic_url, timeout=20000, wait_until="domcontentloaded")
|
|
html = page.content()
|
|
|
|
# Check for Cloudflare challenge
|
|
if "Just a moment" in html or "Access denied" in html:
|
|
print(f" [{i+1}/{len(topic_urls)}] Cloudflare blocked {topic_url}")
|
|
skipped += 1
|
|
continue
|
|
|
|
# Get the topic title for filename
|
|
title_el = page.query_selector("h1")
|
|
title = title_el.inner_text().strip() if title_el else f"topic_{i}"
|
|
filename = f"[offering] {title} - Topic - d2jsp.html"
|
|
filename = sanitize_filename(filename)
|
|
|
|
filepath = out_dir / filename
|
|
filepath.write_text(html, encoding="utf-8")
|
|
saved += 1
|
|
print(f" [{i+1}/{len(topic_urls)}] Saved: {filename}")
|
|
|
|
except Exception as e:
|
|
print(f" [{i+1}/{len(topic_urls)}] Error saving {topic_url}: {e}")
|
|
skipped += 1
|
|
|
|
time.sleep(args.delay_s)
|
|
|
|
browser.close()
|
|
|
|
print(f"\nDone! Saved {saved} topics, skipped {skipped}.")
|
|
print(f"Files in {out_dir}")
|
|
print("Now run: python tools/fg_market_scraper.py --ladder-start-date YYYY-MM-DD --days 14 --offline-dir data/d2jsp_pages")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|