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.
Files changed (36) hide show
  1. {scrapemm-0.3.1/scrapeMM.egg-info → scrapemm-0.3.3}/PKG-INFO +1 -1
  2. {scrapemm-0.3.1 → scrapemm-0.3.3}/pyproject.toml +1 -1
  3. {scrapemm-0.3.1 → scrapemm-0.3.3/scrapeMM.egg-info}/PKG-INFO +1 -1
  4. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/retrieval.py +7 -3
  5. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/decodo.py +16 -19
  6. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/firecrawl.py +5 -2
  7. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/util.py +27 -8
  8. scrapemm-0.3.3/scripts/example.py +7 -0
  9. scrapemm-0.3.1/scripts/example.py +0 -8
  10. {scrapemm-0.3.1 → scrapemm-0.3.3}/.gitignore +0 -0
  11. {scrapemm-0.3.1 → scrapemm-0.3.3}/LICENSE +0 -0
  12. {scrapemm-0.3.1 → scrapemm-0.3.3}/README.md +0 -0
  13. {scrapemm-0.3.1 → scrapemm-0.3.3}/requirements.txt +0 -0
  14. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/SOURCES.txt +0 -0
  15. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/dependency_links.txt +0 -0
  16. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/requires.txt +0 -0
  17. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapeMM.egg-info/top_level.txt +0 -0
  18. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/__init__.py +0 -0
  19. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/common.py +0 -0
  20. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/__init__.py +0 -0
  21. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/base.py +0 -0
  22. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/bluesky.py +0 -0
  23. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/fb.py +0 -0
  24. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/instagram.py +0 -0
  25. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/telegram.py +0 -0
  26. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/tiktok.py +0 -0
  27. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/x.py +0 -0
  28. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/integrations/youtube.py +0 -0
  29. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/__init__.py +0 -0
  30. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/no_bot_domains.txt +0 -0
  31. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/scraping/ytdlp.py +0 -0
  32. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/secrets.py +0 -0
  33. {scrapemm-0.3.1 → scrapemm-0.3.3}/scrapemm/util.py +0 -0
  34. {scrapemm-0.3.1 → scrapemm-0.3.3}/setup.cfg +0 -0
  35. {scrapemm-0.3.1 → scrapemm-0.3.3}/testing/test_retrieval.py +0 -0
  36. {scrapemm-0.3.1 → scrapemm-0.3.3}/testing/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  description = "LLM-friendly scraper for media and text from social media and the open web."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.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 | list[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
- result = await method_map[method_name]()
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
- return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
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
- # Try to get the error message from response
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.debug("Error 402: Payment required. Check your Decodo subscription.")
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
- return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
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
- urls = set()
69
+ hrefs_urls = dict()
68
70
  data_uris = set()
69
71
  for _, _, href in hyperlinks:
70
72
  if is_url(href):
71
- urls.add(href)
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 urls]
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(urls, media))
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 a valid URL."""
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.warning(f"Error decoding base64 data!\n"
165
- f"{type(e).__name__}: {e}")
184
+ logger.debug(f"Error decoding {mime_type} base64 data. \n {type(e).__name__}: {e}")
@@ -0,0 +1,7 @@
1
+ from scrapemm import retrieve
2
+ import asyncio
3
+
4
+ if __name__ == "__main__":
5
+ url = "https://factuel.afp.com/doc.afp.com.43ZN7NP"
6
+ result = asyncio.run(retrieve(url, methods=["decodo"]))
7
+ print(result)
@@ -1,8 +0,0 @@
1
- from scrapemm import retrieve
2
- import asyncio
3
-
4
- if __name__ == "__main__":
5
- url = "https://www.tiktok.com/@xxxx.xxxx5743/video/7521704371109793046"
6
- loop = asyncio.get_event_loop()
7
- result = loop.run_until_complete(retrieve(url))
8
- print(result)
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