-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdriver.py
More file actions
141 lines (117 loc) · 4.7 KB
/
Copy pathdriver.py
File metadata and controls
141 lines (117 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""Reddit User Scrapper — driver that runs all subtools for one username.
Calls each subtool's in-memory entry point (no temp files between tools),
holds the results in memory, and writes a single combined JSON:
{
"username", "generated_at", "counts", "errors",
"profile": { ... profile fields ... },
"activity": [ {subreddit, author, score, created_iso, permalink, type}, ... ]
}
The activity section merges the user's comments (Playwright SPA + old.reddit,
deduped by comment id) and posts (old.reddit), all sharing the 5-field schema
plus a `type` field = "comment" or "post". Sorted newest-first.
Usage:
python driver.py <username> [options]
Options:
--out PATH output JSON (default: <username>_full.json)
--skip-spa skip the slow Playwright comment scraper
--scrolls N SPA scroll iterations (default: 30)
--pages N old.reddit pages for comments AND posts (default: 10)
"""
import argparse
import sys
import time
from datetime import datetime, timezone
from reddit_common import write_json, setup_stdout
from new_comment_scraper import scrape_user_comments
from old_comment_scraper import scrape_user_comments_oldreddit
from old_post_scraper import scrape_user_posts
from user_profile_scraper import scrape_user_profile
from merge_comments import merge, FIELDS
NAME = "Reddit User Scrapper"
def _sort_key(rec):
iso = rec.get("created_iso")
try:
return datetime.fromisoformat(iso)
except (TypeError, ValueError):
return datetime.min.replace(tzinfo=timezone.utc)
def _run(label, fn, errors):
"""Call a subtool, timing it; on failure record the error and return None."""
t0 = time.time()
try:
result = fn()
n = len(result) if isinstance(result, list) else 1
print(f" {label:22} {n} ({time.time() - t0:.1f}s)", file=sys.stderr)
return result
except Exception as e: # one failing source must not sink the whole run
msg = f"{type(e).__name__}: {e}"
print(f" {label:22} FAILED — {msg}", file=sys.stderr)
errors.append({"source": label, "error": msg})
return None
def scrape_user(username, skip_spa=False, scrolls=30, pages=10):
"""Run every subtool for `username` and assemble the combined record."""
errors = []
print(f"scraping {username} ...", file=sys.stderr)
profile = _run("profile", lambda: scrape_user_profile(username), errors)
posts = _run("posts (old.reddit)", lambda: scrape_user_posts(username, pages=pages), errors)
old_comments = _run(
"comments (old.reddit)",
lambda: scrape_user_comments_oldreddit(username, pages=pages),
errors,
)
spa_comments = None
if not skip_spa:
spa_comments = _run(
"comments (SPA)", lambda: scrape_user_comments(username, scrolls=scrolls), errors
)
# Merge comment sources by comment id, then tag type.
comment_sources = [
(lbl, rows)
for lbl, rows in (("oldreddit", old_comments), ("spa", spa_comments))
if rows
]
merged_comments, _stats = merge(comment_sources) if comment_sources else ([], {})
for c in merged_comments:
c["type"] = "comment"
tagged_posts = []
for p in posts or []:
rec = {k: p.get(k) for k in FIELDS}
rec["type"] = "post"
tagged_posts.append(rec)
activity = sorted(merged_comments + tagged_posts, key=_sort_key, reverse=True)
canonical = (profile or {}).get("username") or username
return {
"username": canonical,
"generated_at": datetime.now(timezone.utc).isoformat(),
"counts": {
"comments": len(merged_comments),
"posts": len(tagged_posts),
"activity_total": len(activity),
},
"errors": errors,
"profile": profile,
"activity": activity,
}
def main():
setup_stdout()
ap = argparse.ArgumentParser(description="Run all Reddit subtools for one username.")
ap.add_argument("username", nargs="?", default="spez")
ap.add_argument("--out")
ap.add_argument("--skip-spa", action="store_true")
ap.add_argument("--scrolls", type=int, default=30)
ap.add_argument("--pages", type=int, default=10)
args = ap.parse_args()
t0 = time.time()
data = scrape_user(
args.username, skip_spa=args.skip_spa, scrolls=args.scrolls, pages=args.pages
)
out = args.out or f"{args.username}_full.json"
write_json(data, out)
c = data["counts"]
print(
f"done in {time.time() - t0:.1f}s -> {out} "
f"(profile {'ok' if data['profile'] else 'MISSING'}, "
f"{c['comments']} comments + {c['posts']} posts = {c['activity_total']})",
file=sys.stderr,
)
if __name__ == "__main__":
main()