scrapeMM 0.3.0__tar.gz → 0.3.2__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.0/scrapeMM.egg-info → scrapemm-0.3.2}/PKG-INFO +2 -1
  2. {scrapemm-0.3.0 → scrapemm-0.3.2}/pyproject.toml +2 -1
  3. {scrapemm-0.3.0 → scrapemm-0.3.2/scrapeMM.egg-info}/PKG-INFO +2 -1
  4. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapeMM.egg-info/requires.txt +1 -0
  5. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/retrieval.py +23 -9
  6. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/decodo.py +19 -19
  7. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/firecrawl.py +6 -2
  8. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/util.py +5 -3
  9. scrapemm-0.3.2/scripts/example.py +7 -0
  10. {scrapemm-0.3.0 → scrapemm-0.3.2}/testing/test_retrieval.py +21 -7
  11. scrapemm-0.3.0/scripts/example.py +0 -8
  12. {scrapemm-0.3.0 → scrapemm-0.3.2}/.gitignore +0 -0
  13. {scrapemm-0.3.0 → scrapemm-0.3.2}/LICENSE +0 -0
  14. {scrapemm-0.3.0 → scrapemm-0.3.2}/README.md +0 -0
  15. {scrapemm-0.3.0 → scrapemm-0.3.2}/requirements.txt +0 -0
  16. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapeMM.egg-info/SOURCES.txt +0 -0
  17. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapeMM.egg-info/dependency_links.txt +0 -0
  18. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapeMM.egg-info/top_level.txt +0 -0
  19. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/__init__.py +0 -0
  20. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/common.py +0 -0
  21. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/__init__.py +0 -0
  22. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/base.py +0 -0
  23. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/bluesky.py +0 -0
  24. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/fb.py +0 -0
  25. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/instagram.py +0 -0
  26. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/telegram.py +0 -0
  27. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/tiktok.py +0 -0
  28. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/x.py +0 -0
  29. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/integrations/youtube.py +0 -0
  30. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/__init__.py +0 -0
  31. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/no_bot_domains.txt +0 -0
  32. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/scraping/ytdlp.py +0 -0
  33. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/secrets.py +0 -0
  34. {scrapemm-0.3.0 → scrapemm-0.3.2}/scrapemm/util.py +0 -0
  35. {scrapemm-0.3.0 → scrapemm-0.3.2}/setup.cfg +0 -0
  36. {scrapemm-0.3.0 → scrapemm-0.3.2}/testing/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -19,6 +19,7 @@ Requires-Dist: atproto
19
19
  Requires-Dist: TikTokResearchApi
20
20
  Requires-Dist: yt-dlp
21
21
  Requires-Dist: cryptography
22
+ Requires-Dist: firecrawl-py
22
23
  Dynamic: license-file
23
24
 
24
25
  # scrapeMM: Multimodal Web Retrieval
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.3.0"
3
+ version = "0.3.2"
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"
@@ -17,6 +17,7 @@ dependencies = [
17
17
  "TikTokResearchApi",
18
18
  "yt-dlp",
19
19
  "cryptography",
20
+ "firecrawl-py"
20
21
  ]
21
22
 
22
23
  [project.urls]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -19,6 +19,7 @@ Requires-Dist: atproto
19
19
  Requires-Dist: TikTokResearchApi
20
20
  Requires-Dist: yt-dlp
21
21
  Requires-Dist: cryptography
22
+ Requires-Dist: firecrawl-py
22
23
  Dynamic: license-file
23
24
 
24
25
  # scrapeMM: Multimodal Web Retrieval
@@ -8,3 +8,4 @@ atproto
8
8
  TikTokResearchApi
9
9
  yt-dlp
10
10
  cryptography
