scrapeMM 0.6.2__tar.gz → 0.6.3__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.
- {scrapemm-0.6.2/scrapeMM.egg-info → scrapemm-0.6.3}/PKG-INFO +1 -1
- {scrapemm-0.6.2 → scrapemm-0.6.3}/pyproject.toml +1 -1
- {scrapemm-0.6.2 → scrapemm-0.6.3}/requirements.txt +2 -1
- {scrapemm-0.6.2 → scrapemm-0.6.3/scrapeMM.egg-info}/PKG-INFO +1 -1
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/decodo.py +13 -18
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/retrieval.py +4 -2
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/secrets.py +6 -8
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/util.py +28 -6
- scrapemm-0.6.3/scripts/example.py +11 -0
- scrapemm-0.6.2/scripts/example.py +0 -10
- {scrapemm-0.6.2 → scrapemm-0.6.3}/.gitignore +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/LICENSE +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/README.md +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/SOURCES.txt +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/exceptions.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/scraping_response.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/common.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/images.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/media.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/requests.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/util.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/videos.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/archive_org.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/archive_today.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/base.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/bluesky.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/html_postprocessor.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/fb.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/__init__.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/instagram.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/mvault.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/perma_cc.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/telegram.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/tiktok.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/x.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/youtube.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/ytdlp.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/setup.cfg +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/testing/test_retrieval.py +0 -0
- {scrapemm-0.6.2 → scrapemm-0.6.3}/testing/test_utils.py +0 -0
|
@@ -22,4 +22,5 @@ playwright
|
|
|
22
22
|
playwright-stealth
|
|
23
23
|
beautifulsoup4
|
|
24
24
|
pip-system-certs # For better SSL certificate handling
|
|
25
|
-
backports.zstd # For better content parsing with Firecrawl
|
|
25
|
+
backports.zstd # For better content parsing with Firecrawl
|
|
26
|
+
prompt_toolkit # For handling user terminal input
|
|
@@ -26,23 +26,21 @@ class Decodo:
|
|
|
26
26
|
DECODO_API_URL = "https://scraper-api.decodo.com/v2/scrape"
|
|
27
27
|
|
|
28
28
|
def __init__(self):
|
|
29
|
-
self.
|
|
30
|
-
self.password = None
|
|
29
|
+
self.basic_auth_token = None
|
|
31
30
|
self.n_scrapes = 0
|
|
32
31
|
|
|
33
|
-
def
|
|
32
|
+
def _load_token(self):
|
|
34
33
|
"""Loads Decodo credentials from the secrets manager."""
|
|
35
|
-
self.
|
|
36
|
-
self.password = get_secret("decodo_password")
|
|
34
|
+
self.basic_auth_token = get_secret("decodo_token")
|
|
37
35
|
|
|
38
|
-
if self.
|
|
39
|
-
logger.info("✅ Decodo
|
|
36
|
+
if self.basic_auth_token:
|
|
37
|
+
logger.info("✅ Decodo token set.")
|
|
40
38
|
else:
|
|
41
|
-
logger.warning("⚠️ Decodo
|
|
39
|
+
logger.warning("⚠️ Decodo auth token not found. Please configure it in secrets.")
|
|
42
40
|
|
|
43
|
-
def
|
|
41
|
+
def _has_token(self) -> bool:
|
|
44
42
|
"""Checks if Decodo credentials are available."""
|
|
45
|
-
return bool(self.
|
|
43
|
+
return bool(self.basic_auth_token)
|
|
46
44
|
|
|
47
45
|
async def scrape(
|
|
48
46
|
self, url: str,
|
|
@@ -66,10 +64,10 @@ class Decodo:
|
|
|
66
64
|
Returns:
|
|
67
65
|
MultimodalSequence containing the scraped content, or None if scraping failed
|
|
68
66
|
"""
|
|
69
|
-
if not self.
|
|
70
|
-
self.
|
|
67
|
+
if not self._has_token():
|
|
68
|
+
self._load_token()
|
|
71
69
|
|
|
72
|
-
if not self.
|
|
70
|
+
if not self._has_token():
|
|
73
71
|
logger.warning("Cannot scrape with Decodo: credentials not configured.")
|
|
74
72
|
return None
|
|
75
73
|
|
|
@@ -124,6 +122,7 @@ class Decodo:
|
|
|
124
122
|
"""
|
|
125
123
|
headers = {
|
|
126
124
|
'Content-Type': 'application/json',
|
|
125
|
+
'Authorization': f'Basic {self.basic_auth_token}',
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
# Build request payload
|
|
@@ -141,9 +140,6 @@ class Decodo:
|
|
|
141
140
|
if use_premium_proxy:
|
|
142
141
|
payload["proxy_pool"] = "premium"
|
|
143
142
|
|
|
144
|
-
# Create basic auth
|
|
145
|
-
auth = aiohttp.BasicAuth(self.username, self.password)
|
|
146
|
-
|
|
147
143
|
# Retry loop with exponential backoff
|
|
148
144
|
for attempt in range(max_retries + 1):
|
|
149
145
|
try:
|
|
@@ -151,12 +147,11 @@ class Decodo:
|
|
|
151
147
|
self.DECODO_API_URL,
|
|
152
148
|
json=payload,
|
|
153
149
|
headers=headers,
|
|
154
|
-
auth=auth,
|
|
155
150
|
timeout=aiohttp.ClientTimeout(total=timeout)
|
|
156
151
|
) as response:
|
|
157
152
|
# Validate response health
|
|
158
153
|
if response.status != 200:
|
|
159
|
-
logger.debug("Communication with Decodo API failed.")
|
|
154
|
+
logger.debug(f"Communication with Decodo API failed. Status code: {response.status}")
|
|
160
155
|
|
|
161
156
|
if response.status == 429: # Rate limit
|
|
162
157
|
if attempt >= max_retries:
|
|
@@ -41,6 +41,8 @@ BEST_METHODS = {
|
|
|
41
41
|
"perma.cc": ["integrations"],
|
|
42
42
|
"mvau.lt": ["integrations"],
|
|
43
43
|
"archive.org": ["integrations"],
|
|
44
|
+
"verafiles.org": ["decodo", "firecrawl"],
|
|
45
|
+
"youturn.in": ["decodo"],
|
|
44
46
|
# Archive.today:
|
|
45
47
|
"archive.today": ["integrations", "decodo"],
|
|
46
48
|
"archive.is": ["integrations", "decodo"],
|
|
@@ -185,7 +187,7 @@ async def _retrieve_single(
|
|
|
185
187
|
session=session, format=format, actions=actions),
|
|
186
188
|
"decodo": lambda: decodo.scrape(url, remove_urls, session,
|
|
187
189
|
format=format,
|
|
188
|
-
timeout=
|
|
190
|
+
timeout=15 if prioritize == "speed" else 30,
|
|
189
191
|
max_retries=1 if prioritize == "speed" else 5),
|
|
190
192
|
}
|
|
191
193
|
|
|
@@ -227,7 +229,7 @@ async def _retrieve_single(
|
|
|
227
229
|
|
|
228
230
|
except Exception as e:
|
|
229
231
|
logger.warning(f"Error while retrieving with method '{method_name}': {e}")
|
|
230
|
-
errors[method_name] = e
|
|
232
|
+
errors[method_name] = f"{type(e).__name__}: {e}"
|
|
231
233
|
result = None
|
|
232
234
|
|
|
233
235
|
if result is not None:
|
|
@@ -9,8 +9,8 @@ from cryptography.hazmat.backends import default_backend
|
|
|
9
9
|
from cryptography.hazmat.primitives import hashes
|
|
10
10
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
11
11
|
|
|
12
|
-
from scrapemm.common import get_config_var, update_config,
|
|
13
|
-
from scrapemm.util import
|
|
12
|
+
from scrapemm.common import get_config_var, update_config, CONFIG_DIR
|
|
13
|
+
from scrapemm.util import get_user_input
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger("scrapeMM")
|
|
16
16
|
|
|
@@ -23,8 +23,7 @@ SECRETS = {
|
|
|
23
23
|
"bluesky_password": "Bluesky password",
|
|
24
24
|
"tiktok_client_key": "TikTok client key",
|
|
25
25
|
"tiktok_client_secret": "TikTok client secret",
|
|
26
|
-
"
|
|
27
|
-
"decodo_password": "Decodo Web Scraping API password",
|
|
26
|
+
"decodo_token": "Decodo Web Scraping API basic authentication token",
|
|
28
27
|
"youtube_cookie": "YouTube cookie string",
|
|
29
28
|
"facebook_cookie": "Facebook cookie string",
|
|
30
29
|
}
|
|
@@ -144,9 +143,9 @@ def override_secret(key_name: str):
|
|
|
144
143
|
when nothing entered."""
|
|
145
144
|
description = SECRETS[key_name]
|
|
146
145
|
if key_name in ["youtube_cookie", "facebook_cookie"]:
|
|
147
|
-
user_input =
|
|
146
|
+
user_input = get_user_input(f"Please enter the {description} (Alt+Enter to submit):", multiline=True)
|
|
148
147
|
else:
|
|
149
|
-
user_input =
|
|
148
|
+
user_input = get_user_input(f"Please enter the {description} (leave empty to skip):")
|
|
150
149
|
if user_input:
|
|
151
150
|
set_secret(key_name, user_input)
|
|
152
151
|
|
|
@@ -171,8 +170,7 @@ def configure_secrets(all_keys: bool = False):
|
|
|
171
170
|
|
|
172
171
|
update_config(api_keys_configured=True)
|
|
173
172
|
|
|
174
|
-
logger.info("API keys configured successfully! If you want to change them,
|
|
175
|
-
f"{CONFIG_PATH.as_posix()} and set 'api_keys_configured' to 'false' or "
|
|
173
|
+
logger.info("API keys configured successfully! If you want to change them, "
|
|
176
174
|
f"run scrapemm.secrets.configure_secrets().")
|
|
177
175
|
|
|
178
176
|
|
|
@@ -38,7 +38,7 @@ def get_domain(url: str, keep_subdomain: bool = False) -> Optional[str]:
|
|
|
38
38
|
async def run_with_semaphore(tasks: Iterable[Awaitable],
|
|
39
39
|
limit: int,
|
|
40
40
|
show_progress: bool = True,
|
|
41
|
-
progress_description: str | None = None) ->
|
|
41
|
+
progress_description: str | None = None) -> tuple:
|
|
42
42
|
"""
|
|
43
43
|
Runs asynchronous tasks with a concurrency limit.
|
|
44
44
|
|
|
@@ -76,10 +76,32 @@ def read_urls_from_file(file_path):
|
|
|
76
76
|
return f.read().splitlines()
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
def get_user_input(prompt: str, multiline: bool = False) -> str:
|
|
80
|
+
"""Prompts the user for input.
|
|
81
|
+
- Single-line: uses standard input(); Enter submits, empty input skips.
|
|
82
|
+
- Multiline: uses prompt_toolkit; Enter adds newline, Alt+Enter submits.
|
|
83
|
+
"""
|
|
84
|
+
if multiline:
|
|
85
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
86
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
87
|
+
|
|
88
|
+
print(prompt)
|
|
89
|
+
|
|
90
|
+
bindings = KeyBindings()
|
|
91
|
+
|
|
92
|
+
@bindings.add('escape', 'enter')
|
|
93
|
+
def _submit(event):
|
|
94
|
+
event.current_buffer.validate_and_handle()
|
|
95
|
+
|
|
96
|
+
result = pt_prompt(
|
|
97
|
+
">>> ",
|
|
98
|
+
multiline=True,
|
|
99
|
+
key_bindings=bindings,
|
|
100
|
+
prompt_continuation="... ",
|
|
101
|
+
)
|
|
102
|
+
return result
|
|
103
|
+
else:
|
|
104
|
+
return input(f"{prompt} ")
|
|
83
105
|
|
|
84
106
|
|
|
85
107
|
MAX_MEDIA_PER_PAGE = 32
|
|
@@ -127,7 +149,7 @@ async def resolve_media_hyperlinks(
|
|
|
127
149
|
|
|
128
150
|
# Try to download media for each URL
|
|
129
151
|
tasks = [download_medium(u, session=session, headers={"Referer": url}) for u in hrefs_urls.values()]
|
|
130
|
-
media:
|
|
152
|
+
media: tuple[Item | None] = await run_with_semaphore(tasks, limit=100, show_progress=False)
|
|
131
153
|
|
|
132
154
|
href_media = dict(zip(hrefs_urls.keys(), media))
|
|
133
155
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from scrapemm import retrieve
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
url = "https://youturn.in/factcheck/tamilnadu-india-alliance-final-list.html"
|
|
6
|
+
result = asyncio.run(retrieve(url, methods=["decodo", "firecrawl"]))
|
|
7
|
+
print(f"Retrieval time: {result.retrieval_time:.2f} seconds")
|
|
8
|
+
if result.errors:
|
|
9
|
+
print(result.errors)
|
|
10
|
+
else:
|
|
11
|
+
print(result.content)
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from scrapemm import retrieve
|
|
2
|
-
import asyncio
|
|
3
|
-
|
|
4
|
-
if __name__ == "__main__":
|
|
5
|
-
url = "https://www.snopes.com/fact-check/biden-bounty-maduro/"
|
|
6
|
-
result = asyncio.run(retrieve(url, methods=["firecrawl"]))
|
|
7
|
-
if result.errors:
|
|
8
|
-
print(result.errors)
|
|
9
|
-
else:
|
|
10
|
-
print(result.content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|