From 63a46a8e0bec5b2312427a61a14a434bc2799f1b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 23 Sep 2025 18:32:56 +0300 Subject: [PATCH 01/21] fix(parser): An encoding issue with converting bytes to string on some encoding types Removing that `invalid start byte` annoying bug --- scrapling/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapling/parser.py b/scrapling/parser.py index f0b9e54..776dd55 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -341,7 +341,7 @@ class Selector(SelectorsGeneration): """Return the inner HTML code of the element""" content = tostring(self._root, encoding=self.encoding, method="html", with_tail=False) if isinstance(content, bytes): - content = content.decode("utf-8") + content = content.strip().decode(self.encoding) return TextHandler(content) @property @@ -359,7 +359,7 @@ class Selector(SelectorsGeneration): with_tail=False, ) if isinstance(content, bytes): - content = content.decode("utf-8") + content = content.strip().decode(self.encoding) return TextHandler(content) def has_class(self, class_name: str) -> bool: From 3da806210b81ac8c1588af88f03437f25ef24d08 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 23 Sep 2025 18:34:54 +0300 Subject: [PATCH 02/21] perf: General code restructure to not use more than needed memory --- scrapling/cli.py | 3 ++- scrapling/core/shell.py | 3 ++- scrapling/core/storage.py | 3 ++- scrapling/core/utils/__init__.py | 1 - scrapling/parser.py | 4 ++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scrapling/cli.py b/scrapling/cli.py index 12b96c6..6f9487d 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -2,8 +2,9 @@ from pathlib import Path from subprocess import check_output from sys import executable as python_executable +from scrapling.core.utils import log from scrapling.engines.toolbelt.custom import Response -from scrapling.core.utils import log, _CookieParser, _ParseHeaders +from scrapling.core.utils._shell import _CookieParser, _ParseHeaders from scrapling.core._types import List, Optional, Dict, Tuple, Any, Callable from orjson import loads as json_loads, JSONDecodeError diff --git a/scrapling/core/shell.py b/scrapling/core/shell.py index f9f790f..5ef4458 100644 --- a/scrapling/core/shell.py +++ b/scrapling/core/shell.py @@ -22,10 +22,11 @@ from logging import ( from orjson import loads as json_loads, JSONDecodeError from scrapling import __version__ +from scrapling.core.utils import log from scrapling.parser import Selector, Selectors from scrapling.core.custom_types import TextHandler from scrapling.engines.toolbelt.custom import Response -from scrapling.core.utils import log, _ParseHeaders, _CookieParser +from scrapling.core.utils._shell import _ParseHeaders, _CookieParser from scrapling.core._types import ( Optional, Dict, diff --git a/scrapling/core/storage.py b/scrapling/core/storage.py index 03c05a2..e832cf0 100644 --- a/scrapling/core/storage.py +++ b/scrapling/core/storage.py @@ -6,7 +6,6 @@ from sqlite3 import connect as db_connect from orjson import dumps, loads from lxml.html import HtmlElement -from tldextract import extract as tld from scrapling.core.utils import _StorageTools, log from scrapling.core._types import Dict, Optional, Any @@ -26,6 +25,8 @@ class StorageSystemMixin(ABC): # pragma: no cover return default_value try: + from tldextract import extract as tld + extracted = tld(self.url) return extracted.top_domain_under_public_suffix or extracted.domain or default_value except AttributeError: diff --git a/scrapling/core/utils/__init__.py b/scrapling/core/utils/__init__.py index dc95705..6ae80fe 100644 --- a/scrapling/core/utils/__init__.py +++ b/scrapling/core/utils/__init__.py @@ -7,4 +7,3 @@ from ._utils import ( clean_spaces, html_forbidden, ) -from ._shell import _CookieParser, _ParseHeaders diff --git a/scrapling/parser.py b/scrapling/parser.py index 776dd55..6d0a575 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -1,8 +1,8 @@ -import re from pathlib import Path from inspect import signature from urllib.parse import urljoin from difflib import SequenceMatcher +from re import Pattern as re_Pattern from lxml.html import HtmlElement, HtmlMixin, HTMLParser from cssselect import SelectorError, SelectorSyntaxError, parse as split_selectors @@ -751,7 +751,7 @@ class Selector(SelectorsGeneration): ) attributes.update(arg) - elif isinstance(arg, re.Pattern): + elif isinstance(arg, re_Pattern): patterns.add(arg) elif callable(arg): From 716a5c3572424f3569ce63b6340cb3e5f9f91211 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 02:53:51 +0300 Subject: [PATCH 03/21] feat(cf solver): Make solver able to handle Turnstile and interstitial --- scrapling/engines/_browsers/_base.py | 17 ++++---- scrapling/engines/_browsers/_camoufox.py | 50 +++++++++++++++--------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/scrapling/engines/_browsers/_base.py b/scrapling/engines/_browsers/_base.py index 69681ee..d1a872d 100644 --- a/scrapling/engines/_browsers/_base.py +++ b/scrapling/engines/_browsers/_base.py @@ -12,17 +12,13 @@ from camoufox.utils import ( installed_verstr as camoufox_version, ) -from scrapling.engines.toolbelt.navigation import intercept_route, async_intercept_route -from scrapling.core._types import ( - Any, - Dict, - Optional, -) from ._page import PageInfo, PagePool -from ._config_tools import _compiled_stealth_scripts -from ._config_tools import _launch_kwargs, _context_kwargs +from scrapling.parser import Selector +from scrapling.core._types import Dict, Optional from scrapling.engines.toolbelt.fingerprints import get_os_name from ._validators import validate, PlaywrightConfig, CamoufoxConfig +from ._config_tools import _compiled_stealth_scripts, _launch_kwargs, _context_kwargs +from scrapling.engines.toolbelt.navigation import intercept_route, async_intercept_route __ff_version_str__ = camoufox_version().split(".", 1)[0] @@ -268,4 +264,9 @@ class StealthySessionMixin: if f"cType: '{ctype}'" in page_content: return ctype + # Check if turnstile captcha is embedded inside the page (Usually inside a closed Shadow iframe) + selector = Selector(content=page_content) + if selector.css('script[src*="challenges.cloudflare.com/turnstile/v"]'): + return "embedded" + return None diff --git a/scrapling/engines/_browsers/_camoufox.py b/scrapling/engines/_browsers/_camoufox.py index 5f0e795..207be90 100644 --- a/scrapling/engines/_browsers/_camoufox.py +++ b/scrapling/engines/_browsers/_camoufox.py @@ -237,26 +237,33 @@ class StealthySession(StealthySessionMixin, SyncSession): return else: - while "Verifying you are human." in self._get_page_content(page): - # Waiting for the verify spinner to disappear, checking every 1s if it disappeared - page.wait_for_timeout(500) + box_selector = "#cf_turnstile div, #cf-turnstile div, .turnstile>div>div" + if challenge_type != "embedded": + box_selector = ".main-content p+div>div>div" + while "Verifying you are human." in self._get_page_content(page): + # Waiting for the verify spinner to disappear, checking every 1s if it disappeared + page.wait_for_timeout(500) iframe = page.frame(url=__CF_PATTERN__) if iframe is None: - log.info("Didn't find Cloudflare iframe!") + log.error("Didn't find Cloudflare iframe!") return - while not iframe.frame_element().is_visible(): - # Double-checking that the iframe is loaded - page.wait_for_timeout(500) + if challenge_type != "embedded": + while not iframe.frame_element().is_visible(): + # Double-checking that the iframe is loaded + page.wait_for_timeout(500) + iframe.wait_for_load_state(state="domcontentloaded") + iframe.wait_for_load_state("networkidle") # Calculate the Captcha coordinates for any viewport - outer_box = page.locator(".main-content p+div>div>div").bounding_box() + outer_box = page.locator(box_selector).last.bounding_box() captcha_x, captcha_y = outer_box["x"] + 26, outer_box["y"] + 25 # Move the mouse to the center of the window, then press and hold the left mouse button page.mouse.click(captcha_x, captcha_y, delay=60, button="left") - page.locator(".zone-name-title").wait_for(state="hidden") + if challenge_type != "embedded": + page.locator(".zone-name-title").wait_for(state="hidden") page.wait_for_load_state(state="domcontentloaded") log.info("Cloudflare captcha is solved") @@ -556,26 +563,33 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession): return else: - while "Verifying you are human." in (await self._get_page_content(page)): - # Waiting for the verify spinner to disappear, checking every 1s if it disappeared - await page.wait_for_timeout(500) + box_selector = "#cf_turnstile div, #cf-turnstile div, .turnstile>div>div" + if challenge_type != "embedded": + box_selector = ".main-content p+div>div>div" + while "Verifying you are human." in (await self._get_page_content(page)): + # Waiting for the verify spinner to disappear, checking every 1s if it disappeared + await page.wait_for_timeout(500) iframe = page.frame(url=__CF_PATTERN__) if iframe is None: - log.info("Didn't find Cloudflare iframe!") + log.error("Didn't find Cloudflare iframe!") return - while not await (await iframe.frame_element()).is_visible(): - # Double-checking that the iframe is loaded - await page.wait_for_timeout(500) + if challenge_type != "embedded": + while not await (await iframe.frame_element()).is_visible(): + # Double-checking that the iframe is loaded + await page.wait_for_timeout(500) + await iframe.wait_for_load_state(state="domcontentloaded") + await iframe.wait_for_load_state("networkidle") # Calculate the Captcha coordinates for any viewport - outer_box = await page.locator(".main-content p+div>div>div").bounding_box() + outer_box = await page.locator(box_selector).last.bounding_box() captcha_x, captcha_y = outer_box["x"] + 26, outer_box["y"] + 25 # Move the mouse to the center of the window, then press and hold the left mouse button await page.mouse.click(captcha_x, captcha_y, delay=60, button="left") - await page.locator(".zone-name-title").wait_for(state="hidden") + if challenge_type != "embedded": + await page.locator(".zone-name-title").wait_for(state="hidden") await page.wait_for_load_state(state="domcontentloaded") log.info("Cloudflare captcha is solved") From 292208e22d322a93035a25e2633d35db919d9c10 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 04:54:27 +0300 Subject: [PATCH 04/21] build: update bandit rules --- .bandit.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.bandit.yml b/.bandit.yml index 1773bf5..5acd372 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -5,4 +5,5 @@ skips: - B403 # We are using pickle for tests only - B404 # Using subprocess library - B602 # subprocess call with shell=True identified -- B110 # Try, Except, Pass detected. \ No newline at end of file +- B110 # Try, Except, Pass detected. +- B104 # Possible binding to all interfaces. \ No newline at end of file From 4a661b4875358806d6e86263318d62ca67773609 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 04:54:35 +0300 Subject: [PATCH 05/21] feat: Make mcp able to use http transport --- scrapling/cli.py | 20 ++++++++++++++++++-- scrapling/core/ai.py | 14 +++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/scrapling/cli.py b/scrapling/cli.py index 6f9487d..44fcd33 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -136,10 +136,26 @@ def install(force): # pragma: no cover @command(help="Run Scrapling's MCP server (Check the docs for more info).") -def mcp(): +@option( + "--http", + type=bool, + default=False, + help="Whether to run the MCP server in streamable-http transport or leave it as stdio (Default: False)", +) +@option( + "--host", + type=str, + default="0.0.0.0", + help="The host to use if streamable-http transport is enabled (Default: '0.0.0.0')", +) +@option( + "--port", type=int, default=8000, help="The port to use if streamable-http transport is enabled (Default: 8000)" +) +def mcp(http, host, port): from scrapling.core.ai import ScraplingMCPServer - ScraplingMCPServer().serve() + server = ScraplingMCPServer(host, port) + server.run(transport="stdio" if not http else "streamable-http") @command(help="Interactive scraping console") diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index ae517fa..26eb564 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -41,10 +41,9 @@ def _ContentTranslator(content: Generator[str, None, None], page: _ScraplingResp return ResponseModel(status=page.status, content=[result for result in content], url=page.url) -class ScraplingMCPServer: - _server = FastMCP(name="Scrapling") +def ScraplingMCPServer(host: str, port: int) -> FastMCP: + _server = FastMCP(name="Scrapling", host=host, port=port) - @staticmethod @_server.tool() def get( url: str, @@ -123,7 +122,6 @@ class ScraplingMCPServer: page, ) - @staticmethod @_server.tool() async def bulk_get( urls: Tuple[str, ...], @@ -210,7 +208,6 @@ class ScraplingMCPServer: for page in responses ] - @staticmethod @_server.tool() async def fetch( url: str, @@ -299,7 +296,6 @@ class ScraplingMCPServer: page, ) - @staticmethod @_server.tool() async def bulk_fetch( urls: Tuple[str, ...], @@ -393,7 +389,6 @@ class ScraplingMCPServer: for page in responses ] - @staticmethod @_server.tool() async def stealthy_fetch( url: str, @@ -493,7 +488,6 @@ class ScraplingMCPServer: page, ) - @staticmethod @_server.tool() async def bulk_stealthy_fetch( urls: Tuple[str, ...], @@ -598,6 +592,4 @@ class ScraplingMCPServer: for page in responses ] - def serve(self): - """Serve the MCP server.""" - self._server.run(transport="stdio") + return _server From 8a867fc68e5b2244841f74a0853f8d852ef2353d Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 04:55:00 +0300 Subject: [PATCH 06/21] feat: Add docker support --- .dockerignore | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 37 +++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be64db5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,110 @@ +# Github +.github/ + +# docs +docs/ +images/ +.cache/ +.claude/ + +# cached files +__pycache__/ +*.py[cod] +.cache +.DS_Store +*~ +.*.sw[po] +.build +.ve +.env +.pytest +.benchmarks +.bootstrap +.appveyor.token +*.bak +*.db +*.db-* + +# installation package +*.egg-info/ +dist/ +build/ + +# environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# C extensions +*.so + +# pycharm +.idea/ + +# vscode +*.code-workspace + +# Packages +*.egg +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +wheelhouse +develop-eggs +.installed.cfg +lib +lib64 +venv*/ +.venv*/ +pyvenv*/ +pip-wheel-metadata/ +poetry.lock + +# Installer logs +pip-log.txt + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json +mypy.ini + +# test caches +.tox/ +.pytest_cache/ +.coverage +htmlcov +report.xml +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Buildout +.mr.developer.cfg + +# IDE project files +.project +.pydevproject +.idea +*.iml +*.komodoproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build +public/ +web/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3524d3e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.12-slim-trixie AS builder +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +ADD . /app + +WORKDIR /app + +# Install dependencies +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --no-install-project --all-extras --compile-bytecode + +# Install all browsers and their deps +RUN uv run playwright install-deps chromium firefox +RUN uv run playwright install chromium +RUN uv run camoufox fetch --browserforge + +# Sync the project +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --all-extras --compile-bytecode + +# Clean up to save space +RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Expose port for MCP server HTTP transport +EXPOSE 8000 + +# Set entrypoint to run scrapling +ENTRYPOINT ["uv", "run", "scrapling"] + +# Default command (can be overridden) +CMD ["--help"] \ No newline at end of file From 0b729176bc74877a8c93731b57fd35b6278afeed Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 04:56:01 +0300 Subject: [PATCH 07/21] ops: add workflow to build and push docker images --- .github/workflows/docker-build.yml | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..1cfdaee --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,67 @@ +name: Build and Push Docker Image + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Docker image tag' + required: true + default: 'latest' + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository_owner }}/scrapling + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUILDKIT_INLINE_CACHE=1 + + - name: Image digest + run: echo ${{ steps.build.outputs.digest }} \ No newline at end of file From 74f20d2a0e7f36fa28fba03c3fb7b545dbf10ad4 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 20:14:04 +0300 Subject: [PATCH 08/21] refactor: better implementation for the mcp mode --- scrapling/cli.py | 4 ++-- scrapling/core/ai.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/scrapling/cli.py b/scrapling/cli.py index 44fcd33..04938d0 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -154,8 +154,8 @@ def install(force): # pragma: no cover def mcp(http, host, port): from scrapling.core.ai import ScraplingMCPServer - server = ScraplingMCPServer(host, port) - server.run(transport="stdio" if not http else "streamable-http") + server = ScraplingMCPServer() + server.serve(http, host, port) @command(help="Interactive scraping console") diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index 26eb564..b9503a3 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -41,10 +41,8 @@ def _ContentTranslator(content: Generator[str, None, None], page: _ScraplingResp return ResponseModel(status=page.status, content=[result for result in content], url=page.url) -def ScraplingMCPServer(host: str, port: int) -> FastMCP: - _server = FastMCP(name="Scrapling", host=host, port=port) - - @_server.tool() +class ScraplingMCPServer: + @staticmethod def get( url: str, impersonate: Optional[BrowserTypeLiteral] = "chrome", @@ -122,7 +120,7 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: page, ) - @_server.tool() + @staticmethod async def bulk_get( urls: Tuple[str, ...], impersonate: Optional[BrowserTypeLiteral] = "chrome", @@ -208,7 +206,7 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: for page in responses ] - @_server.tool() + @staticmethod async def fetch( url: str, extraction_type: extraction_types = "markdown", @@ -296,7 +294,7 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: page, ) - @_server.tool() + @staticmethod async def bulk_fetch( urls: Tuple[str, ...], extraction_type: extraction_types = "markdown", @@ -389,7 +387,7 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: for page in responses ] - @_server.tool() + @staticmethod async def stealthy_fetch( url: str, extraction_type: extraction_types = "markdown", @@ -488,7 +486,7 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: page, ) - @_server.tool() + @staticmethod async def bulk_stealthy_fetch( urls: Tuple[str, ...], extraction_type: extraction_types = "markdown", @@ -592,4 +590,22 @@ def ScraplingMCPServer(host: str, port: int) -> FastMCP: for page in responses ] - return _server + def serve(self, http: bool, host: str, port: int): + """Serve the MCP server.""" + server = FastMCP(name="Scrapling", host=host, port=port) + server.add_tool(self.get, title="get", description=self.get.__doc__, structured_output=True) + server.add_tool(self.bulk_get, title="bulk_get", description=self.bulk_get.__doc__, structured_output=True) + server.add_tool(self.fetch, title="fetch", description=self.fetch.__doc__, structured_output=True) + server.add_tool( + self.bulk_fetch, title="bulk_fetch", description=self.bulk_fetch.__doc__, structured_output=True + ) + server.add_tool( + self.stealthy_fetch, title="stealthy_fetch", description=self.stealthy_fetch.__doc__, structured_output=True + ) + server.add_tool( + self.bulk_stealthy_fetch, + title="bulk_stealthy_fetch", + description=self.bulk_stealthy_fetch.__doc__, + structured_output=True, + ) + server.run(transport="stdio" if not http else "streamable-http") From 6f0ce428aaad7effdabb84075f71e178c3057402 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 20:14:17 +0300 Subject: [PATCH 09/21] tests: update tests for mcp --- tests/ai/test_ai_mcp.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/ai/test_ai_mcp.py b/tests/ai/test_ai_mcp.py index d7f8b84..4a9e308 100644 --- a/tests/ai/test_ai_mcp.py +++ b/tests/ai/test_ai_mcp.py @@ -1,6 +1,5 @@ import pytest import pytest_httpbin -from unittest.mock import Mock, patch from scrapling.core.ai import ScraplingMCPServer, ResponseModel @@ -17,11 +16,6 @@ class TestMCPServer: def server(self): return ScraplingMCPServer() - def test_server_creation(self, server): - """Test server instance creation""" - assert server._server is not None - assert server._server.name == "Scrapling" - def test_get_tool(self, server, test_url): """Test the get tool method""" result = server.get(url=test_url, extraction_type="markdown") @@ -62,9 +56,3 @@ class TestMCPServer: """Test the bulk_stealthy_fetch tool method""" result = await server.bulk_stealthy_fetch(urls=(test_url, test_url), headless=True) assert all(isinstance(r, ResponseModel) for r in result) - - def test_serve_method(self, server): - """Test the serve method""" - with patch.object(server._server, 'run') as mock_run: - server.serve() - mock_run.assert_called_once_with(transport="stdio") From 2bad72b77a0ef38b45f3d8f8b559cc19a3f63b6b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 21:19:51 +0300 Subject: [PATCH 10/21] fix: correct `http` option type in the mcp server --- scrapling/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapling/cli.py b/scrapling/cli.py index 04938d0..5824599 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -138,7 +138,7 @@ def install(force): # pragma: no cover @command(help="Run Scrapling's MCP server (Check the docs for more info).") @option( "--http", - type=bool, + is_flag=True, default=False, help="Whether to run the MCP server in streamable-http transport or leave it as stdio (Default: False)", ) From 840aef68844c3c35c3ca7adf90e36c1694e2f021 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 28 Sep 2025 22:04:09 +0300 Subject: [PATCH 11/21] build: Pump up version and deps --- pyproject.toml | 4 ++-- scrapling/__init__.py | 2 +- setup.cfg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1175b63..64a36a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "lxml>=6.0.1", + "lxml>=6.0.2", "cssselect>=1.3.0", "orjson>=3.11.3", "tldextract>=5.3.0", @@ -73,7 +73,7 @@ fetchers = [ "msgspec>=0.19.0", ] ai = [ - "mcp>=1.14.1", + "mcp>=1.15.0", "markdownify>=1.2.0", "scrapling[fetchers]", ] diff --git a/scrapling/__init__.py b/scrapling/__init__.py index d797639..c4bd97d 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -1,5 +1,5 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" -__version__ = "0.3.5" +__version__ = "0.3.6" __copyright__ = "Copyright (c) 2024 Karim Shoair" diff --git a/setup.cfg b/setup.cfg index 629514a..6b0e5a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scrapling -version = 0.3.5 +version = 0.3.6 author = Karim Shoair author_email = karim.shoair@pm.me description = Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be! From 0cf3bcf3aa1648b0ce151d9a6ff06ed07cc71f8c Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 29 Sep 2025 02:53:06 +0300 Subject: [PATCH 12/21] ops: improve the layering of the docker image --- Dockerfile | 37 ++++++++++++++++++++----------------- pyproject.toml | 6 ++---- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3524d3e..5515bf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,34 @@ -FROM python:3.12-slim-trixie AS builder +FROM python:3.12-slim-trixie COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Set environment variables -ENV DEBIAN_FRONTEND=noninteractive -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -ADD . /app +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 WORKDIR /app -# Install dependencies +# Copy dependency file first for better layer caching +COPY pyproject.toml ./ + +# Install dependencies only RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --no-install-project --all-extras --compile-bytecode -# Install all browsers and their deps -RUN uv run playwright install-deps chromium firefox -RUN uv run playwright install chromium -RUN uv run camoufox fetch --browserforge +# Copy source code +COPY . . -# Sync the project +# Install browsers and project in one optimized layer RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --all-extras --compile-bytecode - -# Clean up to save space -RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && \ + uv run playwright install-deps chromium firefox && \ + uv run playwright install chromium && \ + uv run camoufox fetch --browserforge && \ + uv sync --all-extras --compile-bytecode && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Expose port for MCP server HTTP transport EXPOSE 8000 diff --git a/pyproject.toml b/pyproject.toml index 64a36a7..8b2a555 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "scrapling" -dynamic = ["version"] +# Static version instead of dynamic version so we can get better layer caching while building docker, check the docker file to understand +version = "0.3.6" description = "Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be!" readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} @@ -99,9 +100,6 @@ scrapling = "scrapling.cli:main" zip-safe = false include-package-data = true -[tool.setuptools.dynamic] -version = {attr = "scrapling.__version__"} - [tool.setuptools.packages.find] where = ["."] include = ["scrapling*"] \ No newline at end of file From 8ad7cd6343d7a908319155430018fff82a683274 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 29 Sep 2025 03:57:40 +0300 Subject: [PATCH 13/21] docs: Update all pages/docstrings to reflect recent changes --- README.md | 17 ++++-- docs/ai/mcp-server.md | 41 ++++++++++++-- docs/cli/extract-commands.md | 5 +- docs/fetching/dynamic.md | 60 ++++++++++----------- docs/fetching/stealthy.md | 68 ++++++++++++------------ docs/index.md | 4 +- scrapling/cli.py | 2 +- scrapling/core/ai.py | 4 +- scrapling/engines/_browsers/_camoufox.py | 8 +-- scrapling/fetchers.py | 4 +- 10 files changed, 129 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index ce06e5c..f18438e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Scrapling isn't just another Web Scraping library. It's the first **adaptive** scraping library that learns from website changes and evolves with them. While other libraries break when websites update their structure, Scrapling automatically relocates your elements and keeps your scrapers running. -Built for the modern Web, Scrapling has its own rapid parsing engine and its fetchers to handle all Web Scraping challenges you are facing or will face. Built by Web Scrapers for Web Scrapers and regular users, there's something for everyone. +Built for the modern Web, Scrapling features its own rapid parsing engine and fetchers to handle all Web Scraping challenges you face or will face. Built by Web Scrapers for Web Scrapers and regular users, there's something for everyone. ```python >> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, DynamicFetcher @@ -87,7 +87,7 @@ Built for the modern Web, Scrapling has its own rapid parsing engine and its fet ### Advanced Websites Fetching with Session Support - **HTTP Requests**: Fast and stealthy HTTP requests with the `Fetcher` class. Can impersonate browsers' TLS fingerprint, headers, and use HTTP3. - **Dynamic Loading**: Fetch dynamic websites with full browser automation through the `DynamicFetcher` class supporting Playwright's Chromium, real Chrome, and custom stealth mode. -- **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` using a modified version of Firefox and fingerprint spoofing. Can bypass all levels of Cloudflare's Turnstile with automation easily. +- **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` using a modified version of Firefox and fingerprint spoofing. Can bypass all types of Cloudflare's Turnstile and Interstitial with automation easily. - **Session Management**: Persistent session support with `FetcherSession`, `StealthySession`, and `DynamicSession` classes for cookie and state management across requests. - **Async Support**: Complete async support across all fetchers and dedicated async session classes. @@ -235,11 +235,11 @@ scrapling extract stealthy-fetch 'https://nopecha.com/demo/cloudflare' captchas. ``` > [!NOTE] -> There are many additional features, but we want to keep this page short, like the MCP server and the interactive Web Scraping Shell. Check out the full documentation [here](https://scrapling.readthedocs.io/en/latest/) +> There are many additional features, but we want to keep this page concise, such as the MCP server and the interactive Web Scraping Shell. Check out the full documentation [here](https://scrapling.readthedocs.io/en/latest/) ## Performance Benchmarks -Scrapling isn't just powerfulβ€”it's also blazing fast, and the updates since version 0.3 deliver exceptional performance improvements across all operations! +Scrapling isn't just powerfulβ€”it's also blazing fast, and the updates since version 0.3 have delivered exceptional performance improvements across all operations. ### Text Extraction Speed Test (5000 nested elements) @@ -302,6 +302,13 @@ Starting with v0.3.2, this installation only includes the parser engine and its ``` Don't forget that you need to install the browser dependencies with `scrapling install` after any of these extras (if you didn't already) +### Docker +You can also install a Docker image with all extras and browsers with the following command: +```bash +docker pull scrapling +``` +This image is automatically built and pushed to Docker Hub through GitHub actions right here. + ## Contributing We welcome contributions! Please read our [contributing guidelines](https://github.com/D4Vinci/Scrapling/blob/main/CONTRIBUTING.md) before getting started. @@ -309,7 +316,7 @@ We welcome contributions! Please read our [contributing guidelines](https://gith ## Disclaimer > [!CAUTION] -> This library is provided for educational and research purposes only. By using this library, you agree to comply with local and international data scraping and privacy laws. The authors and contributors are not responsible for any misuse of this software. Always respect website terms of service and robots.txt files. +> This library is provided for educational and research purposes only. By using this library, you agree to comply with local and international data scraping and privacy laws. The authors and contributors are not responsible for any misuse of this software. Always respect the terms of service of websites and robots.txt files. ## License diff --git a/docs/ai/mcp-server.md b/docs/ai/mcp-server.md index 19088ec..a8290d9 100644 --- a/docs/ai/mcp-server.md +++ b/docs/ai/mcp-server.md @@ -17,20 +17,20 @@ The Scrapling MCP Server provides six powerful tools for web scraping: - **`bulk_fetch`**: An async version of the above tool that allows scraping of multiple URLs in different browser tabs at the same time! ### πŸ”’ Stealth Scraping -- **`stealthy_fetch`**: Uses our modified version of Camoufox browser to bypass Cloudflare Turnstile and other anti-bot systems with complete control over the request/browser! +- **`stealthy_fetch`**: Uses our modified version of Camoufox browser to bypass Cloudflare Turnstile/Interstitial and other anti-bot systems with complete control over the request/browser! - **`bulk_stealthy_fetch`**: An async version of the above tool that allows stealth scraping of multiple URLs in different browser tabs at the same time! ### Key Capabilities - **Smart Content Extraction**: Convert web pages/elements to Markdown, HTML, or extract a clean version of the text content - **CSS Selector Support**: Use the Scrapling engine to target specific elements with precision before handing the content to the AI -- **Anti-Bot Bypass**: Handle Cloudflare Turnstile and other protections +- **Anti-Bot Bypass**: Handle Cloudflare Turnstile, Interstitial, and other protections - **Proxy Support**: Use proxies for anonymity and geo-targeting - **Browser Impersonation**: Mimic real browsers with TLS fingerprinting, real browser headers matching that version, and more - **Parallel Processing**: Scrape multiple URLs concurrently for efficiency #### But why use Scrapling MCP Server instead of other available tools? -Aside from its stealth capabilities and ability to bypass Cloudflare Turnstile, Scrapling's server is the only one that allows you to pass a CSS selector in the prompt to extract specific elements before handing the content to the AI. +Aside from its stealth capabilities and ability to bypass Cloudflare Turnstile/Interstitial, Scrapling's server is the only one that allows you to pass a CSS selector in the prompt to extract specific elements before handing the content to the AI. The way other servers work is that they extract the content, then pass it all to the AI to extract the fields you want. This causes the AI to consume a lot more tokens that are not needed (from irrelevant content). Scrapling solves this problem by allowing you to pass a CSS selector to narrow down the content you want before passing it to the AI, which makes the whole process much faster and more efficient. @@ -48,6 +48,11 @@ pip install "scrapling[ai]" scrapling install ``` +Or use the Docker image directly: +```bash +docker pull scrapling +``` + ## Setting up the MCP Server Here we will explain how to add Scrapling MCP Server to [Claude Desktop](https://claude.ai/download) and [Claude Code](https://www.anthropic.com/claude-code), but the same logic applies to any other chatbot that supports MCP: @@ -101,6 +106,20 @@ For me, on my Mac, it returned `/Users//.venv/bin/scrapling`, so the } } ``` +#### Docker +If you are using the Docker image, then it would be something like +```json +{ + "mcpServers": { + "ScraplingServer": { + "command": "docker", + "args": [ + "run", "-i", "--rm", "scrapling", "mcp" + ] + } + } +} +``` The same logic applies to [Cursor](https://docs.cursor.com/en/context/mcp), [WindSurf](https://windsurf.com/university/tutorials/configuring-first-mcp-server), and others. @@ -120,6 +139,22 @@ Here's the main article from Anthropic on [how to add MCP servers to Claude code Then, after you've added the server, you need to completely quit and restart the app you used above. In Claude Desktop, you should see an MCP server indicator (πŸ”§) in the bottom-right corner of the chat input or see `ScraplingServer` in the `Search and tools` dropdown in the chat input box. +### Streamable HTTP +As per version 0.3.6, we have added the ability to make the MCP server use the 'Streamable HTTP' transport mode instead of the traditional 'stdio' transport. + +So instead of using the following command (the 'stdio' one): +```bash +scrapling mcp +``` +Use the following to enable 'Streamable HTTP' transport mode: +```bash +scrapling mcp --http +``` +Hence, the default value for the host the server is listening on is '0.0.0.0' and the port is 8000, which both can be configured as below: +```bash +scrapling mcp --http --host '127.0.0.1' --port 8000 +``` + ## Examples Now we will show you some examples of prompts we used while testing the MCP server, but you are probably more creative than we are and better at prompt engineering than we are :) diff --git a/docs/cli/extract-commands.md b/docs/cli/extract-commands.md index e89b480..5c86373 100644 --- a/docs/cli/extract-commands.md +++ b/docs/cli/extract-commands.md @@ -36,6 +36,9 @@ The extract command is a set of simple terminal tools that: # Save a clean version of the text content of the webpage to the file scrapling extract get "https://example.com" content.txt + + # Or use the Docker image with something like this: + docker run -v $(pwd)/output:/output scrapling extract get "https://blog.example.com" /output/article.md ``` - **Extract Specific Content** @@ -345,4 +348,4 @@ If you are not a Web Scraping expert and can't decide what to choose, you can us --- -*Happy scraping! Remember to always respect website policies and comply with all applicable legal requirements.* \ No newline at end of file +*Happy scraping! Remember to always respect website policies and comply with all applicable laws and regulations.* \ No newline at end of file diff --git a/docs/fetching/dynamic.md b/docs/fetching/dynamic.md index 95f6a87..bc57dae 100644 --- a/docs/fetching/dynamic.md +++ b/docs/fetching/dynamic.md @@ -35,7 +35,7 @@ It's the same as the vanilla Playwright option, but it provides a simple stealth Some of the things this fetcher's stealth mode does include: - * Patching the CDP runtime fingerprint through using PatchRight. + * Patching the CDP runtime fingerprint by using PatchRight. * Mimics some of the real browsers' properties by injecting several JS files and using custom options. * Custom flags are used on launch to hide Playwright even more and make it faster. * Generates real browser headers of the same type and user OS, then appends them to the request's headers. @@ -44,7 +44,7 @@ Some of the things this fetcher's stealth mode does include: ```python DynamicFetcher.fetch('https://example.com', real_chrome=True) ``` -If you have a Google Chrome browser installed, use this option. It's the same as the first option, but will use the Google Chrome browser you installed on your device instead of Chromium. +If you have a Google Chrome browser installed, use this option. It's the same as the first option, but it will use the Google Chrome browser you installed on your device instead of Chromium. This will make your requests look more authentic, so it's less detectable, and you can even use the `stealth=True` mode with it for better results, like below: ```python @@ -64,33 +64,33 @@ Instead of launching a browser locally (Chromium/Google Chrome), you can connect ## Full list of arguments Scrapling provides many options with this fetcher and its session classes. To make it as simple as possible, we will list the options here and give examples of using most of them. -| Argument | Description | Optional | -|:-------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| -| url | Target url | ❌ | -| headless | Pass `True` to run the browser in headless/hidden (**default**) or `False` for headful/visible mode. | βœ”οΈ | -| disable_resources | Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | -| cookies | Set cookies for the next request. | βœ”οΈ | -| useragent | Pass a useragent string to be used. **Otherwise, the fetcher will generate and use a real Useragent of the same browser.** | βœ”οΈ | -| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | -| load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | βœ”οΈ | -| timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | βœ”οΈ | -| wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | βœ”οΈ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | βœ”οΈ | -| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | -| init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | βœ”οΈ | -| wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | -| google_search | Enabled by default, Scrapling will set the referer header as if this request came from a Google search of this website's domain name. | βœ”οΈ | -| extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | βœ”οΈ | -| proxy | The proxy to be used with requests. It can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | -| hide_canvas | Add random noise to canvas operations to prevent fingerprinting. | βœ”οΈ | -| disable_webgl | Disables WebGL and WebGL 2.0 support entirely. | βœ”οΈ | -| stealth | Enables stealth mode; you should always check the documentation to see what the stealth mode does currently. | βœ”οΈ | -| real_chrome | If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch and use an instance of your browser. | βœ”οΈ | -| locale | Set the locale for the browser if wanted. The default value is `en-US`. | βœ”οΈ | -| cdp_url | Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. | βœ”οΈ | -| selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | βœ”οΈ | +| Argument | Description | Optional | +|:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| +| url | Target url | ❌ | +| headless | Pass `True` to run the browser in headless/hidden (**default**) or `False` for headful/visible mode. | βœ”οΈ | +| disable_resources | Drop requests for unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | +| cookies | Set cookies for the next request. | βœ”οΈ | +| useragent | Pass a useragent string to be used. **Otherwise, the fetcher will generate and use a real Useragent of the same browser.** | βœ”οΈ | +| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | +| load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | βœ”οΈ | +| timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | βœ”οΈ | +| wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | βœ”οΈ | +| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | βœ”οΈ | +| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | +| init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | βœ”οΈ | +| wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | +| google_search | Enabled by default, Scrapling will set the referer header as if this request came from a Google search of this website's domain name. | βœ”οΈ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | βœ”οΈ | +| proxy | The proxy to be used with requests. It can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | +| hide_canvas | Add random noise to canvas operations to prevent fingerprinting. | βœ”οΈ | +| disable_webgl | Disables WebGL and WebGL 2.0 support entirely. | βœ”οΈ | +| stealth | Enables stealth mode; you should always check the documentation to see what the stealth mode does currently. | βœ”οΈ | +| real_chrome | If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch and use an instance of your browser. | βœ”οΈ | +| locale | Set the locale for the browser if wanted. The default value is `en-US`. | βœ”οΈ | +| cdp_url | Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. | βœ”οΈ | +| selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | βœ”οΈ | -In the session classes, all these arguments can be set for the session globally. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, and `selector_config`. ## Examples @@ -168,7 +168,7 @@ page = DynamicFetcher.fetch( ``` This is the last wait the fetcher will do before returning the response (if enabled). You pass a CSS selector to the `wait_selector` argument, and the fetcher will wait for the state you passed in the `wait_selector_state` argument to be fulfilled. If you didn't pass a state, the default would be `attached`, which means it will wait for the element to be present in the DOM. -After that, if `load_dom` is enabled (the default), the fetcher will check again to see if all JS files are loaded and executed (the `domcontentloaded` state) or continue waiting. If you have enabled `network_idle`, the fetcher will wait for `network_idle` to be fulfilled again, as explained above. +After that, if `load_dom` is enabled (the default), the fetcher will check again to see if all JavaScript files are loaded and executed (in the `domcontentloaded` state) or continue waiting. If you have enabled `network_idle`, the fetcher will wait for `network_idle` to be fulfilled again, as explained above. The states the fetcher can wait for can be any of the following ([source](https://playwright.dev/python/docs/api/class-page#page-wait-for-selector)): @@ -279,7 +279,7 @@ You may have noticed the `max_pages` argument. This is a new argument that enabl This logic allows for multiple websites to be fetched at the same time in the same browser, which saves a lot of resources, but most importantly, is so fast :) -In versions 0.3 and 0.3.1, the pool was reusing finished tabs to save more resources/time. That logic proved to have flaws since it's nearly impossible to protect pages/tabs from contamination of the previous configuration you used with the request before this one. +In versions 0.3 and 0.3.1, the pool was reusing finished tabs to save more resources/time. That logic proved to have flaws, as it's nearly impossible to protect pages/tabs from contamination by the previous configuration used with the request before this one. ### Session Benefits diff --git a/docs/fetching/stealthy.md b/docs/fetching/stealthy.md index d998246..998fd27 100644 --- a/docs/fetching/stealthy.md +++ b/docs/fetching/stealthy.md @@ -1,6 +1,6 @@ # Introduction -Here, we will discuss the `StealthyFetcher` class. This class is similar to [DynamicFetcher](dynamic.md#introduction) in many ways, such as browser automation and utilizing [Playwright's API](https://playwright.dev/python/docs/intro). The main difference is that this class provides advanced anti-bot protection bypass capabilities and a custom version of a modified Firefox browser called [Camoufox](https://github.com/daijro/camoufox), from which most stealth comes. +Here, we will discuss the `StealthyFetcher` class. This class is similar to [DynamicFetcher](dynamic.md#introduction) in many ways, such as browser automation and the utilization of [Playwright's API](https://playwright.dev/python/docs/intro). The main difference is that this class provides advanced anti-bot protection bypass capabilities and a custom version of a modified Firefox browser called [Camoufox](https://github.com/daijro/camoufox), from which most stealth comes. As with [DynamicFetcher](dynamic.md#introduction), you will need some knowledge about [Playwright's Page API](https://playwright.dev/python/docs/api/class-page) to automate the page, as we will explain later. @@ -18,36 +18,36 @@ Check out how to configure the parsing options [here](choosing.md#parser-configu Scrapling provides many options with this fetcher and its session classes. Before jumping to the [examples](#examples), here's the full list of arguments -| Argument | Description | Optional | -|:-------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| -| url | Target url | ❌ | -| headless | Pass `True` to run the browser in headless/hidden (**default**) or `False` for headful/visible mode. | βœ”οΈ | -| block_images | Prevent the loading of images through Firefox preferences. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | -| disable_resources | Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | -| cookies | Set cookies for the next request. | βœ”οΈ | -| google_search | Enabled by default, Scrapling will set the referer header as if this request came from a Google search of this website's domain name. | βœ”οΈ | -| extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | βœ”οΈ | -| block_webrtc | Blocks WebRTC entirely. | βœ”οΈ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | βœ”οΈ | -| addons | List of Firefox addons to use. **Must be paths to extracted addons.** | βœ”οΈ | -| humanize | Humanize the cursor movement. The cursor movement takes either True or the maximum duration in seconds. The cursor typically takes up to 1.5 seconds to move across the window. | βœ”οΈ | -| allow_webgl | Enabled by default. Disabling WebGL is not recommended, as many WAFs now check if WebGL is enabled. | βœ”οΈ | -| geoip | Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address. It will also calculate and spoof the browser's language based on the distribution of language speakers in the target region. | βœ”οΈ | -| os_randomize | If enabled, Scrapling will randomize the OS fingerprints used. The default is matching the fingerprints with the current OS. | βœ”οΈ | -| disable_ads | Disabled by default; this installs the `uBlock Origin` addon on the browser if enabled. | βœ”οΈ | -| solve_cloudflare | When enabled, fetcher solves all three types of Cloudflare's Turnstile wait/captcha page before returning the response to you. | βœ”οΈ | -| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | -| load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | βœ”οΈ | -| timeout | The timeout used in all operations and waits through the page. It's in milliseconds, and the default is 30000. | βœ”οΈ | -| wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | βœ”οΈ | -| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | -| init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | βœ”οΈ | -| wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | -| proxy | The proxy to be used with requests. It can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | -| additional_args | Additional arguments to be passed to Camoufox as additional settings, and they take higher priority than Scrapling's settings. | βœ”οΈ | -| selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | βœ”οΈ | +| Argument | Description | Optional | +|:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| +| url | Target url | ❌ | +| headless | Pass `True` to run the browser in headless/hidden (**default**) or `False` for headful/visible mode. | βœ”οΈ | +| block_images | Prevent the loading of images through Firefox preferences. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | +| disable_resources | Drop requests for unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage, but be cautious with this option, as it may cause some websites to never finish loading._ | βœ”οΈ | +| cookies | Set cookies for the next request. | βœ”οΈ | +| google_search | Enabled by default, Scrapling will set the referer header as if this request came from a Google search of this website's domain name. | βœ”οΈ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | βœ”οΈ | +| block_webrtc | Blocks WebRTC entirely. | βœ”οΈ | +| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | βœ”οΈ | +| addons | List of Firefox addons to use. **Must be paths to extracted addons.** | βœ”οΈ | +| humanize | Humanize the cursor movement. The cursor movement takes either True or the maximum duration in seconds. The cursor typically takes up to 1.5 seconds to move across the window. | βœ”οΈ | +| allow_webgl | Enabled by default. Disabling WebGL is not recommended, as many WAFs now check if WebGL is enabled. | βœ”οΈ | +| geoip | Recommended to use with proxies; Automatically use IPs' longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address. It will also calculate and spoof the browser's language based on the distribution of language speakers in the target region. | βœ”οΈ | +| os_randomize | If enabled, Scrapling will randomize the OS fingerprints used. The default is matching the fingerprints with the current OS. | βœ”οΈ | +| disable_ads | Disabled by default; this installs the `uBlock Origin` addon on the browser if enabled. | βœ”οΈ | +| solve_cloudflare | When enabled, fetcher solves all types of Cloudflare's Turnstile/Interstitial challenges before returning the response to you. | βœ”οΈ | +| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | +| load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | βœ”οΈ | +| timeout | The timeout used in all operations and waits through the page. It's in milliseconds, and the default is 30000. | βœ”οΈ | +| wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | βœ”οΈ | +| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | +| init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | βœ”οΈ | +| wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | +| proxy | The proxy to be used with requests. It can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | +| additional_args | Additional arguments to be passed to Camoufox as additional settings, and they take higher priority than Scrapling's settings. | βœ”οΈ | +| selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | βœ”οΈ | -In the session classes, all these arguments can be set for the session globally. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, and `selector_config`. ## Examples It's easier to understand with examples, so we will now review most of the arguments individually with examples. @@ -91,7 +91,7 @@ page = StealthyFetcher.fetch( ) ``` -The `solve_cloudflare` parameter enables automatic detection and solving all three types of Cloudflare's Turnstile challenges: +The `solve_cloudflare` parameter enables automatic detection and solving all types of Cloudflare's Turnstile/Interstitial challenges: - JavaScript challenges (managed) - Interactive challenges (clicking verification boxes) @@ -100,7 +100,7 @@ The `solve_cloudflare` parameter enables automatic detection and solving all thr **Important notes:** - When `solve_cloudflare=True` is enabled, `humanize=True` is automatically activated for more realistic behavior -- The timeout should be at least 60 seconds when using Cloudflare solver for sufficient challenge-solving time +- The timeout should be at least 60 seconds when using the Cloudflare solver for sufficient challenge-solving time - This feature works seamlessly with proxies and other stealth options ### Additional stealth options @@ -187,7 +187,7 @@ page = StealthyFetcher.fetch( ``` This is the last wait the fetcher will do before returning the response (if enabled). You pass a CSS selector to the `wait_selector` argument, and the fetcher will wait for the state you passed in the `wait_selector_state` argument to be fulfilled. If you didn't pass a state, the default would be `attached`, which means it will wait for the element to be present in the DOM. -After that, if `load_dom` is enabled (the default), the fetcher will check again to see if all JS files are loaded and executed (the `domcontentloaded` state) or continue waiting. If you have enabled `network_idle`, the fetcher will wait for `network_idle` to be fulfilled again, as explained above. +After that, if `load_dom` is enabled (the default), the fetcher will check again to see if all JavaScript files are loaded and executed (in the `domcontentloaded` state) or continue waiting. If you have enabled `network_idle`, the fetcher will wait for `network_idle` to be fulfilled again, as explained above. The states the fetcher can wait for can be any of the following ([source](https://playwright.dev/python/docs/api/class-page#page-wait-for-selector)): @@ -282,7 +282,7 @@ You may have noticed the `max_pages` argument. This is a new argument that enabl This logic allows for multiple websites to be fetched at the same time in the same browser, which saves a lot of resources, but most importantly, is so fast :) -In versions 0.3 and 0.3.1, the pool was reusing finished tabs to save more resources/time. That logic proved to have flaws since it's nearly impossible to protect pages/tabs from contamination of the previous configuration you used with the request before this one. +In versions 0.3 and 0.3.1, the pool was reusing finished tabs to save more resources/time. That logic proved to have flaws, as it's nearly impossible to protect pages/tabs from contamination by the previous configuration used with the request before this one. ### Session Benefits diff --git a/docs/index.md b/docs/index.md index d3c0ad2..7769cf4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ Scrapling isn't just another Web Scraping library. It's the first **adaptive** scraping library that learns from website changes and evolves with them. While other libraries break when websites update their structure, Scrapling automatically relocates your elements and keeps your scrapers running. -Built for the modern Web, Scrapling has its own rapid parsing engine and its fetchers to handle all Web Scraping challenges you are facing or will face. Built by Web Scrapers for Web Scrapers and regular users, there's something for everyone. +Built for the modern Web, Scrapling features its own rapid parsing engine and fetchers to handle all Web Scraping challenges you face or will face. Built by Web Scrapers for Web Scrapers and regular users, there's something for everyone. ```python >> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, DynamicFetcher @@ -50,7 +50,7 @@ Built for the modern Web, Scrapling has its own rapid parsing engine and its fet ### Advanced Websites Fetching with Session Support - **HTTP Requests**: Fast and stealthy HTTP requests with the `Fetcher` class. Can impersonate browsers' TLS fingerprint, headers, and use HTTP/3. - **Dynamic Loading**: Fetch dynamic websites with full browser automation through the `DynamicFetcher` class supporting Playwright's Chromium, real Chrome, and custom stealth mode. -- **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` using a modified version of Firefox and fingerprint spoofing. Can bypass all levels of Cloudflare's Turnstile with automation easily. +- **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` using a modified version of Firefox and fingerprint spoofing. Can bypass all types of Cloudflare's Turnstile/Interstitial with automation easily. - **Session Management**: Persistent session support with `FetcherSession`, `StealthySession`, and `DynamicSession` classes for cookie and state management across requests. - **Async Support**: Complete async support across all fetchers and dedicated async session classes. diff --git a/scrapling/cli.py b/scrapling/cli.py index 5824599..25fdce3 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -783,7 +783,7 @@ def stealthy_fetch( :param disable_resources: Drop requests of unnecessary resources for a speed boost. :param block_webrtc: Blocks WebRTC entirely. :param humanize: Humanize the cursor movement. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges. :param allow_webgl: Allow WebGL (recommended to keep enabled). :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param disable_ads: Install the uBlock Origin addon on the browser. diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index b9503a3..242eded 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -436,7 +436,7 @@ class ScraplingMCPServer: :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param disable_ads: Disabled by default, this installs the `uBlock Origin` addon on the browser if enabled. @@ -535,7 +535,7 @@ class ScraplingMCPServer: :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param disable_ads: Disabled by default, this installs the `uBlock Origin` addon on the browser if enabled. diff --git a/scrapling/engines/_browsers/_camoufox.py b/scrapling/engines/_browsers/_camoufox.py index 207be90..519ae83 100644 --- a/scrapling/engines/_browsers/_camoufox.py +++ b/scrapling/engines/_browsers/_camoufox.py @@ -116,7 +116,7 @@ class StealthySession(StealthySessionMixin, SyncSession): :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. @@ -300,7 +300,7 @@ class StealthySession(StealthySessionMixin, SyncSession): :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param selector_config: The arguments that will be passed in the end while creating the final Selector's class. :return: A `Response` object. """ @@ -442,7 +442,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession): :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. @@ -626,7 +626,7 @@ class AsyncStealthySession(StealthySessionMixin, AsyncSession): :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param selector_config: The arguments that will be passed in the end while creating the final Selector's class. :return: A `Response` object. """ diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index fbb2b28..81389e6 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -92,7 +92,7 @@ class StealthyFetcher(BaseFetcher): :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. @@ -191,7 +191,7 @@ class StealthyFetcher(BaseFetcher): :param cookies: Set cookies for the next request. :param addons: List of Firefox addons to use. Must be paths to extracted addons. :param humanize: Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. - :param solve_cloudflare: Solves all 3 types of the Cloudflare's Turnstile wait page before returning the response to you. + :param solve_cloudflare: Solves all types of the Cloudflare's Turnstile/Interstitial challenges before returning the response to you. :param allow_webgl: Enabled by default. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. From f6c122b87f8221b8d71aa2b1fe6554b1318bde43 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 29 Sep 2025 05:09:23 +0300 Subject: [PATCH 14/21] style: Removing dead code/docstrings --- scrapling/core/ai.py | 4 ++-- scrapling/engines/_browsers/_controllers.py | 4 ++-- scrapling/engines/constants.py | 15 --------------- scrapling/fetchers.py | 5 ++--- tests/fetchers/async/test_dynamic.py | 2 +- tests/fetchers/sync/test_dynamic.py | 2 +- 6 files changed, 8 insertions(+), 24 deletions(-) diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index 242eded..283f183 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -258,7 +258,7 @@ class ScraplingMCPServer: :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. @@ -346,7 +346,7 @@ class ScraplingMCPServer: :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. diff --git a/scrapling/engines/_browsers/_controllers.py b/scrapling/engines/_browsers/_controllers.py index b4b6276..b895f9a 100644 --- a/scrapling/engines/_browsers/_controllers.py +++ b/scrapling/engines/_browsers/_controllers.py @@ -117,7 +117,7 @@ class DynamicSession(DynamicSessionMixin, SyncSession): :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. @@ -360,7 +360,7 @@ class AsyncDynamicSession(DynamicSessionMixin, AsyncSession): :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. diff --git a/scrapling/engines/constants.py b/scrapling/engines/constants.py index 03a678e..df12ee3 100644 --- a/scrapling/engines/constants.py +++ b/scrapling/engines/constants.py @@ -101,18 +101,3 @@ DEFAULT_STEALTH_FLAGS = ( "--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4", "--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,TranslateUI,BlinkGenPropertyTrees", ) - -# Defaulting to the docker mode, token doesn't matter in it as it's passed for the container -NSTBROWSER_DEFAULT_QUERY = { - "once": True, - "headless": True, - "autoClose": True, - "fingerprint": { - "flags": {"timezone": "BasedOnIp", "screen": "Custom"}, - "platform": "linux", # support: windows, mac, linux - "kernel": "chromium", # only support: chromium - "kernelMilestone": "128", - "hardwareConcurrency": 8, - "deviceMemory": 8, - }, -} diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 81389e6..ea99ed5 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -260,7 +260,6 @@ class DynamicFetcher(BaseFetcher): 3) Using custom flags on launch to hide Playwright even more and make it faster. 4) Generates real browser's headers of the same type and same user OS, then append it to the request. - Real browsers by passing the `real_chrome` argument or the CDP URL of your browser to be controlled by the Fetcher, and most of the options can be enabled on it. - - NSTBrowser's docker browserless option by passing the CDP URL and enabling `nstbrowser_mode` option. > Note that these are the main options with PlayWright, but it can be mixed. """ @@ -314,7 +313,7 @@ class DynamicFetcher(BaseFetcher): :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. @@ -401,7 +400,7 @@ class DynamicFetcher(BaseFetcher): :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. diff --git a/tests/fetchers/async/test_dynamic.py b/tests/fetchers/async/test_dynamic.py index 5174c9f..3f5f7b5 100644 --- a/tests/fetchers/async/test_dynamic.py +++ b/tests/fetchers/async/test_dynamic.py @@ -86,7 +86,7 @@ class TestDynamicFetcherAsync: with pytest.raises(TypeError): await fetcher.async_fetch( - urls["html_url"], cdp_url="blahblah", nstbrowser_mode=True + urls["html_url"], cdp_url="blahblah" ) with pytest.raises(Exception): diff --git a/tests/fetchers/sync/test_dynamic.py b/tests/fetchers/sync/test_dynamic.py index a140076..a60d9d8 100644 --- a/tests/fetchers/sync/test_dynamic.py +++ b/tests/fetchers/sync/test_dynamic.py @@ -81,7 +81,7 @@ class TestDynamicFetcher: fetcher.fetch(self.html_url, cdp_url="blahblah") with pytest.raises(TypeError): - fetcher.fetch(self.html_url, cdp_url="blahblah", nstbrowser_mode=True) + fetcher.fetch(self.html_url, cdp_url="blahblah") with pytest.raises(Exception): fetcher.fetch(self.html_url, cdp_url="ws://blahblah") From ec3809ce408b02048cb47c7bf0a418bbcecc6ebf Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 29 Sep 2025 17:50:45 +0300 Subject: [PATCH 15/21] docs: fix installation section on the website --- docs/index.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7769cf4..384d772 100644 --- a/docs/index.md +++ b/docs/index.md @@ -130,19 +130,21 @@ Starting with v0.3.2, this installation only includes the parser engine and its This downloads all browsers with their system dependencies and fingerprint manipulation dependencies. 2. Extra features: - - Install the MCP server feature: + + + - Install the MCP server feature: ```bash pip install "scrapling[ai]" ``` - - Install shell features (Web Scraping shell and the `extract` command): - ```bash - pip install "scrapling[shell]" - ``` - - Install everything: - ```bash - pip install "scrapling[all]" - ``` - Don't forget that you need to install the browser dependencies with `scrapling install` after any of these extras (if you didn't already) + - Install shell features (Web Scraping shell and the `extract` command): + ```bash + pip install "scrapling[shell]" + ``` + - Install everything: + ```bash + pip install "scrapling[all]" + ``` + Don't forget that you need to install the browser dependencies with `scrapling install` after any of these extras (if you didn't already) ## How the documentation is organized Scrapling has a lot of documentation, so we try to follow a guideline called the [DiΓ‘taxis documentation framework](https://diataxis.fr/). From 4135dd86b8f9738984bdf806f565fac0ff822a5b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 03:48:43 +0300 Subject: [PATCH 16/21] refactor: Restructure the fetchers code to not use more memory than needed Also fixes: #92 --- scrapling/fetchers/__init__.py | 36 +++ scrapling/fetchers/chrome.py | 205 ++++++++++++++++ .../{fetchers.py => fetchers/firefox.py} | 229 +----------------- scrapling/fetchers/requests.py | 36 +++ 4 files changed, 278 insertions(+), 228 deletions(-) create mode 100644 scrapling/fetchers/__init__.py create mode 100644 scrapling/fetchers/chrome.py rename scrapling/{fetchers.py => fetchers/firefox.py} (50%) create mode 100644 scrapling/fetchers/requests.py diff --git a/scrapling/fetchers/__init__.py b/scrapling/fetchers/__init__.py new file mode 100644 index 0000000..9c64659 --- /dev/null +++ b/scrapling/fetchers/__init__.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from scrapling.fetchers.requests import Fetcher, AsyncFetcher, FetcherSession + from scrapling.fetchers.chrome import DynamicFetcher, DynamicSession, AsyncDynamicSession + from scrapling.fetchers.firefox import StealthyFetcher, StealthySession, AsyncStealthySession + + +# Lazy import mapping +_LAZY_IMPORTS = { + "Fetcher": ("scrapling.fetchers.requests", "Fetcher"), + "AsyncFetcher": ("scrapling.fetchers.requests", "AsyncFetcher"), + "FetcherSession": ("scrapling.fetchers.requests", "FetcherSession"), + "DynamicFetcher": ("scrapling.fetchers.chrome", "DynamicFetcher"), + "DynamicSession": ("scrapling.fetchers.chrome", "DynamicSession"), + "AsyncDynamicSession": ("scrapling.fetchers.chrome", "AsyncDynamicSession"), + "StealthyFetcher": ("scrapling.fetchers.firefox", "StealthyFetcher"), + "StealthySession": ("scrapling.fetchers.firefox", "StealthySession"), + "AsyncStealthySession": ("scrapling.fetchers.firefox", "AsyncStealthySession"), +} + +__all__ = ["Fetcher", "AsyncFetcher", "StealthyFetcher", "DynamicFetcher"] + + +def __getattr__(name: str) -> Any: + if name in _LAZY_IMPORTS: + module_path, class_name = _LAZY_IMPORTS[name] + module = __import__(module_path, fromlist=[class_name]) + return getattr(module, class_name) + else: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + """Support for dir() and autocomplete.""" + return sorted(list(_LAZY_IMPORTS.keys())) diff --git a/scrapling/fetchers/chrome.py b/scrapling/fetchers/chrome.py new file mode 100644 index 0000000..49fe009 --- /dev/null +++ b/scrapling/fetchers/chrome.py @@ -0,0 +1,205 @@ +from scrapling.core._types import ( + Callable, + Dict, + List, + Optional, + SelectorWaitStates, + Iterable, +) +from scrapling.engines.toolbelt.custom import BaseFetcher, Response +from scrapling.engines._browsers._controllers import DynamicSession, AsyncDynamicSession + + +class DynamicFetcher(BaseFetcher): + """A `Fetcher` class type that provide many options, all of them are based on PlayWright. + + Using this Fetcher class, you can do requests with: + - Vanilla Playwright without any modifications other than the ones you chose. + - Stealthy Playwright with the stealth mode I wrote for it. It's still a work in progress, but it bypasses many online tests like bot.sannysoft.com + Some of the things stealth mode does include: + 1) Patches the CDP runtime fingerprint. + 2) Mimics some of the real browsers' properties by injecting several JS files and using custom options. + 3) Using custom flags on launch to hide Playwright even more and make it faster. + 4) Generates real browser's headers of the same type and same user OS, then append it to the request. + - Real browsers by passing the `real_chrome` argument or the CDP URL of your browser to be controlled by the Fetcher, and most of the options can be enabled on it. + + > Note that these are the main options with PlayWright, but it can be mixed. + """ + + @classmethod + def fetch( + cls, + url: str, + headless: bool = True, + google_search: bool = True, + hide_canvas: bool = False, + disable_webgl: bool = False, + real_chrome: bool = False, + stealth: bool = False, + wait: int | float = 0, + page_action: Optional[Callable] = None, + proxy: Optional[str | Dict[str, str]] = None, + locale: str = "en-US", + extra_headers: Optional[Dict[str, str]] = None, + useragent: Optional[str] = None, + cdp_url: Optional[str] = None, + timeout: int | float = 30000, + disable_resources: bool = False, + wait_selector: Optional[str] = None, + init_script: Optional[str] = None, + cookies: Optional[Iterable[Dict]] = None, + network_idle: bool = False, + load_dom: bool = True, + wait_selector_state: SelectorWaitStates = "attached", + custom_config: Optional[Dict] = None, + ) -> Response: + """Opens up a browser and do your request based on your chosen options below. + + :param url: Target url. + :param headless: Run the browser in headless/hidden (default), or headful/visible mode. + :param disable_resources: Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites. + Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. + This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. + :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. + :param cookies: Set cookies for the next request. + :param network_idle: Wait for the page until there are no network connections for at least 500 ms. + :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. + :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. + :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param wait_selector: Wait for a specific CSS selector to be in a specific state. + :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. + :param locale: Set the locale for the browser if wanted. The default value is `en-US`. + :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. + :param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently. + :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. + :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. + :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. + :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ + :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. + :return: A `Response` object. + """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + raise ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + with DynamicSession( + wait=wait, + proxy=proxy, + locale=locale, + timeout=timeout, + stealth=stealth, + cdp_url=cdp_url, + cookies=cookies, + headless=headless, + load_dom=load_dom, + useragent=useragent, + real_chrome=real_chrome, + page_action=page_action, + hide_canvas=hide_canvas, + init_script=init_script, + network_idle=network_idle, + google_search=google_search, + extra_headers=extra_headers, + wait_selector=wait_selector, + disable_webgl=disable_webgl, + disable_resources=disable_resources, + wait_selector_state=wait_selector_state, + selector_config={**cls._generate_parser_arguments(), **custom_config}, + ) as session: + return session.fetch(url) + + @classmethod + async def async_fetch( + cls, + url: str, + headless: bool = True, + google_search: bool = True, + hide_canvas: bool = False, + disable_webgl: bool = False, + real_chrome: bool = False, + stealth: bool = False, + wait: int | float = 0, + page_action: Optional[Callable] = None, + proxy: Optional[str | Dict[str, str]] = None, + locale: str = "en-US", + extra_headers: Optional[Dict[str, str]] = None, + useragent: Optional[str] = None, + cdp_url: Optional[str] = None, + timeout: int | float = 30000, + disable_resources: bool = False, + wait_selector: Optional[str] = None, + init_script: Optional[str] = None, + cookies: Optional[Iterable[Dict]] = None, + network_idle: bool = False, + load_dom: bool = True, + wait_selector_state: SelectorWaitStates = "attached", + custom_config: Optional[Dict] = None, + ) -> Response: + """Opens up a browser and do your request based on your chosen options below. + + :param url: Target url. + :param headless: Run the browser in headless/hidden (default), or headful/visible mode. + :param disable_resources: Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites. + Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. + This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. + :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. + :param cookies: Set cookies for the next request. + :param network_idle: Wait for the page until there are no network connections for at least 500 ms. + :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. + :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. + :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param wait_selector: Wait for a specific CSS selector to be in a specific state. + :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. + :param locale: Set the locale for the browser if wanted. The default value is `en-US`. + :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. + :param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently. + :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. + :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. + :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. + :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. + :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ + :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. + :return: A `Response` object. + """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + raise ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + async with AsyncDynamicSession( + wait=wait, + max_pages=1, + proxy=proxy, + locale=locale, + timeout=timeout, + stealth=stealth, + cdp_url=cdp_url, + cookies=cookies, + headless=headless, + load_dom=load_dom, + useragent=useragent, + real_chrome=real_chrome, + page_action=page_action, + hide_canvas=hide_canvas, + init_script=init_script, + network_idle=network_idle, + google_search=google_search, + extra_headers=extra_headers, + wait_selector=wait_selector, + disable_webgl=disable_webgl, + disable_resources=disable_resources, + wait_selector_state=wait_selector_state, + selector_config={**cls._generate_parser_arguments(), **custom_config}, + ) as session: + return await session.fetch(url) + + +PlayWrightFetcher = DynamicFetcher # For backward-compatibility diff --git a/scrapling/fetchers.py b/scrapling/fetchers/firefox.py similarity index 50% rename from scrapling/fetchers.py rename to scrapling/fetchers/firefox.py index ea99ed5..ca6db40 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers/firefox.py @@ -4,41 +4,9 @@ from scrapling.core._types import ( List, Optional, SelectorWaitStates, - Iterable, -) -from scrapling.engines.static import ( - FetcherSession, - FetcherClient as _FetcherClient, - AsyncFetcherClient as _AsyncFetcherClient, -) -from scrapling.engines._browsers import ( - DynamicSession, - StealthySession, - AsyncDynamicSession, - AsyncStealthySession, ) from scrapling.engines.toolbelt.custom import BaseFetcher, Response - -__FetcherClientInstance__ = _FetcherClient() -__AsyncFetcherClientInstance__ = _AsyncFetcherClient() - - -class Fetcher(BaseFetcher): - """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`.""" - - get = __FetcherClientInstance__.get - post = __FetcherClientInstance__.post - put = __FetcherClientInstance__.put - delete = __FetcherClientInstance__.delete - - -class AsyncFetcher(BaseFetcher): - """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`.""" - - get = __AsyncFetcherClientInstance__.get - post = __AsyncFetcherClientInstance__.post - put = __AsyncFetcherClientInstance__.put - delete = __AsyncFetcherClientInstance__.delete +from scrapling.engines._browsers._camoufox import StealthySession, AsyncStealthySession class StealthyFetcher(BaseFetcher): @@ -246,198 +214,3 @@ class StealthyFetcher(BaseFetcher): additional_args=additional_args or {}, ) as engine: return await engine.fetch(url) - - -class DynamicFetcher(BaseFetcher): - """A `Fetcher` class type that provide many options, all of them are based on PlayWright. - - Using this Fetcher class, you can do requests with: - - Vanilla Playwright without any modifications other than the ones you chose. - - Stealthy Playwright with the stealth mode I wrote for it. It's still a work in progress, but it bypasses many online tests like bot.sannysoft.com - Some of the things stealth mode does include: - 1) Patches the CDP runtime fingerprint. - 2) Mimics some of the real browsers' properties by injecting several JS files and using custom options. - 3) Using custom flags on launch to hide Playwright even more and make it faster. - 4) Generates real browser's headers of the same type and same user OS, then append it to the request. - - Real browsers by passing the `real_chrome` argument or the CDP URL of your browser to be controlled by the Fetcher, and most of the options can be enabled on it. - - > Note that these are the main options with PlayWright, but it can be mixed. - """ - - @classmethod - def fetch( - cls, - url: str, - headless: bool = True, - google_search: bool = True, - hide_canvas: bool = False, - disable_webgl: bool = False, - real_chrome: bool = False, - stealth: bool = False, - wait: int | float = 0, - page_action: Optional[Callable] = None, - proxy: Optional[str | Dict[str, str]] = None, - locale: str = "en-US", - extra_headers: Optional[Dict[str, str]] = None, - useragent: Optional[str] = None, - cdp_url: Optional[str] = None, - timeout: int | float = 30000, - disable_resources: bool = False, - wait_selector: Optional[str] = None, - init_script: Optional[str] = None, - cookies: Optional[Iterable[Dict]] = None, - network_idle: bool = False, - load_dom: bool = True, - wait_selector_state: SelectorWaitStates = "attached", - custom_config: Optional[Dict] = None, - ) -> Response: - """Opens up a browser and do your request based on your chosen options below. - - :param url: Target url. - :param headless: Run the browser in headless/hidden (default), or headful/visible mode. - :param disable_resources: Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites. - Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. - This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. - :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. - :param cookies: Set cookies for the next request. - :param network_idle: Wait for the page until there are no network connections for at least 500 ms. - :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. - :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 - :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. - :param wait_selector: Wait for a specific CSS selector to be in a specific state. - :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. - :param locale: Set the locale for the browser if wanted. The default value is `en-US`. - :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. - :param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently. - :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. - :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. - :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. - :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. - :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ - :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. - :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. - :return: A `Response` object. - """ - if not custom_config: - custom_config = {} - elif not isinstance(custom_config, dict): - raise ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") - - with DynamicSession( - wait=wait, - proxy=proxy, - locale=locale, - timeout=timeout, - stealth=stealth, - cdp_url=cdp_url, - cookies=cookies, - headless=headless, - load_dom=load_dom, - useragent=useragent, - real_chrome=real_chrome, - page_action=page_action, - hide_canvas=hide_canvas, - init_script=init_script, - network_idle=network_idle, - google_search=google_search, - extra_headers=extra_headers, - wait_selector=wait_selector, - disable_webgl=disable_webgl, - disable_resources=disable_resources, - wait_selector_state=wait_selector_state, - selector_config={**cls._generate_parser_arguments(), **custom_config}, - ) as session: - return session.fetch(url) - - @classmethod - async def async_fetch( - cls, - url: str, - headless: bool = True, - google_search: bool = True, - hide_canvas: bool = False, - disable_webgl: bool = False, - real_chrome: bool = False, - stealth: bool = False, - wait: int | float = 0, - page_action: Optional[Callable] = None, - proxy: Optional[str | Dict[str, str]] = None, - locale: str = "en-US", - extra_headers: Optional[Dict[str, str]] = None, - useragent: Optional[str] = None, - cdp_url: Optional[str] = None, - timeout: int | float = 30000, - disable_resources: bool = False, - wait_selector: Optional[str] = None, - init_script: Optional[str] = None, - cookies: Optional[Iterable[Dict]] = None, - network_idle: bool = False, - load_dom: bool = True, - wait_selector_state: SelectorWaitStates = "attached", - custom_config: Optional[Dict] = None, - ) -> Response: - """Opens up a browser and do your request based on your chosen options below. - - :param url: Target url. - :param headless: Run the browser in headless/hidden (default), or headful/visible mode. - :param disable_resources: Drop requests of unnecessary resources for a speed boost. It depends, but it made requests ~25% faster in my tests for some websites. - Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. - This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. - :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. - :param cookies: Set cookies for the next request. - :param network_idle: Wait for the page until there are no network connections for at least 500 ms. - :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. - :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 - :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. - :param wait_selector: Wait for a specific CSS selector to be in a specific state. - :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. - :param locale: Set the locale for the browser if wanted. The default value is `en-US`. - :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. - :param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently. - :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. - :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. - :param disable_webgl: Disables WebGL and WebGL 2.0 support entirely. - :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. - :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search of this website's domain name. - :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ - :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. - :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. - :return: A `Response` object. - """ - if not custom_config: - custom_config = {} - elif not isinstance(custom_config, dict): - raise ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") - - async with AsyncDynamicSession( - wait=wait, - max_pages=1, - proxy=proxy, - locale=locale, - timeout=timeout, - stealth=stealth, - cdp_url=cdp_url, - cookies=cookies, - headless=headless, - load_dom=load_dom, - useragent=useragent, - real_chrome=real_chrome, - page_action=page_action, - hide_canvas=hide_canvas, - init_script=init_script, - network_idle=network_idle, - google_search=google_search, - extra_headers=extra_headers, - wait_selector=wait_selector, - disable_webgl=disable_webgl, - disable_resources=disable_resources, - wait_selector_state=wait_selector_state, - selector_config={**cls._generate_parser_arguments(), **custom_config}, - ) as session: - return await session.fetch(url) - - -PlayWrightFetcher = DynamicFetcher # For backward-compatibility diff --git a/scrapling/fetchers/requests.py b/scrapling/fetchers/requests.py new file mode 100644 index 0000000..3d198a5 --- /dev/null +++ b/scrapling/fetchers/requests.py @@ -0,0 +1,36 @@ +from scrapling.core._types import ( + Callable, + Dict, + List, + Optional, + SelectorWaitStates, + Iterable, +) +from scrapling.engines.static import ( + FetcherSession, + FetcherClient as _FetcherClient, + AsyncFetcherClient as _AsyncFetcherClient, +) +from scrapling.engines.toolbelt.custom import BaseFetcher, Response + + +__FetcherClientInstance__ = _FetcherClient() +__AsyncFetcherClientInstance__ = _AsyncFetcherClient() + + +class Fetcher(BaseFetcher): + """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`.""" + + get = __FetcherClientInstance__.get + post = __FetcherClientInstance__.post + put = __FetcherClientInstance__.put + delete = __FetcherClientInstance__.delete + + +class AsyncFetcher(BaseFetcher): + """A basic `Fetcher` class type that can only do basic GET, POST, PUT, and DELETE HTTP requests based on `curl_cffi`.""" + + get = __AsyncFetcherClientInstance__.get + post = __AsyncFetcherClientInstance__.post + put = __AsyncFetcherClientInstance__.put + delete = __AsyncFetcherClientInstance__.delete From eedfa855ab4704f4684862f98ad85cc96bfee242 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 03:50:39 +0300 Subject: [PATCH 17/21] fix: Fixes for the type checking in the main `init` file and a bit of cleaning --- scrapling/__init__.py | 44 +++++++++++++++---------- scrapling/core/_types.py | 2 -- scrapling/engines/_browsers/__init__.py | 2 -- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/scrapling/__init__.py b/scrapling/__init__.py index c4bd97d..d30a8db 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -2,27 +2,37 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" __version__ = "0.3.6" __copyright__ = "Copyright (c) 2024 Karim Shoair" +from typing import Any, TYPE_CHECKING -# A lightweight approach to create a lazy loader for each import for backward compatibility -# This will reduces initial memory footprint significantly (only loads what's used) -def __getattr__(name): - lazy_imports = { - "Fetcher": ("scrapling.fetchers", "Fetcher"), - "Selector": ("scrapling.parser", "Selector"), - "Selectors": ("scrapling.parser", "Selectors"), - "AttributesHandler": ("scrapling.core.custom_types", "AttributesHandler"), - "TextHandler": ("scrapling.core.custom_types", "TextHandler"), - "AsyncFetcher": ("scrapling.fetchers", "AsyncFetcher"), - "StealthyFetcher": ("scrapling.fetchers", "StealthyFetcher"), - "DynamicFetcher": ("scrapling.fetchers", "DynamicFetcher"), - } +if TYPE_CHECKING: + from scrapling.parser import Selector, Selectors + from scrapling.core.custom_types import AttributesHandler, TextHandler + from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, DynamicFetcher - if name in lazy_imports: - module_path, class_name = lazy_imports[name] + +# Lazy import mapping +_LAZY_IMPORTS = { + "Fetcher": ("scrapling.fetchers", "Fetcher"), + "Selector": ("scrapling.parser", "Selector"), + "Selectors": ("scrapling.parser", "Selectors"), + "AttributesHandler": ("scrapling.core.custom_types", "AttributesHandler"), + "TextHandler": ("scrapling.core.custom_types", "TextHandler"), + "AsyncFetcher": ("scrapling.fetchers", "AsyncFetcher"), + "StealthyFetcher": ("scrapling.fetchers", "StealthyFetcher"), + "DynamicFetcher": ("scrapling.fetchers", "DynamicFetcher"), +} +__all__ = ["Selector", "Fetcher", "AsyncFetcher", "StealthyFetcher", "DynamicFetcher"] + + +def __getattr__(name: str) -> Any: + if name in _LAZY_IMPORTS: + module_path, class_name = _LAZY_IMPORTS[name] module = __import__(module_path, fromlist=[class_name]) return getattr(module, class_name) else: - raise AttributeError(f"module 'scrapling' has no attribute '{name}'") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -__all__ = ["Selector", "Fetcher", "AsyncFetcher", "StealthyFetcher", "DynamicFetcher"] +def __dir__() -> list[str]: + """Support for dir() and autocomplete.""" + return sorted(__all__ + ["fetchers", "parser", "cli", "core", "__author__", "__version__", "__copyright__"]) diff --git a/scrapling/core/_types.py b/scrapling/core/_types.py index cd8c9c0..59422f2 100644 --- a/scrapling/core/_types.py +++ b/scrapling/core/_types.py @@ -39,6 +39,4 @@ except ImportError: # pragma: no cover try: from typing_extensions import Self # Backport except ImportError: - from typing import TypeVar - Self = object diff --git a/scrapling/engines/_browsers/__init__.py b/scrapling/engines/_browsers/__init__.py index 2cc5947..e69de29 100644 --- a/scrapling/engines/_browsers/__init__.py +++ b/scrapling/engines/_browsers/__init__.py @@ -1,2 +0,0 @@ -from ._controllers import DynamicSession, AsyncDynamicSession -from ._camoufox import StealthySession, AsyncStealthySession From 7aa083053b5909a0f2630db071d7f1f7774dac29 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 03:50:57 +0300 Subject: [PATCH 18/21] test: change tests accordingly --- tests/fetchers/async/test_camoufox_session.py | 2 +- tests/fetchers/async/test_dynamic_session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fetchers/async/test_camoufox_session.py b/tests/fetchers/async/test_camoufox_session.py index af9b32b..05f7953 100644 --- a/tests/fetchers/async/test_camoufox_session.py +++ b/tests/fetchers/async/test_camoufox_session.py @@ -4,7 +4,7 @@ import asyncio import pytest_httpbin -from scrapling.engines._browsers import AsyncStealthySession +from scrapling.fetchers import AsyncStealthySession @pytest_httpbin.use_class_based_httpbin diff --git a/tests/fetchers/async/test_dynamic_session.py b/tests/fetchers/async/test_dynamic_session.py index e6f0b5a..234854c 100644 --- a/tests/fetchers/async/test_dynamic_session.py +++ b/tests/fetchers/async/test_dynamic_session.py @@ -3,7 +3,7 @@ import asyncio import pytest_httpbin -from scrapling.engines._browsers import AsyncDynamicSession +from scrapling.fetchers import AsyncDynamicSession @pytest_httpbin.use_class_based_httpbin From 849913ad779e5a3b8f9d236d7a3834fb94ea8e30 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 04:17:59 +0300 Subject: [PATCH 19/21] style: Correcting return type annotation for Fetcher/AsyncFetcher (#93) --- scrapling/engines/static.py | 378 +++++++++++++++++++++++++++++++++ scrapling/fetchers/requests.py | 10 +- 2 files changed, 379 insertions(+), 9 deletions(-) diff --git a/scrapling/engines/static.py b/scrapling/engines/static.py index 6e5c0de..1622e9d 100644 --- a/scrapling/engines/static.py +++ b/scrapling/engines/static.py @@ -650,6 +650,195 @@ class FetcherClient(FetcherSession): self.__aexit__ = None self._curl_session = True + # Setting the correct return types for the type checking/autocompletion + def get( + self, + url: str, + params: Optional[Dict | List | Tuple] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Response: + return super().get( + url, + params, + headers, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def post( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Response: + return super().post( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def put( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Response: + return super().put( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def delete( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Response: + return super().delete( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + class AsyncFetcherClient(FetcherSession): def __init__(self, *args, **kwargs): @@ -659,3 +848,192 @@ class AsyncFetcherClient(FetcherSession): self.__aenter__ = None self.__aexit__ = None self._async_curl_session = True + + # Setting the correct return types for the type checking/autocompletion + def get( + self, + url: str, + params: Optional[Dict | List | Tuple] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Awaitable[Response]: + return super().get( + url, + params, + headers, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def post( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Awaitable[Response]: + return super().post( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def put( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Awaitable[Response]: + return super().put( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) + + def delete( + self, + url: str, + data: Optional[Dict | str] = None, + json: Optional[Dict | List] = None, + headers: Optional[Mapping[str, Optional[str]]] = _UNSET, + params: Optional[Dict | List | Tuple] = None, + cookies: Optional[CookieTypes] = None, + timeout: Optional[int | float] = _UNSET, + follow_redirects: Optional[bool] = _UNSET, + max_redirects: Optional[int] = _UNSET, + retries: Optional[int] = _UNSET, + retry_delay: Optional[int] = _UNSET, + proxies: Optional[ProxySpec] = _UNSET, + proxy: Optional[str] = _UNSET, + proxy_auth: Optional[Tuple[str, str]] = _UNSET, + auth: Optional[Tuple[str, str]] = None, + verify: Optional[bool] = _UNSET, + cert: Optional[str | Tuple[str, str]] = _UNSET, + impersonate: Optional[BrowserTypeLiteral] = _UNSET, + http3: Optional[bool] = _UNSET, + stealthy_headers: Optional[bool] = _UNSET, + **kwargs, + ) -> Awaitable[Response]: + return super().delete( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ) diff --git a/scrapling/fetchers/requests.py b/scrapling/fetchers/requests.py index 3d198a5..b559cd6 100644 --- a/scrapling/fetchers/requests.py +++ b/scrapling/fetchers/requests.py @@ -1,17 +1,9 @@ -from scrapling.core._types import ( - Callable, - Dict, - List, - Optional, - SelectorWaitStates, - Iterable, -) from scrapling.engines.static import ( FetcherSession, FetcherClient as _FetcherClient, AsyncFetcherClient as _AsyncFetcherClient, ) -from scrapling.engines.toolbelt.custom import BaseFetcher, Response +from scrapling.engines.toolbelt.custom import BaseFetcher __FetcherClientInstance__ = _FetcherClient() From 5127d386c921762a0fa1eb0f46dd8370427985fd Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 06:18:23 +0300 Subject: [PATCH 20/21] style(Fetcher): correcting type hints and annotation --- scrapling/engines/static.py | 401 +++++++++++++++++++----------------- 1 file changed, 213 insertions(+), 188 deletions(-) diff --git a/scrapling/engines/static.py b/scrapling/engines/static.py index 1622e9d..3c8bef9 100644 --- a/scrapling/engines/static.py +++ b/scrapling/engines/static.py @@ -1,7 +1,7 @@ from time import sleep as time_sleep from asyncio import sleep as asyncio_sleep -from curl_cffi.requests.session import CurlError +from curl_cffi.curl import CurlError from curl_cffi import CurlHttpVersion from curl_cffi.requests.impersonate import DEFAULT_CHROME from curl_cffi.requests import ( @@ -22,13 +22,14 @@ from scrapling.core._types import ( Awaitable, List, Any, + cast, ) from .toolbelt.custom import Response from .toolbelt.convertor import ResponseFactory from .toolbelt.fingerprints import generate_convincing_referer, generate_headers, __default_useragent__ -_UNSET = object() +_UNSET: Any = object() class FetcherSession: @@ -94,8 +95,8 @@ class FetcherSession: self.default_http3 = http3 self.selector_config = selector_config or {} - self._curl_session: Optional[CurlSession] | bool = None - self._async_curl_session: Optional[AsyncCurlSession] | bool = None + self._curl_session: Optional[CurlSession] = None + self._async_curl_session: Optional[AsyncCurlSession] = None def _merge_request_args(self, **kwargs) -> Dict[str, Any]: """Merge request-specific arguments with default session arguments.""" @@ -233,7 +234,7 @@ class FetcherSession: request_args: Dict[str, Any], max_retries: int, retry_delay: int, - selector_config: Optional[Dict] = None, + selector_config: Dict, ) -> Response: """ Perform an HTTP request using the configured session. @@ -273,7 +274,7 @@ class FetcherSession: request_args: Dict[str, Any], max_retries: int, retry_delay: int, - selector_config: Optional[Dict] = None, + selector_config: Dict, ) -> Response: """ Perform an HTTP request using the configured session. @@ -644,11 +645,11 @@ class FetcherSession: class FetcherClient(FetcherSession): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.__enter__ = None - self.__exit__ = None - self.__aenter__ = None - self.__aexit__ = None - self._curl_session = True + self.__enter__: Any = None + self.__exit__: Any = None + self.__aenter__: Any = None + self.__aexit__: Any = None + self._curl_session: Any = True # Setting the correct return types for the type checking/autocompletion def get( @@ -673,26 +674,29 @@ class FetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Response: - return super().get( - url, - params, - headers, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Response, + super().get( + url, + params, + headers, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def post( @@ -719,28 +723,31 @@ class FetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Response: - return super().post( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Response, + super().post( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def put( @@ -767,28 +774,31 @@ class FetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Response: - return super().put( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Response, + super().put( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def delete( @@ -815,39 +825,42 @@ class FetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Response: - return super().delete( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Response, + super().delete( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) class AsyncFetcherClient(FetcherSession): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.__enter__ = None - self.__exit__ = None - self.__aenter__ = None - self.__aexit__ = None - self._async_curl_session = True + self.__enter__: Any = None + self.__exit__: Any = None + self.__aenter__: Any = None + self.__aexit__: Any = None + self._async_curl_session: Any = True # Setting the correct return types for the type checking/autocompletion def get( @@ -872,26 +885,29 @@ class AsyncFetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Awaitable[Response]: - return super().get( - url, - params, - headers, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Awaitable[Response], + super().get( + url, + params, + headers, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def post( @@ -918,28 +934,31 @@ class AsyncFetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Awaitable[Response]: - return super().post( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Awaitable[Response], + super().post( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def put( @@ -966,28 +985,31 @@ class AsyncFetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Awaitable[Response]: - return super().put( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Awaitable[Response], + super().put( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) def delete( @@ -1014,26 +1036,29 @@ class AsyncFetcherClient(FetcherSession): stealthy_headers: Optional[bool] = _UNSET, **kwargs, ) -> Awaitable[Response]: - return super().delete( - url, - data, - json, - headers, - params, - cookies, - timeout, - follow_redirects, - max_redirects, - retries, - retry_delay, - proxies, - proxy, - proxy_auth, - auth, - verify, - cert, - impersonate, - http3, - stealthy_headers, - **kwargs, + return cast( + Awaitable[Response], + super().delete( + url, + data, + json, + headers, + params, + cookies, + timeout, + follow_redirects, + max_redirects, + retries, + retry_delay, + proxies, + proxy, + proxy_auth, + auth, + verify, + cert, + impersonate, + http3, + stealthy_headers, + **kwargs, + ), ) From a7e29a1662da46cf3dd1e33656728f8b24165098 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 1 Oct 2025 06:26:19 +0300 Subject: [PATCH 21/21] docs: Update introduction --- README.md | 8 +------- docs/index.md | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f18438e..49305d2 100644 --- a/README.md +++ b/README.md @@ -111,13 +111,7 @@ Built for the modern Web, Scrapling features its own rapid parsing engine and fe - πŸ“ **Auto Selector Generation**: Generate robust CSS/XPath selectors for any element. - πŸ”Œ **Familiar API**: Similar to Scrapy/BeautifulSoup with the same pseudo-elements used in Scrapy/Parsel. - πŸ“˜ **Complete Type Coverage**: Full type hints for excellent IDE support and code completion. - -### New Session Architecture -Scrapling 0.3 introduces a completely revamped session system: -- **Persistent Sessions**: Maintain cookies, headers, and authentication across multiple requests -- **Automatic Session Management**: Smart session lifecycle handling with proper cleanup -- **Session Inheritance**: All fetchers support both one-off requests and persistent session usage -- **Concurrent Session Support**: Run multiple isolated sessions simultaneously +- πŸ”‹ **Ready Docker image**: With each release, a Docker image containing all browsers is automatically built and pushed. ## Getting Started diff --git a/docs/index.md b/docs/index.md index 384d772..9f9288f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -74,6 +74,7 @@ Built for the modern Web, Scrapling features its own rapid parsing engine and fe - πŸ“ **Auto Selector Generation**: Generate robust CSS/XPath selectors for any element. - πŸ”Œ **Familiar API**: Similar to Scrapy/BeautifulSoup with the same pseudo-elements used in Scrapy/Parsel. - πŸ“˜ **Complete Type Coverage**: Full type hints for excellent IDE support and code completion. +- πŸ”‹ **Ready Docker image**: With each release, a Docker image containing all browsers is automatically built and pushed. ## Star History