scrapeMM 0.3.1__tar.gz → 0.3.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.3.1/scrapeMM.egg-info → scrapemm-0.3.3}/PKG-INFO +1 -1
- {scrapemm-0.3.1 → scrapemm-0.3.3}/pyproject.toml +1 -1
- {scrapemm-0.3.1 → scrapemm-0.3.3/scrapeMM.egg-info}/PKG-INFO +1 -1
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/retrieval.py +7 -3
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/decodo.py +16 -19
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/firecrawl.py +5 -2
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/util.py +27 -8
- scrapemm-0.3.3/scripts/example.py +7 -0
- scrapemm-0.3.1/scripts/example.py +0 -8
- {scrapemm-0.3.1 → scrapemm-0.3.3}/.gitignore +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/LICENSE +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/README.md +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/requirements.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/SOURCES.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/__init__.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/common.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/__init__.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/base.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/bluesky.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/fb.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/instagram.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/telegram.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/tiktok.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/x.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/youtube.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/__init__.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/no_bot_domains.txt +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/ytdlp.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/secrets.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/util.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/setup.cfg +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/testing/test_retrieval.py +0 -0
- {scrapemm-0.3.1 → scrapemm-0.3.3}/testing/test_utils.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, Collection
|
|
3
3
|
from traceback import format_exc
|
|
4
4
|
|
|
5
5
|
import aiohttp
|
|
@@ -14,7 +14,7 @@ METHODS = ["integrations", "firecrawl", "decodo"]
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
async def retrieve(
|
|
17
|
-
urls: str |
|
|
17
|
+
urls: str | Collection[str],
|
|
18
18
|
remove_urls: bool = False,
|
|
19
19
|
show_progress: bool = True,
|
|
20
20
|
actions: list[dict] = None,
|
|
@@ -114,7 +114,11 @@ async def _retrieve_single(
|
|
|
114
114
|
continue
|
|
115
115
|
|
|
116
116
|
logger.debug(f"Trying method: {method_name}")
|
|
117
|
-
|
|
117
|
+
try:
|
|
118
|
+
result = await method_map[method_name]()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Error while retrieving with method '{method_name}': {e}")
|
|
121
|
+
result = None
|
|
118
122
|
|
|
119
123
|
if result is not None:
|
|
120
124
|
logger.debug(f"Successfully retrieved with method: {method_name}")
|
|
@@ -6,7 +6,7 @@ import aiohttp
|
|
|
6
6
|
from ezmm import MultimodalSequence
|
|
7
7
|
|
|
8
8
|
from scrapemm.secrets import get_secret
|
|
9
|
-
from scrapemm.scraping.util import to_multimodal_sequence
|
|
9
|
+
from scrapemm.scraping.util import to_multimodal_sequence, get_domain_root
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger("scrapeMM")
|
|
12
12
|
|
|
@@ -74,7 +74,8 @@ class Decodo:
|
|
|
74
74
|
if format == "html":
|
|
75
75
|
return html
|
|
76
76
|
else:
|
|
77
|
-
|
|
77
|
+
domain_root = get_domain_root(url)
|
|
78
|
+
return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session, domain_root=domain_root)
|
|
78
79
|
return None
|
|
79
80
|
|
|
80
81
|
async def _call_decodo(self, url: str,
|
|
@@ -111,8 +112,6 @@ class Decodo:
|
|
|
111
112
|
# Create basic auth
|
|
112
113
|
auth = aiohttp.BasicAuth(self.username, self.password)
|
|
113
114
|
|
|
114
|
-
# logger.debug(f"Decodo request payload: {payload}")
|
|
115
|
-
|
|
116
115
|
try:
|
|
117
116
|
async with session.post(
|
|
118
117
|
self.DECODO_API_URL,
|
|
@@ -121,26 +120,17 @@ class Decodo:
|
|
|
121
120
|
auth=auth,
|
|
122
121
|
timeout=aiohttp.ClientTimeout(total=timeout)
|
|
123
122
|
) as response:
|
|
124
|
-
|
|
123
|
+
# Validate response health
|
|
125
124
|
if response.status != 200:
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
error_body = await response.json()
|
|
129
|
-
error_msg = error_body.get('message', error_body)
|
|
130
|
-
except:
|
|
131
|
-
error_msg = await response.text()
|
|
132
|
-
|
|
133
|
-
logger.info(
|
|
134
|
-
f"Failed to scrape {url} with Decodo\n"
|
|
135
|
-
f"Status code: {response.status} - Reason: {response.reason}\n"
|
|
136
|
-
f"Error details: {error_msg}"
|
|
137
|
-
)
|
|
138
|
-
|
|
125
|
+
logger.debug("Communication with Decodo API failed.")
|
|
139
126
|
match response.status:
|
|
127
|
+
case 400:
|
|
128
|
+
logger.debug("Error 400: Bad request. If you use JavaScript, make sure you have the "
|
|
129
|
+
"Advanced plan subscription.")
|
|
140
130
|
case 401:
|
|
141
131
|
logger.error("Error 401: Unauthorized. Check your Decodo credentials.")
|
|
142
132
|
case 402:
|
|
143
|
-
logger.
|
|
133
|
+
logger.error("Error 402: Payment required. Check your Decodo subscription.")
|
|
144
134
|
case 403:
|
|
145
135
|
logger.debug("Error 403: Forbidden.")
|
|
146
136
|
case 408:
|
|
@@ -157,6 +147,13 @@ class Decodo:
|
|
|
157
147
|
# Parse response
|
|
158
148
|
json_response = await response.json()
|
|
159
149
|
|
|
150
|
+
# Validate if scrape was successful
|
|
151
|
+
if json_response.get("status") == "failed":
|
|
152
|
+
status_code = json_response.get("status_code")
|
|
153
|
+
message = json_response.get("message")
|
|
154
|
+
logger.info(f"Decodo failed to scrape. Error {status_code}: {message}")
|
|
155
|
+
return None
|
|
156
|
+
|
|
160
157
|
# Extract HTML content from results
|
|
161
158
|
if "results" in json_response and len(json_response["results"]) > 0:
|
|
162
159
|
result = json_response["results"][0]
|
|
@@ -7,7 +7,7 @@ from ezmm import MultimodalSequence
|
|
|
7
7
|
|
|
8
8
|
from scrapemm.common import get_config_var, update_config
|
|
9
9
|
from scrapemm.util import read_urls_from_file, get_domain
|
|
10
|
-
from scrapemm.scraping.util import find_firecrawl, to_multimodal_sequence, firecrawl_is_running
|
|
10
|
+
from scrapemm.scraping.util import find_firecrawl, to_multimodal_sequence, firecrawl_is_running, get_domain_root
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger("scrapeMM")
|
|
13
13
|
|
|
@@ -58,6 +58,7 @@ class Firecrawl:
|
|
|
58
58
|
|
|
59
59
|
def connect(self):
|
|
60
60
|
from firecrawl import AsyncFirecrawl
|
|
61
|
+
logging.getLogger("firecrawl").setLevel(logging.WARNING)
|
|
61
62
|
self.firecrawl_url = locate_firecrawl()
|
|
62
63
|
self._firecrawl = AsyncFirecrawl(api_url=self.firecrawl_url)
|
|
63
64
|
|
|
@@ -88,7 +89,9 @@ class Firecrawl:
|
|
|
88
89
|
if format == "html":
|
|
89
90
|
return html
|
|
90
91
|
else:
|
|
91
|
-
|
|
92
|
+
domain_root = get_domain_root(url)
|
|
93
|
+
return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session, domain_root=domain_root)
|
|
94
|
+
return None
|
|
92
95
|
|
|
93
96
|
|
|
94
97
|
fire = Firecrawl()
|
|
@@ -6,6 +6,7 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
import aiohttp
|
|
8
8
|
import requests
|
|
9
|
+
from PIL import UnidentifiedImageError
|
|
9
10
|
from ezmm import MultimodalSequence, download_item, Item, Image, Video
|
|
10
11
|
from markdownify import markdownify as md
|
|
11
12
|
from requests.exceptions import ReadTimeout, ConnectionError, RetryError
|
|
@@ -53,6 +54,7 @@ def postprocess_scraped(text: str) -> str:
|
|
|
53
54
|
|
|
54
55
|
async def resolve_media_hyperlinks(
|
|
55
56
|
text: str, session: aiohttp.ClientSession,
|
|
57
|
+
domain_root: str = None,
|
|
56
58
|
remove_urls: bool = False,
|
|
57
59
|
) -> Optional[MultimodalSequence]:
|
|
58
60
|
"""Downloads all media that are hyperlinked in the provided Markdown text.
|
|
@@ -64,19 +66,21 @@ async def resolve_media_hyperlinks(
|
|
|
64
66
|
|
|
65
67
|
# Extract URLs and base64-encoded data from the text
|
|
66
68
|
hyperlinks = get_markdown_hyperlinks(text)
|
|
67
|
-
|
|
69
|
+
hrefs_urls = dict()
|
|
68
70
|
data_uris = set()
|
|
69
71
|
for _, _, href in hyperlinks:
|
|
70
72
|
if is_url(href):
|
|
71
|
-
|
|
73
|
+
hrefs_urls[href] = href
|
|
74
|
+
elif domain_root and is_root_relative_url(href):
|
|
75
|
+
hrefs_urls[href] = f"{domain_root}{href}"
|
|
72
76
|
elif is_data_uri(href):
|
|
73
77
|
data_uris.add(href)
|
|
74
78
|
|
|
75
79
|
# Try to download media for each URL
|
|
76
|
-
tasks = [download_item(url, session=session) for url in
|
|
80
|
+
tasks = [download_item(url, session=session) for url in hrefs_urls.values()]
|
|
77
81
|
media: list[Item | None] = await run_with_semaphore(tasks, limit=100, show_progress=False)
|
|
78
82
|
|
|
79
|
-
href_media = dict(zip(
|
|
83
|
+
href_media = dict(zip(hrefs_urls.keys(), media))
|
|
80
84
|
|
|
81
85
|
# Convert each base64-encoded data to the respective medium
|
|
82
86
|
for data_uri in data_uris:
|
|
@@ -101,15 +105,29 @@ async def resolve_media_hyperlinks(
|
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def is_url(href: str) -> bool:
|
|
104
|
-
"""Returns True iff the given string is
|
|
108
|
+
"""Returns True iff the given string is an absolute HTTP URL."""
|
|
105
109
|
return re.match(URL_REGEX, href) is not None
|
|
106
110
|
|
|
107
111
|
|
|
112
|
+
def is_root_relative_url(href: str) -> bool:
|
|
113
|
+
"""Returns True iff the given string is a root-relative URL."""
|
|
114
|
+
return href.startswith("/")
|
|
115
|
+
|
|
116
|
+
|
|
108
117
|
def is_data_uri(href: str) -> bool:
|
|
109
118
|
"""Returns True iff the given string is a valid data URI."""
|
|
110
119
|
return re.match(DATA_URI_REGEX, href) is not None
|
|
111
120
|
|
|
112
121
|
|
|
122
|
+
def get_domain_root(url: str) -> Optional[str]:
|
|
123
|
+
"""Extracts the domain root from the given URL. Allows for missing http(s) prefix."""
|
|
124
|
+
match = re.match(r"(:?https?://)?([^/]+)", url)
|
|
125
|
+
if match:
|
|
126
|
+
return match.group(0)
|
|
127
|
+
else:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
113
131
|
def get_markdown_hyperlinks(text: str) -> list[tuple[str, str, str]]:
|
|
114
132
|
"""Extracts all web hyperlinks from the given markdown-formatted string. Returns
|
|
115
133
|
a list of fullmatch-hypertext-URL-triples."""
|
|
@@ -159,7 +177,8 @@ def from_base64(b64_data: str, mime_type: str = "image/jpeg") -> Optional[Item]:
|
|
|
159
177
|
else:
|
|
160
178
|
raise ValueError(f"Unsupported media type: {mime_type}")
|
|
161
179
|
except binascii.Error: # base64 validation failed
|
|
162
|
-
return
|
|
180
|
+
return None
|
|
181
|
+
except UnidentifiedImageError: # Pillow could not identify image format
|
|
182
|
+
return None
|
|
163
183
|
except Exception as e:
|
|
164
|
-
logger.
|
|
165
|
-
f"{type(e).__name__}: {e}")
|
|
184
|
+
logger.debug(f"Error decoding {mime_type} base64 data. \n {type(e).__name__}: {e}")
|
|
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
|