reddipy 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reddipy-1.0.0/LICENSE +21 -0
- reddipy-1.0.0/PKG-INFO +645 -0
- reddipy-1.0.0/README.md +613 -0
- reddipy-1.0.0/pyproject.toml +49 -0
- reddipy-1.0.0/reddipy/__init__.py +4 -0
- reddipy-1.0.0/reddipy/__main__.py +3 -0
- reddipy-1.0.0/reddipy/cli.py +238 -0
- reddipy-1.0.0/reddipy/client.py +254 -0
- reddipy-1.0.0/reddipy/config.py +44 -0
- reddipy-1.0.0/reddipy/exceptions.py +29 -0
- reddipy-1.0.0/reddipy/exporters/__init__.py +5 -0
- reddipy-1.0.0/reddipy/exporters/base.py +9 -0
- reddipy-1.0.0/reddipy/exporters/console.py +96 -0
- reddipy-1.0.0/reddipy/exporters/csv_exporter.py +59 -0
- reddipy-1.0.0/reddipy/exporters/json_exporter.py +24 -0
- reddipy-1.0.0/reddipy/models.py +151 -0
- reddipy-1.0.0/reddipy/scrapers/__init__.py +6 -0
- reddipy-1.0.0/reddipy/scrapers/base.py +16 -0
- reddipy-1.0.0/reddipy/scrapers/post.py +30 -0
- reddipy-1.0.0/reddipy/scrapers/search.py +36 -0
- reddipy-1.0.0/reddipy/scrapers/subreddit.py +29 -0
- reddipy-1.0.0/reddipy/scrapers/user.py +55 -0
- reddipy-1.0.0/reddipy/sdk.py +146 -0
- reddipy-1.0.0/reddipy/utils.py +70 -0
- reddipy-1.0.0/reddipy.egg-info/PKG-INFO +645 -0
- reddipy-1.0.0/reddipy.egg-info/SOURCES.txt +34 -0
- reddipy-1.0.0/reddipy.egg-info/dependency_links.txt +1 -0
- reddipy-1.0.0/reddipy.egg-info/entry_points.txt +2 -0
- reddipy-1.0.0/reddipy.egg-info/requires.txt +8 -0
- reddipy-1.0.0/reddipy.egg-info/top_level.txt +1 -0
- reddipy-1.0.0/setup.cfg +4 -0
- reddipy-1.0.0/tests/test_cli.py +184 -0
- reddipy-1.0.0/tests/test_client.py +367 -0
- reddipy-1.0.0/tests/test_exceptions.py +82 -0
- reddipy-1.0.0/tests/test_models.py +351 -0
- reddipy-1.0.0/tests/test_utils.py +89 -0
reddipy-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Zaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
reddipy-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reddipy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Scrape Reddit posts, comments, and user activity — no API key needed. Python SDK + CLI tool.
|
|
5
|
+
Author: Muhammad Zaid
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/zaidkx37/reddipy
|
|
8
|
+
Project-URL: Repository, https://github.com/zaidkx37/reddipy
|
|
9
|
+
Project-URL: Issues, https://github.com/zaidkx37/reddipy/issues
|
|
10
|
+
Keywords: reddit,scraper,reddit-scraper,reddit-api,posts,comments,subreddit,user-activity,data-extraction,web-scraping,cli,sdk,no-api-key,json,csv,export
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Provides-Extra: socks
|
|
28
|
+
Requires-Dist: requests[socks]; extra == "socks"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
<h1 align="center">Reddipy</h1>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<strong>Scrape Reddit posts, comments, and user activity. Python SDK + CLI.</strong>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="https://pypi.org/project/reddipy/"><img src="https://img.shields.io/pypi/v/reddipy.svg?style=flat-square&color=blue" alt="PyPI version"></a>
|
|
41
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue.svg?style=flat-square" alt="Python versions"></a>
|
|
42
|
+
<a href="https://github.com/zaidkx37/reddipy/actions"><img src="https://img.shields.io/github/actions/workflow/status/zaidkx37/reddipy/ci.yml?style=flat-square&label=CI" alt="CI"></a>
|
|
43
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?style=flat-square" alt="License: MIT"></a>
|
|
44
|
+
<a href="https://pypi.org/project/reddipy/"><img src="https://img.shields.io/pypi/dm/reddipy?style=flat-square&color=orange&label=downloads" alt="Downloads"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
Built on Reddit's public JSON endpoints. Two interfaces: <b>Python SDK</b> and <b>CLI</b>.
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **Search** Reddit by keyword with sort, time, and subreddit filters
|
|
56
|
+
- **Scrape posts** with full comment trees, scores, and metadata
|
|
57
|
+
- **Subreddit browsing** with hot, new, top, rising, and controversial sorting
|
|
58
|
+
- **User profiles** with post/comment activity filtering
|
|
59
|
+
- **Export** to JSON, CSV, or rich console tables
|
|
60
|
+
- **Auto-pagination** to collect beyond the 100-per-request limit
|
|
61
|
+
- **Proxy rotation** with single proxy, proxy file, and dead proxy cooldown
|
|
62
|
+
- **Rate limiting** with configurable delay and exponential backoff
|
|
63
|
+
- **Realistic headers** matching a real Chrome browser fingerprint
|
|
64
|
+
- **Lightweight** with only `requests` and `rich` as dependencies
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
Recommended to **[install using pip](https://pypi.org/project/reddipy/)**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install reddipy
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For SOCKS5 proxy support:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install "reddipy[socks]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Requirements:** Python 3.10+
|
|
83
|
+
|
|
84
|
+
## Authentication
|
|
85
|
+
|
|
86
|
+
Reddipy requires a `reddit_session` cookie from your browser. This is the only cookie needed.
|
|
87
|
+
|
|
88
|
+
### Step 1: Get the Cookie
|
|
89
|
+
|
|
90
|
+
1. Open [reddit.com](https://www.reddit.com) in your browser and log in
|
|
91
|
+
2. Open DevTools (`F12`) > **Application** tab > **Cookies** > `https://www.reddit.com`
|
|
92
|
+
3. Find the cookie named `reddit_session`
|
|
93
|
+
4. Copy its **Value** (a long JWT string starting with `eyJ...`)
|
|
94
|
+
|
|
95
|
+
### Step 2: Configure (One-Time)
|
|
96
|
+
|
|
97
|
+
**SDK:**
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from reddipy import Reddit
|
|
101
|
+
|
|
102
|
+
Reddit.configure(cookie="eyJhbGciOiJS...")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**CLI:**
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
reddipy configure "eyJhbGciOiJS..."
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The cookie is saved to `~/.reddipy/config.json`. You only need to do this once. All future SDK and CLI calls will use it automatically.
|
|
112
|
+
|
|
113
|
+
### Auth Management
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
Reddit.configure(cookie="...") # save cookie
|
|
117
|
+
Reddit.is_configured() # check if cookie exists (returns True/False)
|
|
118
|
+
Reddit.logout() # remove stored cookie
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
reddipy configure "cookie" # save
|
|
123
|
+
reddipy logout # remove
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Override Stored Cookie
|
|
127
|
+
|
|
128
|
+
If you need to use a different cookie for a single session:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
reddit = Reddit(cookie="different_cookie_here")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
reddipy search "python" --cookie "different_cookie_here"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Python SDK
|
|
141
|
+
|
|
142
|
+
### Initialize
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from reddipy import Reddit
|
|
146
|
+
|
|
147
|
+
reddit = Reddit()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
All methods below assume you have already configured authentication.
|
|
151
|
+
|
|
152
|
+
### Search
|
|
153
|
+
|
|
154
|
+
Search Reddit for posts matching a keyword.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Basic search
|
|
158
|
+
posts = reddit.search("machine learning", limit=10)
|
|
159
|
+
for post in posts:
|
|
160
|
+
print(f"[{post.score}] {post.title}")
|
|
161
|
+
print(f" r/{post.subreddit} by {post.author}")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Search within a specific subreddit:**
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
posts = reddit.search("async", subreddit="python", limit=10)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**All parameters:**
|
|
171
|
+
|
|
172
|
+
| Parameter | Type | Default | Description |
|
|
173
|
+
|-----------|------|---------|-------------|
|
|
174
|
+
| `query` | str | required | Search keywords |
|
|
175
|
+
| `limit` | int | 25 | Max results (up to ~1000) |
|
|
176
|
+
| `sort` | str | "relevance" | relevance, hot, new, top, comments |
|
|
177
|
+
| `time_filter` | str | "all" | day, week, month, year, all |
|
|
178
|
+
| `subreddit` | str | None | Scope search to a subreddit |
|
|
179
|
+
|
|
180
|
+
**Returns:** `list[Post]`
|
|
181
|
+
|
|
182
|
+
### Scrape a Post
|
|
183
|
+
|
|
184
|
+
Extract a single post with its full comment tree.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
post, comments = reddit.post("https://www.reddit.com/r/python/comments/abc123/title/")
|
|
188
|
+
|
|
189
|
+
# Post data
|
|
190
|
+
print(post.title)
|
|
191
|
+
print(post.author)
|
|
192
|
+
print(post.score)
|
|
193
|
+
print(post.selftext) # post body
|
|
194
|
+
print(post.flair) # post flair (or None)
|
|
195
|
+
print(post.num_comments)
|
|
196
|
+
print(post.upvote_ratio)
|
|
197
|
+
|
|
198
|
+
# Comments
|
|
199
|
+
print(f"{len(comments)} top-level comments loaded")
|
|
200
|
+
|
|
201
|
+
for comment in comments[:5]:
|
|
202
|
+
print(f"[{comment.score}] {comment.author}: {comment.body[:80]}")
|
|
203
|
+
print(f" depth: {comment.depth}, is_submitter: {comment.is_submitter}")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Accepts any Reddit post URL format:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# All of these work
|
|
210
|
+
reddit.post("https://www.reddit.com/r/python/comments/abc123/title/")
|
|
211
|
+
reddit.post("https://old.reddit.com/r/python/comments/abc123/")
|
|
212
|
+
reddit.post("https://reddit.com/r/python/comments/abc123/title")
|
|
213
|
+
reddit.post("/r/python/comments/abc123/title/") # bare permalink
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Returns:** `tuple[Post, list[Comment]]`
|
|
217
|
+
|
|
218
|
+
### Subreddit Posts
|
|
219
|
+
|
|
220
|
+
Fetch posts from a subreddit with sort and time filters.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# Hot posts (default)
|
|
224
|
+
posts = reddit.subreddit("python")
|
|
225
|
+
|
|
226
|
+
# Top posts this week
|
|
227
|
+
posts = reddit.subreddit("python", sort="top", time_filter="week", limit=50)
|
|
228
|
+
|
|
229
|
+
# New posts
|
|
230
|
+
posts = reddit.subreddit("datascience", sort="new", limit=20)
|
|
231
|
+
|
|
232
|
+
# Rising posts
|
|
233
|
+
posts = reddit.subreddit("technology", sort="rising", limit=10)
|
|
234
|
+
|
|
235
|
+
for post in posts:
|
|
236
|
+
print(f"[{post.score:>5}] {post.title}")
|
|
237
|
+
print(f" {post.num_comments} comments | r/{post.subreddit}")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**All parameters:**
|
|
241
|
+
|
|
242
|
+
| Parameter | Type | Default | Description |
|
|
243
|
+
|-----------|------|---------|-------------|
|
|
244
|
+
| `name` | str | required | Subreddit name (without `r/`) |
|
|
245
|
+
| `sort` | str | "hot" | hot, new, top, rising, controversial |
|
|
246
|
+
| `time_filter` | str | "all" | day, week, month, year, all (for top/controversial) |
|
|
247
|
+
| `limit` | int | 25 | Max posts |
|
|
248
|
+
|
|
249
|
+
**Returns:** `list[Post]`
|
|
250
|
+
|
|
251
|
+
### User Activity
|
|
252
|
+
|
|
253
|
+
Fetch a Reddit user's recent posts and comments.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
# All activity
|
|
257
|
+
activity = reddit.user("spez", limit=10)
|
|
258
|
+
|
|
259
|
+
for item in activity:
|
|
260
|
+
print(f"[{item.type}] r/{item.subreddit} ({item.score} pts)")
|
|
261
|
+
if item.type == "post":
|
|
262
|
+
print(f" {item.title}")
|
|
263
|
+
else:
|
|
264
|
+
print(f" {item.body[:80]}")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Filter by activity type:**
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
# Posts only
|
|
271
|
+
posts = reddit.user("spez", limit=10, activity_type="posts")
|
|
272
|
+
|
|
273
|
+
# Comments only
|
|
274
|
+
comments = reddit.user("spez", limit=10, activity_type="comments")
|
|
275
|
+
|
|
276
|
+
# Both (default)
|
|
277
|
+
all_activity = reddit.user("spez", limit=10, activity_type="all")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**All parameters:**
|
|
281
|
+
|
|
282
|
+
| Parameter | Type | Default | Description |
|
|
283
|
+
|-----------|------|---------|-------------|
|
|
284
|
+
| `username` | str | required | Reddit username (without `u/`) |
|
|
285
|
+
| `limit` | int | 25 | Max items |
|
|
286
|
+
| `sort` | str | "new" | new, hot, top |
|
|
287
|
+
| `activity_type` | str | "all" | posts, comments, all |
|
|
288
|
+
|
|
289
|
+
**Returns:** `list[UserActivity]`
|
|
290
|
+
|
|
291
|
+
### Export Data
|
|
292
|
+
|
|
293
|
+
Save scraped data to JSON, CSV, or display as a formatted table.
|
|
294
|
+
|
|
295
|
+
**JSON:**
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from reddipy.exporters import JSONExporter
|
|
299
|
+
|
|
300
|
+
posts = reddit.search("python", limit=20)
|
|
301
|
+
JSONExporter().export(posts, "results.json")
|
|
302
|
+
|
|
303
|
+
# Or print to stdout (no file path)
|
|
304
|
+
JSONExporter().export(posts)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**CSV:**
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from reddipy.exporters import CSVExporter
|
|
311
|
+
|
|
312
|
+
posts = reddit.subreddit("python", sort="top", limit=50)
|
|
313
|
+
CSVExporter().export(posts, "posts.csv")
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Console table (rich formatted):**
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from reddipy.exporters import ConsoleExporter
|
|
320
|
+
|
|
321
|
+
posts = reddit.search("python", limit=10)
|
|
322
|
+
ConsoleExporter().export(posts)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Exporting comments:**
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
post, comments = reddit.post("https://reddit.com/r/python/comments/abc123/title/")
|
|
329
|
+
|
|
330
|
+
# Comments to JSON (includes post and comment data)
|
|
331
|
+
import json
|
|
332
|
+
combined = {
|
|
333
|
+
"post": post.to_dict(),
|
|
334
|
+
"comments": [c.to_dict() for c in comments]
|
|
335
|
+
}
|
|
336
|
+
with open("post_data.json", "w") as f:
|
|
337
|
+
json.dump(combined, f, indent=2)
|
|
338
|
+
|
|
339
|
+
# Comments to CSV (flattened, with depth/parent_id columns)
|
|
340
|
+
CSVExporter().export(comments, "comments.csv")
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Proxy Support (SDK)
|
|
344
|
+
|
|
345
|
+
Route requests through proxies to distribute load or bypass IP restrictions.
|
|
346
|
+
|
|
347
|
+
**Single proxy:**
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
reddit = Reddit(proxy="http://1.2.3.4:8080")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Multiple proxies with random rotation:**
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
reddit = Reddit(proxy_file="proxies.txt")
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Each request picks a random proxy from the file. Failed proxies are automatically skipped for 60 seconds.
|
|
360
|
+
|
|
361
|
+
**proxies.txt format:**
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
http://1.2.3.4:8080
|
|
365
|
+
http://5.6.7.8:3128
|
|
366
|
+
socks5://9.10.11.12:1080
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
SOCKS5 requires: `pip install "reddipy[socks]"`
|
|
370
|
+
|
|
371
|
+
### Data Models
|
|
372
|
+
|
|
373
|
+
Every scraped item is a Python dataclass with typed fields. All models have a `.to_dict()` method for serialization.
|
|
374
|
+
|
|
375
|
+
**Post**
|
|
376
|
+
|
|
377
|
+
| Field | Type | Description |
|
|
378
|
+
|-------|------|-------------|
|
|
379
|
+
| `id` | str | Reddit post ID |
|
|
380
|
+
| `title` | str | Post title |
|
|
381
|
+
| `author` | str | Author username |
|
|
382
|
+
| `subreddit` | str | Subreddit name |
|
|
383
|
+
| `score` | int | Net upvotes |
|
|
384
|
+
| `upvote_ratio` | float | Upvote percentage (0.0 - 1.0) |
|
|
385
|
+
| `num_comments` | int | Total comment count |
|
|
386
|
+
| `created_utc` | datetime | Post creation time (UTC) |
|
|
387
|
+
| `selftext` | str | Post body text (empty for link posts) |
|
|
388
|
+
| `url` | str | Post URL or linked URL |
|
|
389
|
+
| `permalink` | str | Reddit permalink |
|
|
390
|
+
| `flair` | str or None | Post flair text |
|
|
391
|
+
|
|
392
|
+
**Comment**
|
|
393
|
+
|
|
394
|
+
| Field | Type | Description |
|
|
395
|
+
|-------|------|-------------|
|
|
396
|
+
| `id` | str | Comment ID |
|
|
397
|
+
| `author` | str | Author username |
|
|
398
|
+
| `body` | str | Comment text |
|
|
399
|
+
| `score` | int | Net upvotes |
|
|
400
|
+
| `created_utc` | datetime | Comment creation time (UTC) |
|
|
401
|
+
| `parent_id` | str | Parent post/comment ID |
|
|
402
|
+
| `depth` | int | Nesting depth (0 = top-level) |
|
|
403
|
+
| `is_submitter` | bool | True if comment author is the post author |
|
|
404
|
+
| `replies` | list[Comment] | Nested reply comments |
|
|
405
|
+
|
|
406
|
+
**UserActivity**
|
|
407
|
+
|
|
408
|
+
| Field | Type | Description |
|
|
409
|
+
|-------|------|-------------|
|
|
410
|
+
| `type` | str | "post" or "comment" |
|
|
411
|
+
| `id` | str | Item ID |
|
|
412
|
+
| `author` | str | Username |
|
|
413
|
+
| `subreddit` | str | Subreddit name |
|
|
414
|
+
| `score` | int | Net upvotes |
|
|
415
|
+
| `created_utc` | datetime | Creation time (UTC) |
|
|
416
|
+
| `title` | str or None | Post title (None for comments) |
|
|
417
|
+
| `body` | str or None | Comment body (None for posts) |
|
|
418
|
+
| `permalink` | str | Reddit permalink |
|
|
419
|
+
|
|
420
|
+
**Serialization:**
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
post_dict = post.to_dict()
|
|
424
|
+
# {"id": "abc123", "title": "...", "created_utc": "2026-07-03T12:00:00+00:00", ...}
|
|
425
|
+
|
|
426
|
+
# datetime fields become ISO 8601 strings in to_dict()
|
|
427
|
+
# Nested comment replies are recursively serialized
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Error Handling
|
|
431
|
+
|
|
432
|
+
Reddipy raises specific exceptions you can catch:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from reddipy.exceptions import (
|
|
436
|
+
ReddipyError, # base exception (catch-all)
|
|
437
|
+
SubredditNotFoundError, # subreddit is private, banned, or doesn't exist
|
|
438
|
+
PostNotFoundError, # post URL is invalid or deleted
|
|
439
|
+
UserNotFoundError, # user is suspended or doesn't exist
|
|
440
|
+
RateLimitError, # rate limited after 3 retries with backoff
|
|
441
|
+
NetworkError, # connection failure or timeout
|
|
442
|
+
ProxyError, # all proxies are dead
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
posts = reddit.subreddit("nonexistent_sub_12345")
|
|
447
|
+
except SubredditNotFoundError:
|
|
448
|
+
print("Subreddit doesn't exist")
|
|
449
|
+
except RateLimitError:
|
|
450
|
+
print("Rate limited, try again later or reduce --delay")
|
|
451
|
+
except NetworkError:
|
|
452
|
+
print("Connection failed")
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Advanced Configuration
|
|
456
|
+
|
|
457
|
+
```python
|
|
458
|
+
reddit = Reddit(
|
|
459
|
+
cookie="override_cookie", # override stored cookie
|
|
460
|
+
delay=1.0, # seconds between requests (default: 2.0)
|
|
461
|
+
proxy="http://1.2.3.4:8080", # single proxy
|
|
462
|
+
proxy_file="proxies.txt", # proxy list with random rotation
|
|
463
|
+
user_agent="Custom Agent/1.0", # custom User-Agent string
|
|
464
|
+
verbose=True, # print progress to stderr
|
|
465
|
+
)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## CLI Usage
|
|
471
|
+
|
|
472
|
+
All CLI commands use stored authentication by default. Run `reddipy configure` first.
|
|
473
|
+
|
|
474
|
+
### Search (CLI)
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Basic search
|
|
478
|
+
reddipy search "python"
|
|
479
|
+
|
|
480
|
+
# With filters
|
|
481
|
+
reddipy search "machine learning" --limit 20 --sort top --time week
|
|
482
|
+
|
|
483
|
+
# Search within a subreddit
|
|
484
|
+
reddipy search "flask" --subreddit python --limit 10
|
|
485
|
+
reddipy search "hooks" -s react --sort new
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Post (CLI)
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
# Scrape a post with all comments
|
|
492
|
+
reddipy post "https://www.reddit.com/r/python/comments/abc123/title/"
|
|
493
|
+
|
|
494
|
+
# Export to JSON
|
|
495
|
+
reddipy post "https://reddit.com/r/python/comments/abc123/title/" --export json -o post.json
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Subreddit (CLI)
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
# Hot posts (default)
|
|
502
|
+
reddipy subreddit python
|
|
503
|
+
|
|
504
|
+
# Top posts this month
|
|
505
|
+
reddipy subreddit datascience --sort top --time month --limit 50
|
|
506
|
+
|
|
507
|
+
# New posts
|
|
508
|
+
reddipy subreddit technology --sort new --limit 20
|
|
509
|
+
|
|
510
|
+
# Rising
|
|
511
|
+
reddipy subreddit programming --sort rising
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### User (CLI)
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
# All activity
|
|
518
|
+
reddipy user spez --limit 10
|
|
519
|
+
|
|
520
|
+
# Posts only
|
|
521
|
+
reddipy user spez --type posts --limit 20
|
|
522
|
+
|
|
523
|
+
# Comments only, sorted by top
|
|
524
|
+
reddipy user spez --type comments --sort top
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Export (CLI)
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
# JSON file
|
|
531
|
+
reddipy search "python" --export json -o results.json
|
|
532
|
+
|
|
533
|
+
# CSV file
|
|
534
|
+
reddipy subreddit python --export csv -o posts.csv
|
|
535
|
+
|
|
536
|
+
# Console table (default, no flag needed)
|
|
537
|
+
reddipy search "python" --limit 5
|
|
538
|
+
|
|
539
|
+
# Auto-generated filename (omit -o)
|
|
540
|
+
reddipy search "python" --export json
|
|
541
|
+
# Creates: reddipy_search_20260703_143000.json
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Proxy Support (CLI)
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
# Single proxy
|
|
548
|
+
reddipy search "python" --proxy http://1.2.3.4:8080
|
|
549
|
+
|
|
550
|
+
# Multiple proxies with random rotation
|
|
551
|
+
reddipy search "python" --proxy-file proxies.txt
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**proxies.txt** (one proxy per line):
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
http://1.2.3.4:8080
|
|
558
|
+
http://5.6.7.8:3128
|
|
559
|
+
socks5://9.10.11.12:1080
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
SOCKS5 requires: `pip install "reddipy[socks]"`
|
|
563
|
+
|
|
564
|
+
### All CLI Flags
|
|
565
|
+
|
|
566
|
+
**Common flags (available on all commands):**
|
|
567
|
+
|
|
568
|
+
| Flag | Description | Default |
|
|
569
|
+
|------|-------------|---------|
|
|
570
|
+
| `--limit` | Max number of results | 25 |
|
|
571
|
+
| `--sort` | Sort order | varies by command |
|
|
572
|
+
| `--time` / `-t` | Time filter (day/week/month/year/all) | all |
|
|
573
|
+
| `--export` | Output format (json/csv/console) | console |
|
|
574
|
+
| `--output` / `-o` | Output file path | auto-generated |
|
|
575
|
+
| `--cookie` | Override stored reddit_session cookie | stored config |
|
|
576
|
+
| `--cookie-file` | Path to JSON file with cookies | - |
|
|
577
|
+
| `--proxy` | Single proxy URL | - |
|
|
578
|
+
| `--proxy-file` | Path to proxy list file | - |
|
|
579
|
+
| `--delay` | Seconds between requests | 2.0 |
|
|
580
|
+
| `--user-agent` | Custom User-Agent header | Chrome 149 |
|
|
581
|
+
| `--verbose` / `-v` | Show progress and retry info | false |
|
|
582
|
+
|
|
583
|
+
**Command-specific flags:**
|
|
584
|
+
|
|
585
|
+
| Command | Flag | Description | Default |
|
|
586
|
+
|---------|------|-------------|---------|
|
|
587
|
+
| `search` | `--subreddit` / `-s` | Scope search to a subreddit | all of Reddit |
|
|
588
|
+
| `user` | `--type` | Filter activity (posts/comments/all) | all |
|
|
589
|
+
|
|
590
|
+
**Exit codes:**
|
|
591
|
+
|
|
592
|
+
| Code | Meaning |
|
|
593
|
+
|------|---------|
|
|
594
|
+
| 0 | Success |
|
|
595
|
+
| 1 | General error (invalid args, unexpected failure) |
|
|
596
|
+
| 2 | Network error (connection failed, rate limited, proxies dead) |
|
|
597
|
+
| 3 | Not found (subreddit/post/user doesn't exist) |
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Rate Limiting
|
|
602
|
+
|
|
603
|
+
Reddipy has built-in protections to avoid getting blocked:
|
|
604
|
+
|
|
605
|
+
- **Request delay:** 2-second gap between requests (configurable with `--delay` or `delay=` parameter)
|
|
606
|
+
- **Cooldown on 429:** Automatic exponential backoff (2s, 4s, 8s) when rate-limited by Reddit
|
|
607
|
+
- **Max retries:** 3 attempts with increasing wait times before raising an error
|
|
608
|
+
- **Dead proxy cooldown:** Failed proxies are skipped for 60 seconds before being retried
|
|
609
|
+
- **Realistic headers:** All requests include 14 browser-standard headers (Accept, Sec-CH-UA, Sec-Fetch-*, etc.) matching a real Chrome browser fingerprint
|
|
610
|
+
|
|
611
|
+
**Tips for staying safe:**
|
|
612
|
+
|
|
613
|
+
- Keep `delay` at 2.0 or higher for normal use
|
|
614
|
+
- Use `--verbose` to see when retries and cooldowns happen
|
|
615
|
+
- For large scrapes (500+ items), use proxies to distribute load
|
|
616
|
+
- Don't run multiple instances on the same account simultaneously
|
|
617
|
+
|
|
618
|
+
## Project Structure
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
reddipy/
|
|
622
|
+
__init__.py # Package exports: Reddit, Post, Comment, UserActivity
|
|
623
|
+
sdk.py # Python SDK (Reddit class)
|
|
624
|
+
cli.py # CLI entry point (argparse subcommands)
|
|
625
|
+
client.py # HTTP client (headers, rate limiting, pagination, proxies)
|
|
626
|
+
config.py # Persistent auth (~/.reddipy/config.json)
|
|
627
|
+
models.py # Post, Comment, UserActivity dataclasses
|
|
628
|
+
exceptions.py # ReddipyError, SubredditNotFoundError, etc.
|
|
629
|
+
utils.py # URL parsing, timestamp conversion, filename generation
|
|
630
|
+
scrapers/
|
|
631
|
+
base.py # BaseScraper abstract class
|
|
632
|
+
search.py # SearchScraper
|
|
633
|
+
post.py # PostScraper
|
|
634
|
+
subreddit.py # SubredditScraper
|
|
635
|
+
user.py # UserScraper
|
|
636
|
+
exporters/
|
|
637
|
+
base.py # BaseExporter abstract class
|
|
638
|
+
json_exporter.py
|
|
639
|
+
csv_exporter.py
|
|
640
|
+
console.py # Rich formatted tables
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## License
|
|
644
|
+
|
|
645
|
+
MIT
|