mrscraper-sdk 0.1.0__py3-none-any.whl
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.
mrscraper/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MrScraper Python SDK
|
|
3
|
+
|
|
4
|
+
A clean Python client for the `MrScraper <https://mrscraper.com>`_ API.
|
|
5
|
+
Supports async/await usage.
|
|
6
|
+
|
|
7
|
+
Quickstart::
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from mrscraper import MrScraper
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
client = MrScraper(token="atk_your_token_here")
|
|
14
|
+
result = await client.fetch_html("https://example.com")
|
|
15
|
+
print(result["data"])
|
|
16
|
+
|
|
17
|
+
asyncio.run(main())
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .mrscraper import MrScraper
|
|
21
|
+
from .exceptions import APIError, AuthenticationError, MrScraperError, NetworkError
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"MrScraper",
|
|
25
|
+
"MrScraperError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"APIError",
|
|
28
|
+
"NetworkError",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
mrscraper/exceptions.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Custom exceptions for the MrScraper SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MrScraperError(Exception):
|
|
7
|
+
"""Base exception for all MrScraper SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationError(MrScraperError):
|
|
11
|
+
"""Raised when the API token is invalid or missing."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIError(MrScraperError):
|
|
15
|
+
"""Raised when the MrScraper API returns an error response."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NetworkError(MrScraperError):
|
|
23
|
+
"""Raised when a network-level error occurs (timeout, connection failure, etc.)."""
|
mrscraper/mrscraper.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Async client for the MrScraper API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .exceptions import APIError, AuthenticationError, NetworkError
|
|
11
|
+
|
|
12
|
+
_FETCH_BASE_URL = "https://api.mrscraper.com"
|
|
13
|
+
_API_BASE_URL = "https://api.app.mrscraper.com/api/v1"
|
|
14
|
+
|
|
15
|
+
_DEFAULT_TIMEOUT = 120
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MrScraper:
|
|
19
|
+
"""
|
|
20
|
+
Async client for the MrScraper API.
|
|
21
|
+
|
|
22
|
+
All methods are coroutines and must be awaited.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
token: Your MrScraper API token. Get yours at https://app.mrscraper.com.
|
|
26
|
+
http_client: Optional custom ``httpx.AsyncClient`` instance. If not
|
|
27
|
+
provided a new one is created per request.
|
|
28
|
+
|
|
29
|
+
Example::
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
from mrscraper import AsyncMrScraper
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
client = AsyncMrScraper(token="atk_...")
|
|
36
|
+
html = await client.fetch_html("https://example.com")
|
|
37
|
+
print(html["data"])
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
token: str,
|
|
45
|
+
*,
|
|
46
|
+
http_client: httpx.AsyncClient | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._token = token
|
|
49
|
+
self._http_client = http_client
|
|
50
|
+
|
|
51
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
52
|
+
return {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"accept": "application/json",
|
|
55
|
+
"x-api-token": self._token,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def _client(self) -> httpx.AsyncClient:
|
|
59
|
+
if self._http_client is not None:
|
|
60
|
+
return self._http_client
|
|
61
|
+
return httpx.AsyncClient()
|
|
62
|
+
|
|
63
|
+
async def _get(
|
|
64
|
+
self,
|
|
65
|
+
url: str,
|
|
66
|
+
*,
|
|
67
|
+
headers: dict[str, str] | None = None,
|
|
68
|
+
timeout: float = float(_DEFAULT_TIMEOUT + 30),
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
async with self._client() as client:
|
|
71
|
+
try:
|
|
72
|
+
response = await client.get(url, headers=headers, timeout=timeout)
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
return self._parse(response)
|
|
75
|
+
except httpx.HTTPStatusError as exc:
|
|
76
|
+
self._raise_for_status(exc)
|
|
77
|
+
raise # unreachable; satisfies type checker
|
|
78
|
+
except httpx.HTTPError as exc:
|
|
79
|
+
raise NetworkError(str(exc)) from exc
|
|
80
|
+
|
|
81
|
+
async def _post(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
*,
|
|
85
|
+
payload: dict[str, Any],
|
|
86
|
+
timeout: float = 600.0,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
async with self._client() as client:
|
|
89
|
+
try:
|
|
90
|
+
response = await client.post(
|
|
91
|
+
url,
|
|
92
|
+
headers=self._auth_headers(),
|
|
93
|
+
json=payload,
|
|
94
|
+
timeout=timeout,
|
|
95
|
+
)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
return self._parse(response)
|
|
98
|
+
except httpx.HTTPStatusError as exc:
|
|
99
|
+
self._raise_for_status(exc)
|
|
100
|
+
raise # unreachable; satisfies type checker
|
|
101
|
+
except httpx.HTTPError as exc:
|
|
102
|
+
raise NetworkError(str(exc)) from exc
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _parse(response: httpx.Response) -> dict[str, Any]:
|
|
106
|
+
if response.status_code == 401:
|
|
107
|
+
raise AuthenticationError(
|
|
108
|
+
"Unauthorized or invalid token. "
|
|
109
|
+
"Please go to https://app.mrscraper.com to get your token."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
113
|
+
data: Any = response.json() if "application/json" in content_type else response.text
|
|
114
|
+
return {
|
|
115
|
+
"status_code": response.status_code,
|
|
116
|
+
"data": data,
|
|
117
|
+
"headers": dict(response.headers),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _raise_for_status(exc: httpx.HTTPStatusError) -> None:
|
|
122
|
+
status_code = exc.response.status_code
|
|
123
|
+
if status_code == 401:
|
|
124
|
+
raise AuthenticationError(
|
|
125
|
+
"Unauthorized or invalid token. "
|
|
126
|
+
"Please go to https://app.mrscraper.com to get your token."
|
|
127
|
+
) from exc
|
|
128
|
+
raise APIError(str(exc), status_code=status_code) from exc
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Public API
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async def fetch_html(
|
|
135
|
+
self,
|
|
136
|
+
url: str,
|
|
137
|
+
*,
|
|
138
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
139
|
+
geo_code: str = "US",
|
|
140
|
+
block_resources: bool = False,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Fetch the rendered HTML of a page via the MrScraper stealth browser.
|
|
144
|
+
|
|
145
|
+
The service handles JavaScript rendering, bot detection evasion, and
|
|
146
|
+
optional geolocation proxying for you.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
url: Target URL to scrape (e.g. ``"https://example.com/page"``).
|
|
150
|
+
timeout: Maximum seconds to wait for the page to load (default 120).
|
|
151
|
+
geo_code: ISO country code for proxy-based geolocation (default ``"US"``).
|
|
152
|
+
Examples: ``"US"``, ``"GB"``, ``"ID"``, ``"SG"``.
|
|
153
|
+
block_resources: When ``True``, images, CSS, and fonts are blocked to
|
|
154
|
+
speed up the request (default ``False``).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
A dict with keys ``status_code``, ``data`` (raw HTML or JSON), and
|
|
158
|
+
``headers``.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
AuthenticationError: If the API token is invalid.
|
|
162
|
+
APIError: If the API returns a non-2xx error.
|
|
163
|
+
NetworkError: If a connection or timeout error occurs.
|
|
164
|
+
|
|
165
|
+
Example::
|
|
166
|
+
|
|
167
|
+
result = await client.fetch_html(
|
|
168
|
+
"https://stockx.com/air-jordan-1-retro-low-og-chicago-2025",
|
|
169
|
+
geo_code="US",
|
|
170
|
+
timeout=120,
|
|
171
|
+
)
|
|
172
|
+
print(result["data"]) # raw HTML string
|
|
173
|
+
"""
|
|
174
|
+
params = {
|
|
175
|
+
"token": self._token,
|
|
176
|
+
"timeout": timeout,
|
|
177
|
+
"geoCode": geo_code,
|
|
178
|
+
"url": url,
|
|
179
|
+
"blockResources": str(block_resources).lower(),
|
|
180
|
+
}
|
|
181
|
+
full_url = f"{_FETCH_BASE_URL}?{urlencode(params)}"
|
|
182
|
+
return await self._get(full_url, timeout=float(timeout + 30))
|
|
183
|
+
|
|
184
|
+
async def create_scraper(
|
|
185
|
+
self,
|
|
186
|
+
url: str,
|
|
187
|
+
message: str,
|
|
188
|
+
*,
|
|
189
|
+
agent: Literal["general", "listing", "map"] = "general",
|
|
190
|
+
proxy_country: str | None = None,
|
|
191
|
+
max_depth: int = 2,
|
|
192
|
+
max_pages: int = 50,
|
|
193
|
+
limit: int = 1000,
|
|
194
|
+
include_patterns: str = "",
|
|
195
|
+
exclude_patterns: str = "",
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""
|
|
198
|
+
Create an AI-powered scraper and run it immediately.
|
|
199
|
+
|
|
200
|
+
The scraper uses natural-language instructions to understand the page
|
|
201
|
+
structure and extract the requested data automatically — no CSS
|
|
202
|
+
selectors required.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
url: Target URL to scrape.
|
|
206
|
+
message: Natural-language description of what to extract.
|
|
207
|
+
Examples: ``"Extract all product names and prices"``,
|
|
208
|
+
``"Get article titles and publication dates"``.
|
|
209
|
+
agent: AI agent type (default ``"general"``).
|
|
210
|
+
|
|
211
|
+
* ``"general"`` — works on almost any page; best default.
|
|
212
|
+
* ``"listing"`` — optimised for listing/grid pages.
|
|
213
|
+
* ``"map"`` — crawls all sub-pages of a site.
|
|
214
|
+
proxy_country: ISO country code for proxy selection (optional).
|
|
215
|
+
Examples: ``"US"``, ``"GB"``, ``"SG"``.
|
|
216
|
+
max_depth: *(map agent only)* Crawl depth from the start URL
|
|
217
|
+
(default 2). 0 = start URL only.
|
|
218
|
+
max_pages: *(map agent only)* Maximum pages to process (default 50).
|
|
219
|
+
limit: *(map agent only)* Maximum records to extract (default 1000).
|
|
220
|
+
include_patterns: *(map agent only)* ``||``-separated URL regex
|
|
221
|
+
patterns to include when following links.
|
|
222
|
+
exclude_patterns: *(map agent only)* ``||``-separated URL regex
|
|
223
|
+
patterns to skip when following links.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A dict with keys ``status_code``, ``data`` (scraper info including
|
|
227
|
+
the scraper ID), and ``headers``.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
AuthenticationError: If the API token is invalid.
|
|
231
|
+
APIError: If the API returns a non-2xx error.
|
|
232
|
+
NetworkError: If a connection or timeout error occurs.
|
|
233
|
+
|
|
234
|
+
Example::
|
|
235
|
+
|
|
236
|
+
result = await client.create_scraper(
|
|
237
|
+
"https://example.com/products",
|
|
238
|
+
"Extract all product names, prices, and ratings",
|
|
239
|
+
agent="listing",
|
|
240
|
+
proxy_country="US",
|
|
241
|
+
)
|
|
242
|
+
scraper_id = result["data"]["id"]
|
|
243
|
+
"""
|
|
244
|
+
endpoint = f"{_API_BASE_URL}/scrapers-ai"
|
|
245
|
+
|
|
246
|
+
if agent in ("general", "listing"):
|
|
247
|
+
payload: dict[str, Any] = {
|
|
248
|
+
"url": url,
|
|
249
|
+
"message": message,
|
|
250
|
+
"agent": agent,
|
|
251
|
+
"proxyCountry": proxy_country,
|
|
252
|
+
}
|
|
253
|
+
else: # map
|
|
254
|
+
payload = {
|
|
255
|
+
"url": url,
|
|
256
|
+
"agent": agent,
|
|
257
|
+
"maxDepth": max_depth,
|
|
258
|
+
"maxPages": max_pages,
|
|
259
|
+
"limit": limit,
|
|
260
|
+
"includePatterns": include_patterns,
|
|
261
|
+
"excludePatterns": exclude_patterns,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return await self._post(endpoint, payload=payload)
|
|
265
|
+
|
|
266
|
+
async def rerun_scraper(
|
|
267
|
+
self,
|
|
268
|
+
scraper_id: str,
|
|
269
|
+
url: str,
|
|
270
|
+
*,
|
|
271
|
+
max_depth: int = 2,
|
|
272
|
+
max_pages: int = 50,
|
|
273
|
+
limit: int = 1000,
|
|
274
|
+
include_patterns: str = "",
|
|
275
|
+
exclude_patterns: str = "",
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""
|
|
278
|
+
Rerun an existing AI scraper on a (new) URL.
|
|
279
|
+
|
|
280
|
+
This lets you reuse the extraction logic from a previously created
|
|
281
|
+
scraper on any compatible URL.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
scraper_id: ID of the scraper to rerun (from :meth:`create_scraper`).
|
|
285
|
+
url: Target URL. Can be the original URL or a different page.
|
|
286
|
+
max_depth: *(map agent only)* Crawl depth (default 2).
|
|
287
|
+
max_pages: *(map agent only)* Maximum pages (default 50).
|
|
288
|
+
limit: *(map agent only)* Maximum records (default 1000).
|
|
289
|
+
include_patterns: *(map agent only)* ``||``-separated include patterns.
|
|
290
|
+
exclude_patterns: *(map agent only)* ``||``-separated exclude patterns.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
A dict with keys ``status_code``, ``data``, and ``headers``.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
AuthenticationError: If the API token is invalid.
|
|
297
|
+
APIError: If the API returns a non-2xx error.
|
|
298
|
+
NetworkError: If a connection or timeout error occurs.
|
|
299
|
+
|
|
300
|
+
Example::
|
|
301
|
+
|
|
302
|
+
result = await client.rerun_scraper(
|
|
303
|
+
scraper_id="scraper_12345",
|
|
304
|
+
url="https://example.com/category/electronics",
|
|
305
|
+
max_depth=3,
|
|
306
|
+
max_pages=100,
|
|
307
|
+
)
|
|
308
|
+
"""
|
|
309
|
+
endpoint = f"{_API_BASE_URL}/scrapers-ai-rerun"
|
|
310
|
+
payload: dict[str, Any] = {
|
|
311
|
+
"scraperId": scraper_id,
|
|
312
|
+
"url": url,
|
|
313
|
+
"maxDepth": max_depth,
|
|
314
|
+
"maxPages": max_pages,
|
|
315
|
+
"limit": limit,
|
|
316
|
+
"includePatterns": include_patterns,
|
|
317
|
+
"excludePatterns": exclude_patterns,
|
|
318
|
+
}
|
|
319
|
+
return await self._post(endpoint, payload=payload)
|
|
320
|
+
|
|
321
|
+
async def bulk_rerun_scraper(
|
|
322
|
+
self,
|
|
323
|
+
scraper_id: str,
|
|
324
|
+
urls: list[str],
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
"""
|
|
327
|
+
Rerun an existing AI scraper on multiple URLs in a single batch request.
|
|
328
|
+
|
|
329
|
+
More efficient than calling :meth:`rerun_scraper` in a loop because all
|
|
330
|
+
URLs are dispatched in parallel server-side.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
scraper_id: ID of the scraper to rerun (from :meth:`create_scraper`).
|
|
334
|
+
urls: List of target URLs. Must contain at least one URL.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
A dict with keys ``status_code``, ``data``, and ``headers``.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
AuthenticationError: If the API token is invalid.
|
|
341
|
+
APIError: If the API returns a non-2xx error.
|
|
342
|
+
NetworkError: If a connection or timeout error occurs.
|
|
343
|
+
|
|
344
|
+
Example::
|
|
345
|
+
|
|
346
|
+
result = await client.bulk_rerun_scraper(
|
|
347
|
+
scraper_id="scraper_12345",
|
|
348
|
+
urls=[
|
|
349
|
+
"https://example.com/products/item1",
|
|
350
|
+
"https://example.com/products/item2",
|
|
351
|
+
],
|
|
352
|
+
)
|
|
353
|
+
"""
|
|
354
|
+
endpoint = f"{_API_BASE_URL}/scrapers-ai-rerun/bulk"
|
|
355
|
+
payload: dict[str, Any] = {
|
|
356
|
+
"scraperId": scraper_id,
|
|
357
|
+
"urls": urls,
|
|
358
|
+
}
|
|
359
|
+
return await self._post(endpoint, payload=payload)
|
|
360
|
+
|
|
361
|
+
async def rerun_manual_scraper(
|
|
362
|
+
self,
|
|
363
|
+
scraper_id: str,
|
|
364
|
+
url: str,
|
|
365
|
+
) -> dict[str, Any]:
|
|
366
|
+
"""
|
|
367
|
+
Rerun a manually configured scraper (created via the MrScraper dashboard)
|
|
368
|
+
on a new URL.
|
|
369
|
+
|
|
370
|
+
Use this for scrapers built with custom CSS selectors or XPath rules, not
|
|
371
|
+
for AI scrapers created with :meth:`create_scraper`.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
scraper_id: ID of the manual scraper (found in the MrScraper dashboard).
|
|
375
|
+
url: Target URL. The page structure should match the original scraper's
|
|
376
|
+
target for selectors to work correctly.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
A dict with keys ``status_code``, ``data``, and ``headers``.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
AuthenticationError: If the API token is invalid.
|
|
383
|
+
APIError: If the API returns a non-2xx error.
|
|
384
|
+
NetworkError: If a connection or timeout error occurs.
|
|
385
|
+
|
|
386
|
+
Example::
|
|
387
|
+
|
|
388
|
+
result = await client.rerun_manual_scraper(
|
|
389
|
+
scraper_id="manual_scraper_67890",
|
|
390
|
+
url="https://example.com/products/new-item",
|
|
391
|
+
)
|
|
392
|
+
"""
|
|
393
|
+
endpoint = f"{_API_BASE_URL}/scrapers-manual-rerun"
|
|
394
|
+
payload: dict[str, Any] = {
|
|
395
|
+
"scraperId": scraper_id,
|
|
396
|
+
"url": url,
|
|
397
|
+
}
|
|
398
|
+
return await self._post(endpoint, payload=payload)
|
|
399
|
+
|
|
400
|
+
async def bulk_rerun_manual_scraper(
|
|
401
|
+
self,
|
|
402
|
+
scraper_id: str,
|
|
403
|
+
urls: list[str],
|
|
404
|
+
) -> dict[str, Any]:
|
|
405
|
+
"""
|
|
406
|
+
Rerun a manually configured scraper on multiple URLs in a single batch.
|
|
407
|
+
|
|
408
|
+
More efficient than calling :meth:`rerun_manual_scraper` multiple times,
|
|
409
|
+
as all URLs are processed in parallel server-side. Ideal for scraping
|
|
410
|
+
multiple pages, products, or articles with the same extraction logic.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
scraper_id: ID of the manual scraper (from the MrScraper dashboard).
|
|
414
|
+
Must be a scraper created manually via the web interface,
|
|
415
|
+
not an AI scraper. Find it at https://app.mrscraper.com.
|
|
416
|
+
urls: List of target URLs to scrape. Must contain at least one URL.
|
|
417
|
+
Each URL is processed independently using the scraper's logic.
|
|
418
|
+
Example: ``["https://example.com/page1", "https://example.com/page2"]``.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
A dict with keys ``status_code``, ``data`` (bulk job info including job ID,
|
|
422
|
+
status, metadata; use :meth:`get_all_results` or :meth:`get_result_by_id`
|
|
423
|
+
to fetch per-URL results), and ``headers``.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
AuthenticationError: If the API token is invalid.
|
|
427
|
+
APIError: If the API returns a non-2xx error.
|
|
428
|
+
NetworkError: If a connection or timeout error occurs.
|
|
429
|
+
|
|
430
|
+
Example::
|
|
431
|
+
|
|
432
|
+
result = await client.bulk_rerun_manual_scraper(
|
|
433
|
+
scraper_id="scraper_12345",
|
|
434
|
+
urls=[
|
|
435
|
+
"https://www.example.com/products/item1",
|
|
436
|
+
"https://www.example.com/products/item2",
|
|
437
|
+
"https://www.example.com/products/item3",
|
|
438
|
+
],
|
|
439
|
+
)
|
|
440
|
+
"""
|
|
441
|
+
endpoint = f"{_API_BASE_URL}/scrapers-manual-rerun/bulk"
|
|
442
|
+
payload: dict[str, Any] = {
|
|
443
|
+
"scraperId": scraper_id,
|
|
444
|
+
"urls": urls,
|
|
445
|
+
}
|
|
446
|
+
return await self._post(endpoint, payload=payload, timeout=600.0)
|
|
447
|
+
|
|
448
|
+
async def get_all_results(
|
|
449
|
+
self,
|
|
450
|
+
*,
|
|
451
|
+
sort_field: Literal[
|
|
452
|
+
"createdAt",
|
|
453
|
+
"updatedAt",
|
|
454
|
+
"id",
|
|
455
|
+
"type",
|
|
456
|
+
"url",
|
|
457
|
+
"status",
|
|
458
|
+
"error",
|
|
459
|
+
"tokenUsage",
|
|
460
|
+
"runtime",
|
|
461
|
+
] = "updatedAt",
|
|
462
|
+
sort_order: Literal["ASC", "DESC"] = "DESC",
|
|
463
|
+
page_size: int = 10,
|
|
464
|
+
page: int = 1,
|
|
465
|
+
search: str | None = None,
|
|
466
|
+
date_range_column: str | None = None,
|
|
467
|
+
start_at: str | None = None,
|
|
468
|
+
end_at: str | None = None,
|
|
469
|
+
) -> dict[str, Any]:
|
|
470
|
+
"""
|
|
471
|
+
Retrieve a paginated, sortable, and filterable list of all scraping results.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
sort_field: Field to sort by (default ``"updatedAt"``).
|
|
475
|
+
Options: ``"createdAt"``, ``"updatedAt"``, ``"id"``,
|
|
476
|
+
``"type"``, ``"url"``, ``"status"``, ``"error"``,
|
|
477
|
+
``"tokenUsage"``, ``"runtime"``.
|
|
478
|
+
sort_order: ``"ASC"`` or ``"DESC"`` (default ``"DESC"``).
|
|
479
|
+
page_size: Results per page (default 10).
|
|
480
|
+
page: Page number, 1-indexed (default 1).
|
|
481
|
+
search: Free-text search query across result fields (optional).
|
|
482
|
+
date_range_column: Column name to filter by date range (optional).
|
|
483
|
+
Common values: ``"updatedAt"``, ``"createdAt"``.
|
|
484
|
+
start_at: ISO-8601 start date for ``date_range_column`` filter (optional).
|
|
485
|
+
Example: ``"2024-01-01"`` or ``"2024-01-01T00:00:00"``.
|
|
486
|
+
end_at: ISO-8601 end date for ``date_range_column`` filter (optional).
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
A dict with keys ``status_code``, ``data`` (paginated results and
|
|
490
|
+
pagination metadata), and ``headers``.
|
|
491
|
+
|
|
492
|
+
Raises:
|
|
493
|
+
AuthenticationError: If the API token is invalid.
|
|
494
|
+
APIError: If the API returns a non-2xx error.
|
|
495
|
+
NetworkError: If a connection or timeout error occurs.
|
|
496
|
+
|
|
497
|
+
Example::
|
|
498
|
+
|
|
499
|
+
page = await client.get_all_results(
|
|
500
|
+
sort_field="updatedAt",
|
|
501
|
+
sort_order="DESC",
|
|
502
|
+
page_size=20,
|
|
503
|
+
date_range_column="updatedAt",
|
|
504
|
+
start_at="2024-01-01",
|
|
505
|
+
end_at="2024-01-08",
|
|
506
|
+
)
|
|
507
|
+
"""
|
|
508
|
+
params: dict[str, Any] = {
|
|
509
|
+
"sortField": sort_field,
|
|
510
|
+
"sortOrder": sort_order,
|
|
511
|
+
"pageSize": page_size,
|
|
512
|
+
"page": page,
|
|
513
|
+
}
|
|
514
|
+
if search is not None:
|
|
515
|
+
params["search"] = search
|
|
516
|
+
if date_range_column is not None:
|
|
517
|
+
params["dateRangeColumn"] = date_range_column
|
|
518
|
+
if start_at is not None:
|
|
519
|
+
params["startAt"] = start_at
|
|
520
|
+
if end_at is not None:
|
|
521
|
+
params["endAt"] = end_at
|
|
522
|
+
|
|
523
|
+
url = f"{_API_BASE_URL}/results?{urlencode(params)}"
|
|
524
|
+
return await self._get(url, headers=self._auth_headers(), timeout=600.0)
|
|
525
|
+
|
|
526
|
+
async def get_result_by_id(self, result_id: str) -> dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
Retrieve full details of a specific scraping result by its ID.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
result_id: Unique identifier of the result. Result IDs are returned
|
|
532
|
+
by scraper execution methods and by :meth:`get_all_results`.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
A dict with keys ``status_code``, ``data`` (complete result object),
|
|
536
|
+
and ``headers``.
|
|
537
|
+
|
|
538
|
+
Raises:
|
|
539
|
+
AuthenticationError: If the API token is invalid.
|
|
540
|
+
APIError: If the API returns a non-2xx error.
|
|
541
|
+
NetworkError: If a connection or timeout error occurs.
|
|
542
|
+
|
|
543
|
+
Example::
|
|
544
|
+
|
|
545
|
+
result = await client.get_result_by_id("result_12345")
|
|
546
|
+
print(result["data"])
|
|
547
|
+
"""
|
|
548
|
+
url = f"{_API_BASE_URL}/results/{result_id}"
|
|
549
|
+
return await self._get(url, headers=self._auth_headers(), timeout=600.0)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mrscraper-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the MrScraper web-scraping API
|
|
5
|
+
Project-URL: Homepage, https://mrscraper.com
|
|
6
|
+
Project-URL: Documentation, https://docs.mrscraper.com
|
|
7
|
+
Project-URL: Repository, https://github.com/mrscraper/mrscraper-python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/mrscraper/mrscraper-python/issues
|
|
9
|
+
Author: MrScraper
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,mrscraper,scraping,sdk,web scraping
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# MrScraper Python SDK
|
|
36
|
+
|
|
37
|
+
A clean, typed Python client for the [MrScraper](https://mrscraper.com) web-scraping API.
|
|
38
|
+
Supports **async/await** usage.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install mrscraper-sdk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.9+.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
Every client is initialised with your MrScraper API token.
|
|
55
|
+
Get yours at <https://app.mrscraper.com>.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from mrscraper import MrScraper
|
|
59
|
+
|
|
60
|
+
client = MrScraper(token="atk_your_token_here")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
### Fetch raw HTML (stealth browser)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
from mrscraper import MrScraper
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
client = MrScraper(token="atk_your_token_here")
|
|
75
|
+
|
|
76
|
+
result = await client.fetch_html(
|
|
77
|
+
"https://stockx.com/air-jordan-1-retro-low-og-chicago-2025",
|
|
78
|
+
geo_code="US",
|
|
79
|
+
timeout=120,
|
|
80
|
+
block_resources=False,
|
|
81
|
+
)
|
|
82
|
+
print(result["data"]) # raw HTML string
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Create an AI scraper
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
result = await client.create_scraper(
|
|
91
|
+
url="https://example.com/products",
|
|
92
|
+
message="Extract all product names, prices, and ratings",
|
|
93
|
+
agent="listing", # "general" | "listing" | "map"
|
|
94
|
+
proxy_country="US",
|
|
95
|
+
)
|
|
96
|
+
scraper_id = result["data"]["data"]["id"]
|
|
97
|
+
print("Scraper ID:", scraper_id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Rerun a scraper on a new URL
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
result = await client.rerun_scraper(
|
|
104
|
+
scraper_id=scraper_id,
|
|
105
|
+
url="https://example.com/products?page=2",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Bulk rerun on multiple URLs (AI scraper)
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = await client.bulk_rerun_scraper(
|
|
113
|
+
scraper_id=scraper_id,
|
|
114
|
+
urls=[
|
|
115
|
+
"https://example.com/products/item1",
|
|
116
|
+
"https://example.com/products/item2",
|
|
117
|
+
"https://example.com/products/item3",
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Rerun a manually configured scraper
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
result = await client.rerun_manual_scraper(
|
|
126
|
+
scraper_id="manual_scraper_67890",
|
|
127
|
+
url="https://example.com/products/new-item",
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Bulk rerun manual scraper on multiple URLs
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
result = await client.bulk_rerun_manual_scraper(
|
|
135
|
+
scraper_id="scraper_12345",
|
|
136
|
+
urls=[
|
|
137
|
+
"https://www.example.com/products/item1",
|
|
138
|
+
"https://www.example.com/products/item2",
|
|
139
|
+
"https://www.example.com/products/item3",
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Retrieve results
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# All results (paginated)
|
|
148
|
+
page = await client.get_all_results(
|
|
149
|
+
sort_field="updatedAt",
|
|
150
|
+
sort_order="DESC",
|
|
151
|
+
page_size=20,
|
|
152
|
+
page=1,
|
|
153
|
+
search="product",
|
|
154
|
+
date_range_column="updatedAt",
|
|
155
|
+
start_at="2024-01-01",
|
|
156
|
+
end_at="2024-01-31",
|
|
157
|
+
)
|
|
158
|
+
print(page["data"])
|
|
159
|
+
|
|
160
|
+
# A specific result by ID
|
|
161
|
+
result = await client.get_result_by_id("result_12345")
|
|
162
|
+
print(result["data"])
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## API Reference
|
|
168
|
+
|
|
169
|
+
### `MrScraper`
|
|
170
|
+
|
|
171
|
+
All methods are coroutines and must be awaited.
|
|
172
|
+
|
|
173
|
+
| Method | Description |
|
|
174
|
+
|--------|-------------|
|
|
175
|
+
| `fetch_html(url, *, timeout, geo_code, block_resources)` | Fetch rendered HTML via the MrScraper stealth browser |
|
|
176
|
+
| `create_scraper(url, message, *, agent, proxy_country, ...)` | Create & run an AI-powered scraper |
|
|
177
|
+
| `rerun_scraper(scraper_id, url, *, max_depth, max_pages, limit, ...)` | Rerun an AI scraper on a new URL |
|
|
178
|
+
| `bulk_rerun_scraper(scraper_id, urls)` | Rerun an AI scraper on multiple URLs in one batch |
|
|
179
|
+
| `rerun_manual_scraper(scraper_id, url)` | Rerun a manually configured scraper on a single URL |
|
|
180
|
+
| `bulk_rerun_manual_scraper(scraper_id, urls)` | Rerun a manual scraper on multiple URLs in one batch |
|
|
181
|
+
| `get_all_results(*, sort_field, sort_order, page_size, page, search, ...)` | List all results with filtering & pagination |
|
|
182
|
+
| `get_result_by_id(result_id)` | Fetch a single result by its ID |
|
|
183
|
+
|
|
184
|
+
All methods return a `dict` with the following keys:
|
|
185
|
+
|
|
186
|
+
| Key | Type | Description |
|
|
187
|
+
|-----|------|-------------|
|
|
188
|
+
| `status_code` | `int` | HTTP status code |
|
|
189
|
+
| `data` | `Any` | Parsed JSON body or raw text |
|
|
190
|
+
| `headers` | `dict` | Response headers |
|
|
191
|
+
|
|
192
|
+
### `bulk_rerun_manual_scraper`
|
|
193
|
+
|
|
194
|
+
Reruns a manually configured scraper on multiple URLs simultaneously in a single batch operation. This is more efficient than calling `rerun_manual_scraper` multiple times, as it processes all URLs in parallel and returns consolidated results. Ideal for scraping multiple pages, products, or articles with the same extraction logic.
|
|
195
|
+
|
|
196
|
+
| Argument | Description |
|
|
197
|
+
|----------|-------------|
|
|
198
|
+
| `scraper_id` | The ID of the manual scraper to rerun (obtained from the MrScraper dashboard). Must be a scraper created manually through the web interface, not an AI scraper. Find it at https://app.mrscraper.com |
|
|
199
|
+
| `urls` | A list of target URLs to scrape (required, must contain at least one URL). Each URL will be processed independently using the scraper's extraction logic. Example: `["https://example.com/page1", "https://example.com/page2"]` |
|
|
200
|
+
|
|
201
|
+
**Returns:** A dict with `status_code`, `data` (bulk job info including job ID, status, metadata; use `get_all_results` or `get_result_by_id` to fetch per-URL results), and `headers`.
|
|
202
|
+
|
|
203
|
+
**Example:**
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
result = await client.bulk_rerun_manual_scraper(
|
|
207
|
+
scraper_id="scraper_12345",
|
|
208
|
+
urls=[
|
|
209
|
+
"https://www.example.com/products/item1",
|
|
210
|
+
"https://www.example.com/products/item2",
|
|
211
|
+
"https://www.example.com/products/item3",
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `create_scraper` — agent types
|
|
217
|
+
|
|
218
|
+
| Agent | Best used for |
|
|
219
|
+
|-------|---------------|
|
|
220
|
+
| `"general"` | Default; handles almost any page |
|
|
221
|
+
| `"listing"` | Product listings, job boards, search results |
|
|
222
|
+
| `"map"` | Crawling all sub-pages / sitemaps of a site |
|
|
223
|
+
|
|
224
|
+
The `max_depth`, `max_pages`, `limit`, `include_patterns`, and `exclude_patterns`
|
|
225
|
+
parameters are only meaningful when `agent="map"`.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Exceptions
|
|
230
|
+
|
|
231
|
+
| Exception | Raised when |
|
|
232
|
+
|-----------|-------------|
|
|
233
|
+
| `MrScraperError` | Base class for all SDK errors |
|
|
234
|
+
| `AuthenticationError` | API token is invalid or missing (HTTP 401) |
|
|
235
|
+
| `APIError` | API returned a non-2xx error; has `.status_code` attribute |
|
|
236
|
+
| `NetworkError` | Connection timeout or network-level failure |
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from mrscraper.exceptions import AuthenticationError, APIError, NetworkError
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
result = await client.fetch_html("https://example.com")
|
|
243
|
+
except AuthenticationError:
|
|
244
|
+
print("Check your API token at https://app.mrscraper.com")
|
|
245
|
+
except APIError as e:
|
|
246
|
+
print(f"API error {e.status_code}: {e}")
|
|
247
|
+
except NetworkError as e:
|
|
248
|
+
print(f"Network problem: {e}")
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Install with dev dependencies
|
|
257
|
+
pip install -e ".[dev]"
|
|
258
|
+
|
|
259
|
+
# Run tests
|
|
260
|
+
pytest
|
|
261
|
+
|
|
262
|
+
# Lint & format
|
|
263
|
+
ruff check .
|
|
264
|
+
ruff format .
|
|
265
|
+
|
|
266
|
+
# Type check
|
|
267
|
+
mypy src/mrscraper
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT © MrScraper
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mrscraper/__init__.py,sha256=oz--B3PhxkUAxfc_q0xPHMPLI0dKEuCGDsVP5c9BDLw,656
|
|
2
|
+
mrscraper/exceptions.py,sha256=oixafdD4I2hzounileLFnQqFWPoSrsEbJ189gllzdno,656
|
|
3
|
+
mrscraper/mrscraper.py,sha256=eOISFlyHSQXgYzGdLp-7dLAFc5bk_uq-oupXrukmG8A,19943
|
|
4
|
+
mrscraper_sdk-0.1.0.dist-info/METADATA,sha256=AqnPxfpAHTQc3nzIZ-d1DjW82m7Z0VuYO3PqKXiIpKI,7880
|
|
5
|
+
mrscraper_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
mrscraper_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=PLsi4oNgNysc0BISmsiErxXUTKWrzKHMNv0whTadqJI,1066
|
|
7
|
+
mrscraper_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MrScraper
|
|
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.
|