11
+ firecrawl-py
@@ -18,8 +18,9 @@ async def retrieve(
18
18
  remove_urls: bool = False,
19
19
  show_progress: bool = True,
20
20
  actions: list[dict] = None,
21
- methods: list[str] = None
22
- ) -> Optional[MultimodalSequence] | list[Optional[MultimodalSequence]]:
21
+ methods: list[str] = None,
22
+ format: str = "multimodal_sequence",
23
+ ) -> Optional[MultimodalSequence | str] | list[Optional[MultimodalSequence | str]]:
23
24
  """Main function of this repository. Downloads the contents present at the given URL(s).
24
25
  For each URL, returns a MultimodalSequence containing text, images, and videos.
25
26
  Returns None if the corresponding URL is not supported or if retrieval failed.
@@ -37,6 +38,9 @@ async def retrieve(
37
38
  - "firecrawl" (Firecrawl scraping service)
38
39
  - "decodo" (Decodo Web Scraping API)
39
40
  You can specify any subset in any order, e.g., ["decodo", "firecrawl"] or ["integrations"].
41
+ :param format: The format of the output. Available formats:
42
+ - "multimodal_sequence" (MultimodalSequence containing parsed and downloaded media from the page)
43
+ - "html" (string containing the raw HTML code of the page, not compatible with 'integrations' method)
40
44
  """
41
45
  if methods is None:
42
46
  methods = METHODS
@@ -62,7 +66,7 @@ async def retrieve(
62
66
  urls_unique = set(urls_to_retrieve)
63
67
 
64
68
  # Retrieve URLs concurrently
65
- tasks = [_retrieve_single(url, remove_urls, session, methods, actions) for url in urls_unique]
69
+ tasks = [_retrieve_single(url, remove_urls, session, methods, actions, format) for url in urls_unique]
66
70
  results = await run_with_semaphore(tasks, limit=20, show_progress=show_progress and len(urls_to_retrieve) > 1,
67
71
  progress_description="Retrieving URLs...")
68
72
 
@@ -80,21 +84,27 @@ async def _retrieve_single(
80
84
  session: aiohttp.ClientSession,
81
85
  methods: list[str],
82
86
  actions: list[dict] = None,
83
- ) -> Optional[MultimodalSequence]:
87
+ format: str = "multimodal_sequence",
88
+ ) -> Optional[MultimodalSequence | str]:
84
89
  try:
85
90
  # Ensure URL is a string
86
91
  url = str(url)
87
92
 
93
+ # Ensure compatibility with methods
94
+ if format == "html" and "integrations" in methods:
95
+ methods.remove("integrations")
96
+
88
97
  # Try to download as medium
89
- if medium := await download_item(url, session=session):
90
- return MultimodalSequence(medium)
98
+ if format != "html":
99
+ if medium := await download_item(url, session=session):
100
+ return MultimodalSequence(medium)
91
101
 
92
102
  # Define available retrieval methods
93
103
  method_map = {
94
104
  "integrations": lambda: retrieve_via_integration(url, session),
95
105
  "firecrawl": lambda: fire.scrape(url, remove_urls=remove_urls,
96
- session=session, actions=actions),
97
- "decodo": lambda: decodo.scrape(url, remove_urls, session),
106
+ session=session, format=format, actions=actions),
107
+ "decodo": lambda: decodo.scrape(url, remove_urls, session, format=format),
98
108
  }
99
109
 
100
110
  # Try each method in the specified order until one succeeds
