From e04d59bd7974474792442d230dc88488286bed2f Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 2 Mar 2026 17:31:14 +0200 Subject: [PATCH 01/18] docs: update a sponsor status --- README.md | 20 ++++++++++++++++++-- docs/README_AR.md | 20 ++++++++++++++++++-- docs/README_CN.md | 20 ++++++++++++++++++-- docs/README_DE.md | 20 ++++++++++++++++++-- docs/README_ES.md | 20 ++++++++++++++++++-- docs/README_JP.md | 20 ++++++++++++++++++-- docs/README_RU.md | 20 ++++++++++++++++++-- docs/index.md | 3 ++- 8 files changed, 128 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8c047f2..ddc0c94 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,25 @@ MySpider().start() ``` # Platinum Sponsors + + + + + +
+ + +
+
🚀 + Subscribe + | 💬 + Discord + +
Scrapling's fetchers handle Cloudflare Turnstile, but for enterprise-grade protection, Hyper Solutions provides API endpoints that generate valid antibot tokens for: AkamaiDataDomeKasadaIncapsula +
No browser automation. Simple API calls returning the exact cookies and headers these systems require. +
-Do you want to be the first company to show up here? Click [here](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +Do you want to show your ad here? Click [here](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # Sponsors @@ -90,7 +107,6 @@ MySpider().start() - diff --git a/docs/README_AR.md b/docs/README_AR.md index 162bf17..9b801f6 100644 --- a/docs/README_AR.md +++ b/docs/README_AR.md @@ -74,8 +74,25 @@ MySpider().start() # الرعاة البلاتينيون + + + + + +
+ + +
+
🚀 + اشترك + | 💬 + Discord + +
جوالب Scrapling تتعامل مع Cloudflare Turnstile، لكن للحماية على مستوى المؤسسات، توفر Hyper Solutions نقاط نهاية API تولّد رموز antibot صالحة لـ: AkamaiDataDomeKasadaIncapsula +
بدون أتمتة متصفح. استدعاءات API بسيطة تُرجع الكوكيز والرؤوس الدقيقة التي تتطلبها هذه الأنظمة. +
-هل تريد أن تكون أول شركة تظهر هنا؟ انقر [هنا](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +هل تريد عرض إعلانك هنا؟ انقر [هنا](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # الرعاة @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/README_CN.md b/docs/README_CN.md index 1d886eb..eb15d76 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -74,8 +74,25 @@ MySpider().start() # 铂金赞助商 + + + + + +
+ + +
+
🚀 + 订阅 + | 💬 + Discord + +
Scrapling 的 fetcher 可以处理 Cloudflare Turnstile,但对于企业级保护,Hyper Solutions 提供生成有效 antibot 令牌的 API 端点,支持:AkamaiDataDomeKasadaIncapsula +
无需浏览器自动化。简单的 API 调用即可返回这些系统所需的确切 cookie 和请求头。 +
-想成为第一个出现在这里的公司吗?点击[这里](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +想在这里展示您的广告吗?点击[这里](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # 赞助商 @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/README_DE.md b/docs/README_DE.md index 6823f62..448893d 100644 --- a/docs/README_DE.md +++ b/docs/README_DE.md @@ -74,8 +74,25 @@ MySpider().start() # Platin-Sponsoren + + + + + +
+ + +
+
🚀 + Abonnieren + | 💬 + Discord + +
Scraplings Fetcher bewältigen Cloudflare Turnstile, aber für Schutz auf Unternehmensebene bietet Hyper Solutions API-Endpunkte, die gültige Antibot-Tokens generieren für: AkamaiDataDomeKasadaIncapsula +
Keine Browser-Automatisierung. Einfache API-Aufrufe, die genau die Cookies und Header zurückgeben, die diese Systeme erfordern. +
-Möchten Sie das erste Unternehmen sein, das hier erscheint? Klicken Sie [hier](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +Möchten Sie Ihre Anzeige hier zeigen? Klicken Sie [hier](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # Sponsoren @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/README_ES.md b/docs/README_ES.md index 7411bc6..6f12758 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -74,8 +74,25 @@ MySpider().start() # Patrocinadores Platino + + + + + +
+ + +
+
🚀 + Suscribirse + | 💬 + Discord + +
Los fetchers de Scrapling manejan Cloudflare Turnstile, pero para protección de nivel empresarial, Hyper Solutions proporciona endpoints API que generan tokens antibot válidos para: AkamaiDataDomeKasadaIncapsula +
Sin automatización de navegador. Simples llamadas API que devuelven las cookies y cabeceras exactas que estos sistemas requieren. +
-¿Quieres ser la primera empresa en aparecer aquí? Haz clic [aquí](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +¿Quieres mostrar tu anuncio aquí? Haz clic [aquí](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # Patrocinadores @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/README_JP.md b/docs/README_JP.md index 7392524..1e606ce 100644 --- a/docs/README_JP.md +++ b/docs/README_JP.md @@ -74,8 +74,25 @@ MySpider().start() # プラチナスポンサー + + + + + +
+ + +
+
🚀 + 登録 + | 💬 + Discord + +
ScraplingのfetcherはCloudflare Turnstileに対応していますが、エンタープライズレベルの保護には、Hyper Solutionsが有効なantibotトークンを生成するAPIエンドポイントを提供します:AkamaiDataDomeKasadaIncapsula +
ブラウザ自動化不要。これらのシステムが要求する正確なCookieとヘッダーを返すシンプルなAPI呼び出し。 +
-ここに最初に表示される企業になりませんか?[こちら](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646)をクリック +ここに広告を表示したいですか?[こちら](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646)をクリック # スポンサー @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/README_RU.md b/docs/README_RU.md index 1e49bc9..b0ab161 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -74,8 +74,25 @@ MySpider().start() # Платиновые спонсоры + + + + + +
+ + +
+
🚀 + Подписаться + | 💬 + Discord + +
Fetcher'ы Scrapling справляются с Cloudflare Turnstile, но для защиты корпоративного уровня Hyper Solutions предоставляет API-эндпоинты, генерирующие валидные antibot-токены для: AkamaiDataDomeKasadaIncapsula +
Без автоматизации браузера. Простые API-вызовы, возвращающие именно те cookie и заголовки, которые требуют эти системы. +
-Хотите стать первой компанией, которая появится здесь? Нажмите [здесь](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +Хотите показать здесь свою рекламу? Нажмите [здесь](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) # Спонсоры @@ -87,7 +104,6 @@ MySpider().start() - diff --git a/docs/index.md b/docs/index.md index 405b4aa..fe5deec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,8 @@ MySpider().start()
+ +

@@ -55,7 +57,6 @@ MySpider().start() -
From 183950662262dd50711396af48ab2f5d9afdbb65 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 3 Mar 2026 23:08:15 +0200 Subject: [PATCH 02/18] fix(browsers): make flag concatenation type-safe Better implementation of #157 that's accepted by pyright/mypy. Thanks to @rostchri . Co-Authored-By: Christian Rost --- scrapling/engines/_browsers/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapling/engines/_browsers/_base.py b/scrapling/engines/_browsers/_base.py index 51ce308..501db76 100644 --- a/scrapling/engines/_browsers/_base.py +++ b/scrapling/engines/_browsers/_base.py @@ -419,7 +419,7 @@ class BaseSessionMixin: if not config.cdp_url: flags = self._browser_options["args"] if config.extra_flags or extra_flags: - flags = list(set(flags + (config.extra_flags or extra_flags))) + flags = list(set(tuple(flags) + tuple(config.extra_flags or extra_flags or ()))) self._browser_options.update( { @@ -480,7 +480,7 @@ class StealthySessionMixin(BaseSessionMixin): config = cast(StealthConfig, self._config) flags: Tuple[str, ...] = tuple() if not config.cdp_url: - flags = DEFAULT_ARGS + STEALTH_ARGS + flags = tuple(DEFAULT_ARGS) + tuple(STEALTH_ARGS) if config.block_webrtc: flags += ( From 37f5c48a5fedf19782b89f808df85170f3887e74 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 3 Mar 2026 23:09:36 +0200 Subject: [PATCH 03/18] build: pump up the version Co-Authored-By: Christian Rost --- pyproject.toml | 2 +- scrapling/__init__.py | 2 +- server.json | 4 ++-- setup.cfg | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e16db98..984f971 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "scrapling" # Static version instead of a dynamic version so we can get better layer caching while building docker, check the docker file to understand -version = "0.4.1" +version = "0.4.2" 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"} diff --git a/scrapling/__init__.py b/scrapling/__init__.py index 1b35dbe..7a2230c 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -1,5 +1,5 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" -__version__ = "0.4.1" +__version__ = "0.4.2" __copyright__ = "Copyright (c) 2024 Karim Shoair" from typing import Any, TYPE_CHECKING diff --git a/server.json b/server.json index fa9a08e..ab3aa13 100644 --- a/server.json +++ b/server.json @@ -14,12 +14,12 @@ "mimeType": "image/png" } ], - "version": "0.4.1", + "version": "0.4.2", "packages": [ { "registryType": "pypi", "identifier": "scrapling", - "version": "0.4.1", + "version": "0.4.2", "runtimeHint": "uvx", "packageArguments": [ { diff --git a/setup.cfg b/setup.cfg index bad13d4..88711be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scrapling -version = 0.4.1 +version = 0.4.2 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 c8e1d8d75d82d2bbabc9925a6e7a0ec16a493790 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Thu, 5 Mar 2026 16:45:10 +0200 Subject: [PATCH 04/18] fix(type hints): use correct import for Python < 3.12 (Fixes #163 ) --- scrapling/core/_types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scrapling/core/_types.py b/scrapling/core/_types.py index f2fc097..25ddb5b 100644 --- a/scrapling/core/_types.py +++ b/scrapling/core/_types.py @@ -4,7 +4,6 @@ Type definitions for type checking purposes. from typing import ( TYPE_CHECKING, - TypedDict, TypeAlias, cast, overload, @@ -32,7 +31,7 @@ from typing import ( Coroutine, SupportsIndex, ) -from typing_extensions import Self, Unpack +from typing_extensions import Self, Unpack, TypedDict # Proxy can be a string URL or a dict (Playwright format: {"server": "...", "username": "...", "password": "..."}) ProxyType = Union[str, Dict[str, str]] From 5457a697faec1ea76015c022ba5630a5cb27f95c Mon Sep 17 00:00:00 2001 From: Matt Hillebrand Date: Thu, 5 Mar 2026 20:34:44 -0800 Subject: [PATCH 05/18] fix: Selector.get_all_text() doesn't get all text #167 - Updated text extraction using recursion - Added new unit test --- scrapling/parser.py | 24 +++++++++++++++++------- tests/parser/test_general.py | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/scrapling/parser.py b/scrapling/parser.py index 166e6ea..e510fdf 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -304,13 +304,23 @@ class Selector(SelectorsGeneration): ignored_elements.update(cast(list, _find_all_elements(element))) _all_strings = [] - for node in self._root.iter(): - if node not in ignored_elements: - text = node.text - if text and isinstance(text, str): - processed_text = text.strip() if strip else text - if not valid_values or processed_text.strip(): - _all_strings.append(processed_text) + + def append_text(text: Any) -> None: + if text and isinstance(text, str): + processed_text = text.strip() if strip else text + if not valid_values or processed_text.strip(): + _all_strings.append(processed_text) + + def walk(node: Any) -> None: + if node in ignored_elements: + return + + append_text(node.text) + for child in node: + walk(child) + append_text(child.tail) + + walk(self._root) return cast(TextHandler, TextHandler(separator).join(_all_strings)) diff --git a/tests/parser/test_general.py b/tests/parser/test_general.py index 26f6e6c..d459d44 100644 --- a/tests/parser/test_general.py +++ b/tests/parser/test_general.py @@ -327,6 +327,30 @@ def test_getting_all_text(page): assert page.get_all_text() != "" +def test_getting_all_text_from_nested_content(): + """Test getting all text preserves interleaved text nodes""" + html = """ + + +
+ string1 + string2 + string3 +
+ string4 +
+ string5 +
+ + + """ + + page = Selector(html, adaptive=False) + node = page.css("main")[0] + + assert node.get_all_text("\n", strip=True) == "string1\nstring2\nstring3\nstring4\nstring5" + + def test_regex_on_text(page): """Test regex operations on text""" element = page.css('[data-id="1"] .price')[0] From 5806b8e01489e4d03168ae4e7890ab49b41aabd0 Mon Sep 17 00:00:00 2001 From: Matt Hillebrand Date: Fri, 6 Mar 2026 09:01:32 -0800 Subject: [PATCH 06/18] - Add pre-compiled XPath text selector - Avoid recursion --- scrapling/parser.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/scrapling/parser.py b/scrapling/parser.py index e510fdf..eced0a9 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -58,6 +58,7 @@ _find_all_elements = XPath(".//*") _find_all_elements_with_spaces = XPath( ".//*[normalize-space(text())]" ) # This selector gets all elements with text content +_find_all_text_nodes = XPath(".//text()") class Selector(SelectorsGeneration): @@ -299,28 +300,31 @@ class Selector(SelectorsGeneration): ignored_elements: set[Any] = set() if ignore_tags: - for element in self._root.iter(*ignore_tags): - ignored_elements.add(element) - ignored_elements.update(cast(list, _find_all_elements(element))) + ignored_elements.update(self._root.iter(*ignore_tags)) _all_strings = [] - def append_text(text: Any) -> None: - if text and isinstance(text, str): - processed_text = text.strip() if strip else text - if not valid_values or processed_text.strip(): - _all_strings.append(processed_text) + def append_text(text: str) -> None: + processed_text = text.strip() if strip else text + if not valid_values or processed_text.strip(): + _all_strings.append(processed_text) - def walk(node: Any) -> None: - if node in ignored_elements: - return + def is_visible_text_node(text_node: _ElementUnicodeResult) -> bool: + parent = text_node.getparent() + if parent is None: + return False - append_text(node.text) - for child in node: - walk(child) - append_text(child.tail) + owner = parent.getparent() if text_node.is_tail else parent + while owner is not None: + if owner in ignored_elements: + return False + owner = owner.getparent() + return True - walk(self._root) + for text_node in cast(list[_ElementUnicodeResult], _find_all_text_nodes(self._root)): + text = str(text_node) + if text and is_visible_text_node(text_node): + append_text(text) return cast(TextHandler, TextHandler(separator).join(_all_strings)) From 00ad6b4c5f3dc0f219cf7151d14f383b23df2a41 Mon Sep 17 00:00:00 2001 From: Matt Hillebrand Date: Fri, 6 Mar 2026 11:39:00 -0800 Subject: [PATCH 07/18] Add two ignored elements to unit test --- tests/parser/test_general.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/parser/test_general.py b/tests/parser/test_general.py index d459d44..36241a6 100644 --- a/tests/parser/test_general.py +++ b/tests/parser/test_general.py @@ -340,6 +340,8 @@ def test_getting_all_text_from_nested_content(): string4 string5 + + From 7cbe566acdbc3cfc78a375b454bf6c226e10a665 Mon Sep 17 00:00:00 2001 From: Matt Hillebrand Date: Sun, 8 Mar 2026 08:14:35 -0700 Subject: [PATCH 08/18] Move test for get_all_text to test_parser_advanced.py --- tests/parser/test_general.py | 26 -------------------------- tests/parser/test_parser_advanced.py | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/tests/parser/test_general.py b/tests/parser/test_general.py index 36241a6..26f6e6c 100644 --- a/tests/parser/test_general.py +++ b/tests/parser/test_general.py @@ -327,32 +327,6 @@ def test_getting_all_text(page): assert page.get_all_text() != "" -def test_getting_all_text_from_nested_content(): - """Test getting all text preserves interleaved text nodes""" - html = """ - - -
- string1 - string2 - string3 -
- string4 -
- string5 - - -
- - - """ - - page = Selector(html, adaptive=False) - node = page.css("main")[0] - - assert node.get_all_text("\n", strip=True) == "string1\nstring2\nstring3\nstring4\nstring5" - - def test_regex_on_text(page): """Test regex operations on text""" element = page.css('[data-id="1"] .price')[0] diff --git a/tests/parser/test_parser_advanced.py b/tests/parser/test_parser_advanced.py index 57086ba..f34ba5c 100644 --- a/tests/parser/test_parser_advanced.py +++ b/tests/parser/test_parser_advanced.py @@ -183,6 +183,33 @@ class TestAdvancedSelectors: text = page.get_all_text(valid_values=False) assert text != "" + def test_get_all_text_preserves_interleaved_text_nodes(self): + """Test get_all_text preserves interleaved text nodes""" + html = """ + + +
+ string1 + string2 + string3 +
+ string4 +
+ string5 + + string6 + + string7 +
+ + + """ + + page = Selector(html, adaptive=False) + node = page.css("main")[0] + + assert node.get_all_text("\n", strip=True) == "string1\nstring2\nstring3\nstring4\nstring5\nstring6\nstring7" + class TestTextHandlerAdvanced: """Test advanced TextHandler functionality""" From 68b9e128bca0a7ab532de791faf3b8b40b26cc9a Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Sun, 8 Mar 2026 11:32:04 -0400 Subject: [PATCH 09/18] fix: return bare Google URL in referer instead of search query Real browsers send `https://www.google.com/` as the Referer header when clicking search results, not the full search URL with query parameters. The previous format was a fingerprinting signal that the referer was spoofed. Closes #172 --- scrapling/engines/toolbelt/fingerprints.py | 8 ++++---- tests/fetchers/test_utils.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/scrapling/engines/toolbelt/fingerprints.py b/scrapling/engines/toolbelt/fingerprints.py index 6774691..d84a682 100644 --- a/scrapling/engines/toolbelt/fingerprints.py +++ b/scrapling/engines/toolbelt/fingerprints.py @@ -20,13 +20,13 @@ chrome_version = 143 @lru_cache(10, typed=True) def generate_convincing_referer(url: str) -> str | None: - """Takes the domain from the URL without the subdomain/suffix and make it look like you were searching Google for this website + """Generate a convincing Google referer header >>> generate_convincing_referer('https://www.somewebsite.com/blah') - 'https://www.google.com/search?q=somewebsite' + 'https://www.google.com/' :param url: The URL you are about to fetch. - :return: Google's search URL of the domain name, or None for localhost/IP addresses + :return: Google's URL as referer, or None for localhost/IP addresses """ # Fixing the inaccurate return type hint in `get_tld` extracted: Result | None = cast(Result, get_tld(url, as_object=True, fail_silently=True)) @@ -43,7 +43,7 @@ def generate_convincing_referer(url: str) -> str | None: if all(part.isdigit() for part in website_name.split(".") if part): return None - return f"https://www.google.com/search?q={website_name}" + return "https://www.google.com/" @lru_cache(1, typed=True) diff --git a/tests/fetchers/test_utils.py b/tests/fetchers/test_utils.py index 0a4be9f..526f898 100644 --- a/tests/fetchers/test_utils.py +++ b/tests/fetchers/test_utils.py @@ -209,8 +209,7 @@ class TestFingerprintFunctions: url = "https://sub.example.com/page.html" result = generate_convincing_referer(url) - assert result.startswith("https://www.google.com/search?q=") - assert "example" in result + assert result == "https://www.google.com/" def test_generate_convincing_referer_caching(self): """Test referer generation caching""" From 27409927a3557de3571b2c280507956b2f53f398 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 8 Mar 2026 23:57:10 +0200 Subject: [PATCH 10/18] build: pump up deps and browsers versions --- pyproject.toml | 7 ++++--- scrapling/engines/toolbelt/fingerprints.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 984f971..3b00dbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ "lxml>=6.0.2", "cssselect>=1.4.0", "orjson>=3.11.7", - "tld>=0.13.1", + "tld>=0.13.2", "w3lib>=2.4.0", "typing_extensions", ] @@ -73,9 +73,10 @@ dependencies = [ fetchers = [ "click>=8.3.0", "curl_cffi>=0.14.0", - "playwright==1.56.0", - "patchright==1.56.0", + "playwright==1.58.0", + "patchright==1.58.2", "browserforge>=1.2.4", + "apify-fingerprint-datapoints>=0.11.0", "msgspec>=0.20.0", "anyio>=4.12.1" ] diff --git a/scrapling/engines/toolbelt/fingerprints.py b/scrapling/engines/toolbelt/fingerprints.py index d84a682..f755fb4 100644 --- a/scrapling/engines/toolbelt/fingerprints.py +++ b/scrapling/engines/toolbelt/fingerprints.py @@ -14,8 +14,8 @@ from scrapling.core._types import Dict, Literal, Tuple, cast __OS_NAME__ = platform_system() OSName = Literal["linux", "macos", "windows"] # Current versions hardcoded for now (Playwright doesn't allow to know the version of a browser without launching it) -chromium_version = 141 -chrome_version = 143 +chromium_version = 145 +chrome_version = 145 @lru_cache(10, typed=True) From b547e04207a9b691bc0dbffcc3da593dc7481070 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:05:46 +0200 Subject: [PATCH 11/18] ops: update playwright pinned version --- .github/workflows/tests.yml | 2 +- tests/requirements.txt | 2 +- tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 790170e..1cf6139 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,7 +73,7 @@ jobs: - name: Install all browsers dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install playwright==1.56.0 patchright==1.56.0 + python3 -m pip install playwright==1.58.0 patchright==1.58.2 - name: Get Playwright version id: playwright-version diff --git a/tests/requirements.txt b/tests/requirements.txt index 07e287c..00ee277 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ pytest>=2.8.0,<9 pytest-cov -playwright==1.56.0 +playwright==1.58.0 werkzeug<3.0.0 pytest-httpbin==2.1.0 pytest-asyncio diff --git a/tox.ini b/tox.ini index c2a30ae..ba85fe3 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,8 @@ envlist = pre-commit,py{310,311,312,313} usedevelop = True changedir = tests deps = - playwright==1.56.0 - patchright==1.56.0 + playwright==1.58.0 + patchright==1.58.2 -r{toxinidir}/tests/requirements.txt extras = ai,shell commands = From a929de6ca4f6f84f09b42226ee5670643cffccc8 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:25:19 +0200 Subject: [PATCH 12/18] fix: update code and docstrings to remove the old google referer logic --- scrapling/core/ai.py | 20 +++++++------- scrapling/engines/_browsers/_controllers.py | 25 +++++++----------- scrapling/engines/_browsers/_stealth.py | 25 +++++++----------- scrapling/engines/static.py | 8 +++--- scrapling/engines/toolbelt/fingerprints.py | 29 --------------------- scrapling/fetchers/chrome.py | 4 +-- scrapling/fetchers/stealth_chrome.py | 8 +++--- 7 files changed, 40 insertions(+), 79 deletions(-) diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index f637437..3481970 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -105,7 +105,7 @@ class ScraplingMCPServer: :param auth: HTTP basic auth in dictionary format with `username` and `password` keys. :param verify: Whether to verify HTTPS certificates. :param http3: Whether to use HTTP3. Defaults to False. It might be problematic if used it with `impersonate`. - :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets the referer header as if this request came from a Google search of URL's domain. + :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets a Google referer header. """ normalized_proxy_auth = _normalize_credentials(proxy_auth) normalized_auth = _normalize_credentials(auth) @@ -186,7 +186,7 @@ class ScraplingMCPServer: :param auth: HTTP basic auth in dictionary format with `username` and `password` keys. :param verify: Whether to verify HTTPS certificates. :param http3: Whether to use HTTP3. Defaults to False. It might be problematic if used it with `impersonate`. - :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets the referer header as if this request came from a Google search of URL's domain. + :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets a Google referer header. """ normalized_proxy_auth = _normalize_credentials(proxy_auth) normalized_auth = _normalize_credentials(auth) @@ -276,8 +276,8 @@ class ScraplingMCPServer: :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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. """ page = await DynamicFetcher.async_fetch( @@ -358,8 +358,8 @@ class ScraplingMCPServer: :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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. """ async with AsyncDynamicSession( @@ -454,8 +454,8 @@ class ScraplingMCPServer: :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param block_webrtc: Forces WebRTC to respect proxy settings to prevent local IP address leak. :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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 additional_args: Additional arguments to be passed to Playwright's context as additional settings, and it takes higher priority than Scrapling's settings. """ @@ -551,8 +551,8 @@ class ScraplingMCPServer: :param hide_canvas: Add random noise to canvas operations to prevent fingerprinting. :param block_webrtc: Forces WebRTC to respect proxy settings to prevent local IP address leak. :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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 additional_args: Additional arguments to be passed to Playwright's context as additional settings, and it takes higher priority than Scrapling's settings. """ diff --git a/scrapling/engines/_browsers/_controllers.py b/scrapling/engines/_browsers/_controllers.py index 5d9e801..fa6e8b6 100644 --- a/scrapling/engines/_browsers/_controllers.py +++ b/scrapling/engines/_browsers/_controllers.py @@ -14,7 +14,6 @@ from scrapling.core.utils import log from scrapling.core._types import Optional, ProxyType, Unpack from scrapling.engines.toolbelt.proxy_rotation import is_proxy_error from scrapling.engines.toolbelt.convertor import Response, ResponseFactory -from scrapling.engines.toolbelt.fingerprints import generate_convincing_referer from scrapling.engines._browsers._types import PlaywrightSession, PlaywrightFetchParams from scrapling.engines._browsers._base import SyncSession, AsyncSession, DynamicSessionMixin from scrapling.engines._browsers._validators import validate_fetch as _validate, PlaywrightConfig @@ -58,8 +57,8 @@ class DynamicSession(SyncSession, DynamicSessionMixin): :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 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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. :param extra_flags: A list of additional browser flags to pass to the browser on launch. @@ -103,11 +102,11 @@ class DynamicSession(SyncSession, DynamicSessionMixin): """Opens up the browser and do your request based on your chosen options. :param url: The Target url. - :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 google_search: Enabled by default, Scrapling will set a Google referer header. :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 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 extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). @@ -127,9 +126,7 @@ class DynamicSession(SyncSession, DynamicSessionMixin): request_headers_keys = {h.lower() for h in params.extra_headers.keys()} if params.extra_headers else set() referer = ( - generate_convincing_referer(url) - if (params.google_search and "referer" not in request_headers_keys) - else None + "https://www.google.com/" if (params.google_search and "referer" not in request_headers_keys) else None ) for attempt in range(self._config.retries): @@ -226,8 +223,8 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 max_pages: The maximum number of tabs to be opened at the same time. It will be used in rotation through a PagePool. :param user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. @@ -271,11 +268,11 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): """Opens up the browser and do your request based on your chosen options. :param url: The Target url. - :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 google_search: Enabled by default, Scrapling will set a Google referer header. :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 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 extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). @@ -296,9 +293,7 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): request_headers_keys = {h.lower() for h in params.extra_headers.keys()} if params.extra_headers else set() referer = ( - generate_convincing_referer(url) - if (params.google_search and "referer" not in request_headers_keys) - else None + "https://www.google.com/" if (params.google_search and "referer" not in request_headers_keys) else None ) for attempt in range(self._config.retries): diff --git a/scrapling/engines/_browsers/_stealth.py b/scrapling/engines/_browsers/_stealth.py index 797a0c1..8fb248f 100644 --- a/scrapling/engines/_browsers/_stealth.py +++ b/scrapling/engines/_browsers/_stealth.py @@ -16,7 +16,6 @@ from scrapling.core.utils import log from scrapling.core._types import Any, Optional, ProxyType, Unpack from scrapling.engines.toolbelt.proxy_rotation import is_proxy_error from scrapling.engines.toolbelt.convertor import Response, ResponseFactory -from scrapling.engines.toolbelt.fingerprints import generate_convincing_referer from scrapling.engines._browsers._types import StealthSession, StealthFetchParams from scrapling.engines._browsers._base import SyncSession, AsyncSession, StealthySessionMixin from scrapling.engines._browsers._validators import validate_fetch as _validate, StealthConfig @@ -66,8 +65,8 @@ class StealthySession(SyncSession, StealthySessionMixin): :param allow_webgl: Enabled by default. Disabling it disables WebGL and WebGL 2.0 support entirely. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. :param extra_flags: A list of additional browser flags to pass to the browser on launch. @@ -189,11 +188,11 @@ class StealthySession(SyncSession, StealthySessionMixin): """Opens up the browser and do your request based on your chosen options. :param url: The Target url. - :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 google_search: Enabled by default, Scrapling will set a Google referer header. :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 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 extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). @@ -214,9 +213,7 @@ class StealthySession(SyncSession, StealthySessionMixin): request_headers_keys = {h.lower() for h in params.extra_headers.keys()} if params.extra_headers else set() referer = ( - generate_convincing_referer(url) - if (params.google_search and "referer" not in request_headers_keys) - else None + "https://www.google.com/" if (params.google_search and "referer" not in request_headers_keys) else None ) for attempt in range(self._config.retries): @@ -322,8 +319,8 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): :param allow_webgl: Enabled by default. Disabling it disables WebGL and WebGL 2.0 support entirely. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. :param extra_flags: A list of additional browser flags to pass to the browser on launch. @@ -444,11 +441,11 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): """Opens up the browser and do your request based on your chosen options. :param url: The Target url. - :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 google_search: Enabled by default, Scrapling will set a Google referer header. :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 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 extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). @@ -470,9 +467,7 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): request_headers_keys = {h.lower() for h in params.extra_headers.keys()} if params.extra_headers else set() referer = ( - generate_convincing_referer(url) - if (params.google_search and "referer" not in request_headers_keys) - else None + "https://www.google.com/" if (params.google_search and "referer" not in request_headers_keys) else None ) for attempt in range(self._config.retries): diff --git a/scrapling/engines/static.py b/scrapling/engines/static.py index 204c105..1f4b09b 100644 --- a/scrapling/engines/static.py +++ b/scrapling/engines/static.py @@ -26,7 +26,7 @@ from .toolbelt.custom import Response from .toolbelt.convertor import ResponseFactory from .toolbelt.proxy_rotation import ProxyRotator, is_proxy_error from ._browsers._types import RequestsSession, GetRequestParams, DataRequestParams, ImpersonateType -from .toolbelt.fingerprints import generate_convincing_referer, generate_headers, __default_useragent__ +from .toolbelt.fingerprints import generate_headers, __default_useragent__ _NO_SESSION: Any = object() @@ -166,14 +166,14 @@ class _ConfigurationLogic(ABC): """ 1. Adds a useragent to the headers if it doesn't have one 2. Generates real headers and append them to current headers - 3. Generates a referer header that looks like as if this request came from a Google's search of the current URL's domain. + 3. Sets a Google referer header. """ # Merge session headers with request headers, request takes precedence (if it was set) final_headers = {**self._default_headers, **(headers if headers else {})} headers_keys = {k.lower() for k in final_headers} if stealth: if "referer" not in headers_keys: - final_headers["referer"] = generate_convincing_referer(url) + final_headers["referer"] = "https://www.google.com/" if not impersonate_enabled: # Curl will generate the suitable headers extra_headers = generate_headers(browser_mode=False) @@ -672,7 +672,7 @@ class FetcherSession: """ :param impersonate: Browser version to impersonate. Can be a single browser string or a list of browser strings for random selection. (Default: latest available Chrome version) :param http3: Whether to use HTTP3. Defaults to False. It might be problematic if used it with `impersonate`. - :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets the referer header as if this request came from a Google search of URL's domain. + :param stealthy_headers: If enabled (default), it creates and adds real browser headers. It also sets a Google referer header. :param proxies: Dict of proxies to use. Format: {"http": proxy_url, "https": proxy_url}. :param proxy: Proxy URL to use. Format: "http://username:password@localhost:8030". Cannot be used together with the `proxies` parameter. diff --git a/scrapling/engines/toolbelt/fingerprints.py b/scrapling/engines/toolbelt/fingerprints.py index f755fb4..9a84199 100644 --- a/scrapling/engines/toolbelt/fingerprints.py +++ b/scrapling/engines/toolbelt/fingerprints.py @@ -5,7 +5,6 @@ Functions related to generating headers and fingerprints generally from functools import lru_cache from platform import system as platform_system -from tld import get_tld, Result from browserforge.headers import Browser, HeaderGenerator from browserforge.headers.generator import SUPPORTED_OPERATING_SYSTEMS @@ -18,34 +17,6 @@ chromium_version = 145 chrome_version = 145 -@lru_cache(10, typed=True) -def generate_convincing_referer(url: str) -> str | None: - """Generate a convincing Google referer header - - >>> generate_convincing_referer('https://www.somewebsite.com/blah') - 'https://www.google.com/' - - :param url: The URL you are about to fetch. - :return: Google's URL as referer, or None for localhost/IP addresses - """ - # Fixing the inaccurate return type hint in `get_tld` - extracted: Result | None = cast(Result, get_tld(url, as_object=True, fail_silently=True)) - if not extracted: - return None - - website_name = extracted.domain - - # Skip generating referer for localhost, IP addresses, or when there's no valid domain - if not website_name or not extracted.tld or website_name in ("localhost", "127.0.0.1", "::1"): - return None - - # Check if it's an IP address (simple check for IPv4) - if all(part.isdigit() for part in website_name.split(".") if part): - return None - - return "https://www.google.com/" - - @lru_cache(1, typed=True) def get_os_name() -> OSName | Tuple: """Get the current OS name in the same format needed for browserforge, if the OS is Unknown, return None so browserforge uses all. diff --git a/scrapling/fetchers/chrome.py b/scrapling/fetchers/chrome.py index 3b746f4..594b178 100644 --- a/scrapling/fetchers/chrome.py +++ b/scrapling/fetchers/chrome.py @@ -28,7 +28,7 @@ class DynamicFetcher(BaseFetcher): :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. :param extra_headers: A dictionary of extra headers to add to the request. :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 extra_flags: A list of additional browser flags to pass to the browser on launch. @@ -68,7 +68,7 @@ class DynamicFetcher(BaseFetcher): :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. :param extra_headers: A dictionary of extra headers to add to the request. :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 extra_flags: A list of additional browser flags to pass to the browser on launch. diff --git a/scrapling/fetchers/stealth_chrome.py b/scrapling/fetchers/stealth_chrome.py index 6a70246..1cf89f3 100644 --- a/scrapling/fetchers/stealth_chrome.py +++ b/scrapling/fetchers/stealth_chrome.py @@ -39,8 +39,8 @@ class StealthyFetcher(BaseFetcher): :param allow_webgl: Enabled by default. Disabling it disables WebGL and WebGL 2.0 support entirely. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. :param extra_flags: A list of additional browser flags to pass to the browser on launch. @@ -88,8 +88,8 @@ class StealthyFetcher(BaseFetcher): :param allow_webgl: Enabled by default. Disabling it disables WebGL and WebGL 2.0 support entirely. Disabling WebGL is not recommended as many WAFs now check if WebGL is enabled. :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 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 google_search: Enabled by default, Scrapling will set a Google referer header. + :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 user_data_dir: Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. :param extra_flags: A list of additional browser flags to pass to the browser on launch. From 8e3bf89e6cb98aeea6223884404fe25df898513d Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:27:10 +0200 Subject: [PATCH 13/18] fix(agent): update the agent skills docs and version --- agent-skill/Scrapling-Skill/SKILL.md | 4 ++-- agent-skill/Scrapling-Skill/examples/README.md | 2 +- agent-skill/Scrapling-Skill/references/fetching/dynamic.md | 6 +++--- agent-skill/Scrapling-Skill/references/fetching/static.md | 2 +- agent-skill/Scrapling-Skill/references/fetching/stealthy.md | 6 +++--- agent-skill/Scrapling-Skill/references/mcp-server.md | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/agent-skill/Scrapling-Skill/SKILL.md b/agent-skill/Scrapling-Skill/SKILL.md index 4578893..724e704 100644 --- a/agent-skill/Scrapling-Skill/SKILL.md +++ b/agent-skill/Scrapling-Skill/SKILL.md @@ -1,7 +1,7 @@ --- name: scrapling-official description: Scrape web pages using Scrapling with anti-bot bypass (like Cloudflare Turnstile), stealth headless browsing, spiders framework, adaptive scraping, and JavaScript rendering. Use when asked to scrape, crawl, or extract data from websites; web_fetch fails; the site has anti-bot protections; write Python code to scrape/crawl; or write spiders. -version: 0.4.1 +version: 0.4.2 license: Complete terms in LICENSE.txt --- @@ -22,7 +22,7 @@ Blazing fast crawls with real-time stats and streaming. Built by Web Scrapers fo Create a virtual Python environment through any way available, like `venv`, then inside the environment do: -`pip install "scrapling[all]>=0.4.1"` +`pip install "scrapling[all]>=0.4.2"` Then do this to download all the browsers' dependencies: diff --git a/agent-skill/Scrapling-Skill/examples/README.md b/agent-skill/Scrapling-Skill/examples/README.md index 4d5741a..dfc65e4 100644 --- a/agent-skill/Scrapling-Skill/examples/README.md +++ b/agent-skill/Scrapling-Skill/examples/README.md @@ -9,7 +9,7 @@ All examples collect **all 100 quotes across 10 pages**. Make sure Scrapling is installed: ```bash -pip install "scrapling[all]>=0.4.1" +pip install "scrapling[all]>=0.4.2" scrapling install --force ``` diff --git a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md index ea3369e..a87fdf6 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md +++ b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md @@ -64,8 +64,8 @@ All arguments for `DynamicFetcher` and its session classes: | 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._ | ✔️ | +| google_search | Enabled by default, Scrapling will set a Google referer header. | ✔️ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 only the keys 'server', 'username', and 'password'. | ✔️ | | 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 | Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value, as well as number and date formatting rules. Defaults to the system default locale. | ✔️ | @@ -84,7 +84,7 @@ In session classes, all these arguments can be set globally for the session. Sti **Notes:** 1. The `disable_resources` option made requests ~25% faster in tests for some websites and can help save proxy usage, but be careful with it, as it can cause some websites to never finish loading. -2. The `google_search` argument is enabled by default for all requests, making the request appear to come from a Google search page. So, a request for `https://example.com` will set the referer to `https://www.google.com/search?q=example`. Also, if used together, it takes priority over the referer set by the `extra_headers` argument. +2. The `google_search` argument is enabled by default for all requests, setting the referer to `https://www.google.com/`. If used together with `extra_headers`, it takes priority over the referer set there. 3. Since version 0.3.13, the `stealth` option has been removed here in favor of the `StealthyFetcher` class, and the `hide_canvas` option has been moved to it. The `disable_webgl` argument has been moved to the `StealthyFetcher` class and renamed as `allow_webgl`. 4. If you didn't set a user agent and enabled headless mode, the fetcher will generate a real user agent for the same browser version and use it. If you didn't set a user agent and didn't enable headless mode, the fetcher will use the browser's default user agent, which is the same as in standard browsers in the latest versions. diff --git a/agent-skill/Scrapling-Skill/references/fetching/static.md b/agent-skill/Scrapling-Skill/references/fetching/static.md index 72ff74a..fa4d2a5 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/static.md +++ b/agent-skill/Scrapling-Skill/references/fetching/static.md @@ -14,7 +14,7 @@ Check out how to configure the parsing options [here](choosing.md#parser-configu All methods for making requests here share some arguments, so let's discuss them first. - **url**: The targeted URL -- **stealthy_headers**: If enabled (default), it creates and adds real browser headers. It also sets the referer header as if this request came from a Google search of the URL's domain. +- **stealthy_headers**: If enabled (default), it creates and adds real browser headers. It also sets a Google referer header. - **follow_redirects**: As the name implies, tell the fetcher to follow redirections. **Enabled by default** - **timeout**: The number of seconds to wait for each request to be finished. **Defaults to 30 seconds**. - **retries**: The number of retries that the fetcher will do for failed requests. **Defaults to three retries**. diff --git a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md index 1bde6f4..98aecd5 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md +++ b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md @@ -43,8 +43,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | 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._ | ✔️ | +| google_search | Enabled by default, Scrapling will set a Google referer header. | ✔️ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 only the keys 'server', 'username', and 'password'. | ✔️ | | 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 | Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value, as well as number and date formatting rules. Defaults to the system default locale. | ✔️ | @@ -69,7 +69,7 @@ In session classes, all these arguments can be set globally for the session. Sti 1. It's basically the same arguments as [DynamicFetcher](dynamic.md) class, but with these additional arguments: `solve_cloudflare`, `block_webrtc`, `hide_canvas`, and `allow_webgl`. 2. The `disable_resources` option made requests ~25% faster in tests for some websites and can help save proxy usage, but be careful with it, as it can cause some websites to never finish loading. -3. The `google_search` argument is enabled by default for all requests, making the request appear to come from a Google search page. So, a request for `https://example.com` will set the referer to `https://www.google.com/search?q=example`. Also, if used together, it takes priority over the referer set by the `extra_headers` argument. +3. The `google_search` argument is enabled by default for all requests, setting the referer to `https://www.google.com/`. If used together with `extra_headers`, it takes priority over the referer set there. 4. If you didn't set a user agent and enabled headless mode, the fetcher will generate a real user agent for the same browser version and use it. If you didn't set a user agent and didn't enable headless mode, the fetcher will use the browser's default user agent, which is the same as in standard browsers in the latest versions. ## Examples diff --git a/agent-skill/Scrapling-Skill/references/mcp-server.md b/agent-skill/Scrapling-Skill/references/mcp-server.md index fae1f14..fbe1b7d 100644 --- a/agent-skill/Scrapling-Skill/references/mcp-server.md +++ b/agent-skill/Scrapling-Skill/references/mcp-server.md @@ -25,7 +25,7 @@ Fast HTTP GET with browser fingerprint impersonation (TLS, headers). Suitable fo | `timeout` | number | 30 | Seconds before timeout | | `retries` | int | 3 | Retry attempts on failure | | `retry_delay` | int | 1 | Seconds between retries | -| `stealthy_headers` | bool | true | Generate realistic browser headers and Google-search referer | +| `stealthy_headers` | bool | true | Generate realistic browser headers and Google referer | | `http3` | bool | false | Use HTTP/3 (may conflict with `impersonate`) | | `follow_redirects` | bool | true | Follow HTTP redirects | | `max_redirects` | int | 30 | Max redirects (-1 for unlimited) | @@ -58,7 +58,7 @@ Opens a Chromium browser via Playwright to render JavaScript. Suitable for dynam | `wait_selector_state` | str | `"attached"` | State for wait_selector: `"attached"` / `"visible"` / `"hidden"` / `"detached"` | | `network_idle` | bool | false | Wait until no network activity for 500ms | | `disable_resources` | bool | false | Block fonts, images, media, stylesheets, etc. for speed | -| `google_search` | bool | true | Set referer as if from Google search | +| `google_search` | bool | true | Set a Google referer header | | `real_chrome` | bool | false | Use locally installed Chrome instead of bundled Chromium | | `cdp_url` | str or null | null | Connect to existing browser via CDP URL | | `extra_headers` | dict or null | null | Additional request headers | From d48ac78c791bb8ca0c070054cf64771da2fe9fab Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:28:32 +0200 Subject: [PATCH 14/18] test: update tests accordingly --- tests/fetchers/test_utils.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/fetchers/test_utils.py b/tests/fetchers/test_utils.py index 526f898..bebb5fd 100644 --- a/tests/fetchers/test_utils.py +++ b/tests/fetchers/test_utils.py @@ -7,7 +7,6 @@ from scrapling.engines.toolbelt.navigation import ( create_async_intercept_handler, ) from scrapling.engines.toolbelt.fingerprints import ( - generate_convincing_referer, get_os_name, generate_headers ) @@ -204,21 +203,6 @@ class TestConstructProxyDict: class TestFingerprintFunctions: """Test fingerprint generation functions""" - def test_generate_convincing_referer(self): - """Test referer generation""" - url = "https://sub.example.com/page.html" - result = generate_convincing_referer(url) - - assert result == "https://www.google.com/" - - def test_generate_convincing_referer_caching(self): - """Test referer generation caching""" - url = "https://example.com" - result1 = generate_convincing_referer(url) - result2 = generate_convincing_referer(url) - - assert result1 == result2 - def test_get_os_name(self): """Test OS name detection""" result = get_os_name() From 73832f9dfe6bfcaab2e3cbf9b1be34118ee97520 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:29:54 +0200 Subject: [PATCH 15/18] docs: update the website to reflect the google referer logic --- .../Scrapling-Skill/references/fetching/stealthy.md | 3 +-- docs/fetching/dynamic.md | 6 +++--- docs/fetching/static.md | 2 +- docs/fetching/stealthy.md | 9 ++++----- docs/overview.md | 5 ++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md index 98aecd5..5708d61 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md +++ b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md @@ -21,8 +21,7 @@ The `StealthyFetcher` class is a stealthy version of the [DynamicFetcher](dynami 3. It isolates JS execution, removes many Playwright fingerprints, and stops detection through some of the known behaviors that bots do. 4. It generates canvas noise to prevent fingerprinting through canvas. 5. It automatically patches known methods to detect running in headless mode and provides an option to defeat timezone mismatch attacks. -6. It makes requests look as if they came from Google's search page of the requested website. -7. and other anti-protection options... +6. and other anti-protection options... ## Full list of arguments Scrapling provides many options with this fetcher and its session classes. Before jumping to the [examples](#examples), here's the full list of arguments diff --git a/docs/fetching/dynamic.md b/docs/fetching/dynamic.md index 31574a6..1d33a59 100644 --- a/docs/fetching/dynamic.md +++ b/docs/fetching/dynamic.md @@ -76,8 +76,8 @@ Scrapling provides many options with this fetcher and its session classes. To ma | 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._ | ✔️ | +| google_search | Enabled by default, Scrapling will set a Google referer header. | ✔️ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 only the keys 'server', 'username', and 'password'. | ✔️ | | 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 | Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value, as well as number and date formatting rules. Defaults to the system default locale. | ✔️ | @@ -97,7 +97,7 @@ In session classes, all these arguments can be set globally for the session. Sti !!! note "Notes:" 1. The `disable_resources` option made requests ~25% faster in my tests for some websites and can help save your proxy usage, but be careful with it, as it can cause some websites to never finish loading. - 2. The `google_search` argument is enabled by default for all requests, making the request appear to come from a Google search page. So, a request for `https://example.com` will set the referer to `https://www.google.com/search?q=example`. Also, if used together, it takes priority over the referer set by the `extra_headers` argument. + 2. The `google_search` argument is enabled by default for all requests, setting the referer to `https://www.google.com/`. If used together with `extra_headers`, it takes priority over the referer set there. 3. Since version 0.3.13, the `stealth` option has been removed here in favor of the `StealthyFetcher` class, and the `hide_canvas` option has been moved to it. The `disable_webgl` argument has been moved to the `StealthyFetcher` class and renamed as `allow_webgl`. 4. If you didn't set a user agent and enabled headless mode, the fetcher will generate a real user agent for the same browser version and use it. If you didn't set a user agent and didn't enable headless mode, the fetcher will use the browser's default user agent, which is the same as in standard browsers in the latest versions. diff --git a/docs/fetching/static.md b/docs/fetching/static.md index 0071785..587ebd1 100644 --- a/docs/fetching/static.md +++ b/docs/fetching/static.md @@ -20,7 +20,7 @@ Check out how to configure the parsing options [here](choosing.md#parser-configu All methods for making requests here share some arguments, so let's discuss them first. - **url**: The targeted URL -- **stealthy_headers**: If enabled (default), it creates and adds real browser headers. It also sets the referer header as if this request came from a Google search of the URL's domain. +- **stealthy_headers**: If enabled (default), it creates and adds real browser headers. It also sets a Google referer header. - **follow_redirects**: As the name implies, tell the fetcher to follow redirections. **Enabled by default** - **timeout**: The number of seconds to wait for each request to be finished. **Defaults to 30 seconds**. - **retries**: The number of retries that the fetcher will do for failed requests. **Defaults to three retries**. diff --git a/docs/fetching/stealthy.md b/docs/fetching/stealthy.md index fcbc8ea..4fd888e 100644 --- a/docs/fetching/stealthy.md +++ b/docs/fetching/stealthy.md @@ -32,8 +32,7 @@ The `StealthyFetcher` class is a stealthy version of the [DynamicFetcher](dynami 3. It isolates JS execution, removes many Playwright fingerprints, and stops detection through some of the known behaviors that bots do. 4. It generates canvas noise to prevent fingerprinting through canvas. 5. It automatically patches known methods to detect running in headless mode and provides an option to defeat timezone mismatch attacks. -6. It makes requests look as if they came from Google's search page of the requested website. -7. and other anti-protection options... +6. and other anti-protection options... ## Full list of arguments Scrapling provides many options with this fetcher and its session classes. Before jumping to the [examples](#examples), here's the full list of arguments @@ -54,8 +53,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | 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._ | ✔️ | +| google_search | Enabled by default, Scrapling will set a Google referer header. | ✔️ | +| extra_headers | A dictionary of extra headers to add to the request. _The referer set by `google_search` 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 only the keys 'server', 'username', and 'password'. | ✔️ | | 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 | Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value, as well as number and date formatting rules. Defaults to the system default locale. | ✔️ | @@ -80,7 +79,7 @@ In session classes, all these arguments can be set globally for the session. Sti 1. It's basically the same arguments as [DynamicFetcher](dynamic.md#introduction) class, but with these additional arguments: `solve_cloudflare`, `block_webrtc`, `hide_canvas`, and `allow_webgl`. 2. The `disable_resources` option made requests ~25% faster in my tests for some websites and can help save your proxy usage, but be careful with it, as it can cause some websites to never finish loading. - 3. The `google_search` argument is enabled by default for all requests, making the request appear to come from a Google search page. So, a request for `https://example.com` will set the referer to `https://www.google.com/search?q=example`. Also, if used together, it takes priority over the referer set by the `extra_headers` argument. + 3. The `google_search` argument is enabled by default for all requests, setting the referer to `https://www.google.com/`. If used together with `extra_headers`, it takes priority over the referer set there. 4. If you didn't set a user agent and enabled headless mode, the fetcher will generate a real user agent for the same browser version and use it. If you didn't set a user agent and didn't enable headless mode, the fetcher will use the browser's default user agent, which is the same as in standard browsers in the latest versions. ## Examples diff --git a/docs/overview.md b/docs/overview.md index aa91967..f9a0529 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -280,7 +280,7 @@ For Async requests, you will replace the import like below: !!! note "Notes:" - 1. You have the `stealthy_headers` argument, which, when enabled, makes requests to generate real browser headers and use them, including a referer header, as if this request came from a Google search of this domain. It's enabled by default. + 1. You have the `stealthy_headers` argument, which, when enabled, makes requests to generate real browser headers and use them, including a Google referer header. It's enabled by default. 2. The `impersonate` argument lets you fake the TLS fingerprint for a specific browser version. 3. There's also the `http3` argument, which, when enabled, makes the fetcher use HTTP/3 for requests, which makes your requests more authentic @@ -320,8 +320,7 @@ Some of the things it does: 3. It isolates JS execution, removes many Playwright fingerprints, and stops detection through some of the known behaviors that bots do. 4. It generates canvas noise to prevent fingerprinting through canvas. 5. It automatically patches known methods to detect running in headless mode and provides an option to defeat timezone mismatch attacks. -6. It makes requests look as if they came from Google's search page of the requested website. -7. and other anti-protection options... +6. and other anti-protection options... ```python >>> from scrapling.fetchers import StealthyFetcher From 66e0e7af576b2de767277161df6eebb51b83f09f Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:31:57 +0200 Subject: [PATCH 16/18] fix(agent): update the agent skill zip file with the new version --- agent-skill/Scrapling-Skill.zip | Bin 78387 -> 81634 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/agent-skill/Scrapling-Skill.zip b/agent-skill/Scrapling-Skill.zip index 2bcbbf84faf82548413de4638e82be024014d8be..0bbd64c8f80cba8803b3228803d58b9b8e0bdb74 100644 GIT binary patch delta 27442 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 N2LM>m!G;XL{{dtn$teH; From f80743a97bbd72b49bcd5edca4e4780bb8c2a4b5 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:35:06 +0200 Subject: [PATCH 17/18] style: remove unused import --- scrapling/engines/toolbelt/fingerprints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapling/engines/toolbelt/fingerprints.py b/scrapling/engines/toolbelt/fingerprints.py index 9a84199..3d256fa 100644 --- a/scrapling/engines/toolbelt/fingerprints.py +++ b/scrapling/engines/toolbelt/fingerprints.py @@ -8,7 +8,7 @@ from platform import system as platform_system from browserforge.headers import Browser, HeaderGenerator from browserforge.headers.generator import SUPPORTED_OPERATING_SYSTEMS -from scrapling.core._types import Dict, Literal, Tuple, cast +from scrapling.core._types import Dict, Literal, Tuple __OS_NAME__ = platform_system() OSName = Literal["linux", "macos", "windows"] From 6ec40ad19da84cef683ac5f45fed446bb0d38e99 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 9 Mar 2026 00:48:15 +0200 Subject: [PATCH 18/18] docs: remove a sponsor --- README.md | 1 - docs/README_AR.md | 1 - docs/README_CN.md | 1 - docs/README_DE.md | 1 - docs/README_ES.md | 1 - docs/README_FR.md | 1 - docs/README_JP.md | 1 - docs/README_RU.md | 1 - images/thordata.jpg | Bin 7269 -> 0 bytes 9 files changed, 8 deletions(-) delete mode 100644 images/thordata.jpg diff --git a/README.md b/README.md index 09e870c..fe3e4aa 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,6 @@ MySpider().start() - diff --git a/docs/README_AR.md b/docs/README_AR.md index f8fa194..70f1d96 100644 --- a/docs/README_AR.md +++ b/docs/README_AR.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_CN.md b/docs/README_CN.md index 6c4f5b3..4531269 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_DE.md b/docs/README_DE.md index f1a2d01..c75a5c4 100644 --- a/docs/README_DE.md +++ b/docs/README_DE.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_ES.md b/docs/README_ES.md index 574cc88..2835f70 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_FR.md b/docs/README_FR.md index 16cb96c..3b94cc5 100644 --- a/docs/README_FR.md +++ b/docs/README_FR.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_JP.md b/docs/README_JP.md index a2d8a1e..22a119c 100644 --- a/docs/README_JP.md +++ b/docs/README_JP.md @@ -140,7 +140,6 @@ MySpider().start() - diff --git a/docs/README_RU.md b/docs/README_RU.md index 26bf87f..68f4382 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -143,7 +143,6 @@ MySpider().start() - diff --git a/images/thordata.jpg b/images/thordata.jpg deleted file mode 100644 index 5f680a7d5591e25c966860b20e69a79f444db341..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7269 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 KDIn}-_J05sH110P