@@ -104,7 +114,11 @@ async def _retrieve_single(
104
114
  continue
105
115
 
106
116
  logger.debug(f"Trying method: {method_name}")
107
- 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
108
122
 
109
123
  if result is not None:
110
124
  logger.debug(f"Successfully retrieved with method: {method_name}")
@@ -39,8 +39,9 @@ class Decodo:
39
39
  async def scrape(self, url: str,
40
40
  remove_urls: bool,
41
41
  session: aiohttp.ClientSession,
42
+ format: str,
42
43
  enable_js: bool = True,
43
- timeout: int = 30) -> Optional[MultimodalSequence]:
44
+ timeout: int = 30) -> Optional[MultimodalSequence | str]:
44
45
  """Downloads the contents of the specified webpage using Decodo's API.
45
46
 
46
47
  Args:
@@ -70,7 +71,10 @@ class Decodo:
70
71
  html = await self._call_decodo(url, session, enable_js=False, timeout=timeout)
71
72
 
72
73
  if html:
73
- return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
74
+ if format == "html":
75
+ return html
76
+ else:
77
+ return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
74
78
  return None
75
79
 
76
80
  async def _call_decodo(self, url: str,
@@ -107,8 +111,6 @@ class Decodo:
107
111
  # Create basic auth
108
112
  auth = aiohttp.BasicAuth(self.username, self.password)
109
113
 
110
- # logger.debug(f"Decodo request payload: {payload}")
111
-
112
114
  try:
113
115
  async with session.post(
114
116
  self.DECODO_API_URL,
@@ -117,26 +119,17 @@ class Decodo:
117
119
  auth=auth,
118
120
  timeout=aiohttp.ClientTimeout(total=timeout)
119
121
  ) as response:
120
-
122
+ # Validate response health
121
123
  if response.status != 200:
122
- # Try to get the error message from response
123
- try:
124
- error_body = await response.json()
125
- error_msg = error_body.get('message', error_body)
126
- except:
127
- error_msg = await response.text()
128
-
129
- logger.info(
130
- f"Failed to scrape {url} with Decodo\n"
131
- f"Status code: {response.status} - Reason: {response.reason}\n"
132
- f"Error details: {error_msg}"
133
- )
134
-
124
+ logger.debug("Communication with Decodo API failed.")
135
125
  match response.status:
126
+ case 400:
127
+ logger.debug("Error 400: Bad request. If you use JavaScript, make sure you have the "
128
+ "Advanced plan subscription.")
136
129
  case 401:
137
130
  logger.error("Error 401: Unauthorized. Check your Decodo credentials.")
138
131
  case 402:
139
- logger.debug("Error 402: Payment required. Check your Decodo subscription.")
132
+ logger.error("Error 402: Payment required. Check your Decodo subscription.")
140
133
  case 403:
141
134
  logger.debug("Error 403: Forbidden.")
142
135
  case 408:
@@ -153,6 +146,13 @@ class Decodo:
153
146
  # Parse response
154
147
  json_response = await response.json()
155
148
 
149
+ # Validate if scrape was successful
150
+ if json_response.get("status") == "failed":
151
+ status_code = json_response.get("status_code")
152
+ message = json_response.get("message")
153
+ logger.info(f"Decodo failed to scrape. Error {status_code}: {message}")
154
+ return None
155
+
156
156
  # Extract HTML content from results
157
157
  if "results" in json_response and len(json_response["results"]) > 0:
158
158
  result = json_response["results"][0]
@@ -65,7 +65,8 @@ class Firecrawl:
65
65
  url: str,
66
66
  remove_urls: bool,
67
67
  session: aiohttp.ClientSession,
68
- **kwargs) -> Optional[MultimodalSequence]:
68
+ format: str,
69
+ **kwargs) -> Optional[MultimodalSequence | str]:
69
70
  if is_no_bot_site(url):
70
71
  return None
71
72
 
@@ -84,7 +85,10 @@ class Firecrawl:
84
85
  html = document.html
85
86
 
86
87
  if html:
87
- return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
88
+ if format == "html":
89
+ return html
90
+ else:
91
+ return await to_multimodal_sequence(html, remove_urls=remove_urls, session=session)
88
92
 
89
93
 
90
94
  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
@@ -159,7 +160,8 @@ def from_base64(b64_data: str, mime_type: str = "image/jpeg") -> Optional[Item]:
159
160
  else:
160
161
  raise ValueError(f"Unsupported media type: {mime_type}")
161
162
  except binascii.Error: # base64 validation failed
162
- return
163
+ return None
164
+ except UnidentifiedImageError: # Pillow could not identify image format
165
+ return None
163
166
  except Exception as e:
164
- logger.warning(f"Error decoding base64 data!\n"
165
- f"{type(e).__name__}: {e}")
167
+ 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://verafiles.org"
6
+ result = asyncio.run(retrieve(url))
7
+ print(result)
@@ -1,25 +1,39 @@
1
1
  import pytest
2
+ from ezmm import MultimodalSequence
2
3
 
3
4
  from scrapemm import retrieve
4
5
 
5
6
 
6
7
  @pytest.mark.asyncio
7
8
  @pytest.mark.parametrize("url", [
8
- "https://www.zeit.de/politik/deutschland/2025-07/spionage-iran-festnahme-anschlag-juden-berlin-daenemark",
9
- "https://factnameh.com/fa/fact-checks/2025-04-16-araghchi-witkoff-fake-photo",
10
- "https://www.thip.media/health-news-fact-check/fact-check-can-a-kalava-on-the-wrist-prevent-paralysis/74724/",
11
9
  "https://www.vishvasnews.com/viral/fact-check-upsc-has-not-reduced-the-maximum-age-limit-for-ias-and-ips-exams/",
12
- "https://www.thip.media/health-news-fact-check/fact-check-does-wrapping-body-with-banana-leaves-help-with-obesity-and-indigestion/71333/",
13
10
  "https://health.medicaldialogues.in/fact-check/brain-health-fact-check/fact-check-is-sprite-the-best-remedy-for-headaches-in-the-world-140368",
14
11
  "https://www.washingtonpost.com/politics/2024/05/15/bidens-false-claim-that-inflation-was-9-percent-when-he-took-office/",
15
12
  "https://assamese.factcrescendo.com/viral-claim-that-the-video-shows-the-incident-from-uttar-pradesh-and-the-youth-on-the-bike-and-the-youth-being-beaten-and-taken-away-by-the-police-are-the-same-youth-named-abdul-is-false/",
16
13
  "https://factuel.afp.com/doc.afp.com.43ZN7NP",
17
14
  "https://leadstories.com/365cb414b83e29d26fecae374d55c743a3eac4c7.png",
18
15
  ])
19
- async def test_generic_retrieval(url):
20
- result = await retrieve(url)
16
+ @pytest.mark.parametrize("method", ["firecrawl", "decodo"])
17
+ async def test_generic_retrieval(url, method):
18
+ result = await retrieve(url, methods=[method])
19
+ print(result)
20
+ assert result
21
+ assert isinstance(result, MultimodalSequence)
22
+ assert result.has_images()
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ @pytest.mark.parametrize("url", [
27
+ "https://www.zeit.de/politik/deutschland/2025-07/spionage-iran-festnahme-anschlag-juden-berlin-daenemark",
28
+ "https://factnameh.com/fa/fact-checks/2025-04-16-araghchi-witkoff-fake-photo",
29
+ "https://www.thip.media/health-news-fact-check/fact-check-can-a-kalava-on-the-wrist-prevent-paralysis/74724/",
30
+ ])
31
+ @pytest.mark.parametrize("method", ["firecrawl", "decodo"])
32
+ async def test_html_retrieval(url, method):
33
+ result = await retrieve(url, format="html", methods=[method])
21
34
  print(result)
22
35
  assert result
36
+ assert isinstance(result, str)
23
37
 
24
38
 
25
39
  @pytest.mark.asyncio
@@ -75,7 +89,7 @@ async def test_instagram_retrieval(url):
75
89
  @pytest.mark.parametrize("url", [
76
90
  "https://www.facebook.com/reel/1089214926521000", # video
77
91
  "https://www.facebook.com/reel/3466446073497470", # video, restricted for misinformation
78
- "https://www.facebook.com/groups/1973976962823632/posts/3992825270938781/", # video, restricted for misinformation
92
+ "https://www.facebook.com/groups/1973976962823632/posts/3992825270938781/", # video, restricted for misinformation
79
93
  "https://www.facebook.com/photo/?fbid=1721085455188778&set=a.107961589834514&_rdc=1&_rdr", # image
80
94
  ])
81
95
  async def test_facebook_retrieval(url):
@@ -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