From 7178279586749a802337b2426b95218b13712775 Mon Sep 17 00:00:00 2001 From: haosenwang1018 <1293965075@qq.com> Date: Sun, 15 Mar 2026 17:27:55 +0800 Subject: [PATCH 01/11] test: add coverage for TextHandler regex, clean, and TextHandlers.re() Several critical code paths in custom_types.py lacked test coverage: - TextHandler.re(check_match=True): returns bool, not TextHandlers - TextHandler.re(replace_entities=False): entity preservation path - TextHandler.re() with capture groups: flatten behavior - TextHandler.re_first() default value when no match - TextHandler.clean(remove_entities=True): entity replacement path - TextHandler.json() valid and invalid input - TextHandlers.re(): list-level regex with result flattening - TextHandlers.extract()/get_all(): identity return Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/parser/test_parser_advanced.py | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/parser/test_parser_advanced.py b/tests/parser/test_parser_advanced.py index f34ba5c..969ccc3 100644 --- a/tests/parser/test_parser_advanced.py +++ b/tests/parser/test_parser_advanced.py @@ -250,6 +250,68 @@ class TestTextHandlerAdvanced: matches = text3.re(r"He l lo", clean_match=True, case_sensitive=False) assert len(matches) == 1 + def test_text_handler_regex_check_match(self): + """Test TextHandler.re() with check_match=True returns bool""" + text = TextHandler("Price: $10.99") + assert text.re(r"\$[\d.]+", check_match=True) is True + assert text.re(r"no-match-pattern", check_match=True) is False + + def test_text_handler_regex_replace_entities_false(self): + """Test TextHandler.re() with replace_entities=False preserves entities""" + text = TextHandler("Hello & World") + results = text.re(r"&", replace_entities=False) + assert len(results) == 1 + assert results[0] == "&" + + def test_text_handler_regex_with_groups(self): + """Test TextHandler.re() with capture groups flattens results""" + text = TextHandler("name=Alice age=30 name=Bob age=25") + results = text.re(r"name=(\w+) age=(\d+)") + assert len(results) == 4 + assert "Alice" in results + assert "30" in results + + def test_text_handler_re_first_with_default(self): + """Test TextHandler.re_first() returns default when no match""" + text = TextHandler("no numbers here") + result = text.re_first(r"\d+", default="N/A") + assert result == "N/A" + + def test_text_handler_re_first_returns_first_match(self): + """Test TextHandler.re_first() returns first match""" + text = TextHandler("a1 b2 c3") + result = text.re_first(r"\d") + assert result == "1" + assert isinstance(result, TextHandler) + + def test_text_handler_clean_with_entities(self): + """Test TextHandler.clean() with remove_entities=True""" + text = TextHandler("Hello\t&\nWorld") + cleaned = text.clean(remove_entities=True) + assert "&" not in cleaned + assert "&" in cleaned + assert "\t" not in cleaned + assert "\n" not in cleaned + + def test_text_handler_clean_without_entities(self): + """Test TextHandler.clean() preserves entities by default""" + text = TextHandler("Hello\t&\nWorld") + cleaned = text.clean(remove_entities=False) + assert "&" in cleaned + + def test_text_handler_json_valid(self): + """Test TextHandler.json() with valid JSON""" + text = TextHandler('{"key": "value", "num": 42}') + data = text.json() + assert data["key"] == "value" + assert data["num"] == 42 + + def test_text_handler_json_invalid(self): + """Test TextHandler.json() raises on invalid JSON""" + text = TextHandler("not json") + with pytest.raises(Exception): + text.json() + def test_text_handlers_operations(self): """Test TextHandlers list operations""" handlers = TextHandlers([ @@ -266,6 +328,37 @@ class TestTextHandlerAdvanced: assert handlers.get("default") == "First" assert TextHandlers([]).get("default") == "default" + def test_text_handlers_re(self): + """Test TextHandlers.re() flattens results across all elements""" + handlers = TextHandlers([ + TextHandler("a1 b2"), + TextHandler("c3 d4"), + ]) + results = handlers.re(r"[a-z]\d") + assert isinstance(results, TextHandlers) + assert len(results) == 4 + assert results[0] == "a1" + assert results[3] == "d4" + + def test_text_handlers_re_empty(self): + """Test TextHandlers.re() on empty list""" + handlers = TextHandlers([]) + results = handlers.re(r"\d+") + assert isinstance(results, TextHandlers) + assert len(results) == 0 + + def test_text_handlers_re_no_matches(self): + """Test TextHandlers.re() when no element matches""" + handlers = TextHandlers([TextHandler("abc"), TextHandler("def")]) + results = handlers.re(r"\d+") + assert len(results) == 0 + + def test_text_handlers_extract(self): + """Test TextHandlers.extract() returns self""" + handlers = TextHandlers([TextHandler("a"), TextHandler("b")]) + assert handlers.extract() is handlers + assert handlers.get_all() is handlers + class TestSelectorsAdvanced: """Test advanced Selectors functionality""" From a31763afdeaec2f4cfd4b402e81692cd301b4c0a Mon Sep 17 00:00:00 2001 From: haosenwang1018 <1293965075@qq.com> Date: Sun, 15 Mar 2026 17:52:57 +0800 Subject: [PATCH 02/11] fix: replace bare raise with return False in _restore_from_checkpoint When _checkpoint_system_enabled is False, the method uses a bare `raise` with no active exception, which causes RuntimeError at runtime. The method's docstring says it returns False when restoration is not possible, so return False is the correct behavior. The caller in crawl() currently guards with `if self._checkpoint_system_enabled`, but the method's own contract should be self-consistent. Co-Authored-By: Claude Opus 4.6 (1M context) --- scrapling/spiders/engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapling/spiders/engine.py b/scrapling/spiders/engine.py index 416911d..d77f838 100644 --- a/scrapling/spiders/engine.py +++ b/scrapling/spiders/engine.py @@ -205,7 +205,7 @@ class CrawlerEngine: Returns True if successfully restored, False otherwise. """ if not self._checkpoint_system_enabled: - raise + return False data = await self._checkpoint_manager.load() if data is None: From d3c251c1ab3a935a14f1d3333a3643505cc933c4 Mon Sep 17 00:00:00 2001 From: haosenwang1018 <1293965075@qq.com> Date: Sun, 15 Mar 2026 17:53:23 +0800 Subject: [PATCH 03/11] fix: add max retry limit to _get_page_content to prevent infinite loop Both _get_page_content and _get_async_page_content use a while-True loop that retries page.content() on PlaywrightError with no upper bound. If the page is in a permanently broken state (crashed tab, closed context), this loops forever and hangs the process. Replace with a bounded for-loop (default 10 retries = 5s), returning an empty string if all attempts fail. This preserves the existing retry behavior for the transient Windows issue (playwright#16108) while preventing hangs. Co-Authored-By: Claude Opus 4.6 (1M context) --- scrapling/engines/toolbelt/convertor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scrapling/engines/toolbelt/convertor.py b/scrapling/engines/toolbelt/convertor.py index 1a8d799..1860153 100644 --- a/scrapling/engines/toolbelt/convertor.py +++ b/scrapling/engines/toolbelt/convertor.py @@ -187,34 +187,34 @@ class ResponseFactory: return history @classmethod - def _get_page_content(cls, page: SyncPage) -> str: + def _get_page_content(cls, page: SyncPage, max_retries: int = 10) -> str: """ A workaround for the Playwright issue with `page.content()` on Windows. Ref.: https://github.com/microsoft/playwright/issues/16108 :param page: The page to extract content from. + :param max_retries: Maximum number of retry attempts before returning empty string. :return: """ - while True: + for _ in range(max_retries): try: return page.content() or "" except PlaywrightError: page.wait_for_timeout(500) - continue - return "" # pyright: ignore + return "" @classmethod - async def _get_async_page_content(cls, page: AsyncPage) -> str: + async def _get_async_page_content(cls, page: AsyncPage, max_retries: int = 10) -> str: """ A workaround for the Playwright issue with `page.content()` on Windows. Ref.: https://github.com/microsoft/playwright/issues/16108 :param page: The page to extract content from. + :param max_retries: Maximum number of retry attempts before returning empty string. :return: """ - while True: + for _ in range(max_retries): try: return (await page.content()) or "" except PlaywrightError: await page.wait_for_timeout(500) - continue - return "" # pyright: ignore + return "" @classmethod async def from_async_playwright_response( From cec5acd68a3c73791342f3c3e08d4f7bd58c7b06 Mon Sep 17 00:00:00 2001 From: awanawona <> Date: Mon, 16 Mar 2026 18:01:11 +0800 Subject: [PATCH 04/11] test: add edge case tests for filter, iterancestors, and find_similar - test_selectors_filter.py: covers chained filter(), empty result, all-pass predicate, and calling filter() on empty Selectors - test_ancestor_navigation.py: covers iterancestors() order, depth, text-node safety, root-element edge case, and find_ancestor() with no match - test_find_similar_advanced.py: covers similarity_threshold levels, match_text=True behavior, ignore_attributes combinations, and text-node safety --- tests/parser/test_ancestor_navigation.py | 66 ++++++++++++++++++ tests/parser/test_find_similar_advanced.py | 78 ++++++++++++++++++++++ tests/parser/test_selectors_filter.py | 64 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 tests/parser/test_ancestor_navigation.py create mode 100644 tests/parser/test_find_similar_advanced.py create mode 100644 tests/parser/test_selectors_filter.py diff --git a/tests/parser/test_ancestor_navigation.py b/tests/parser/test_ancestor_navigation.py new file mode 100644 index 0000000..1814ec5 --- /dev/null +++ b/tests/parser/test_ancestor_navigation.py @@ -0,0 +1,66 @@ +""" +Tests for Selector.iterancestors() and Selector.find_ancestor() methods. +Target file: tests/parser/test_general.py (append to TestElementNavigation class) +""" +import pytest +from scrapling import Selector + + +@pytest.fixture +def nested_page(): + html = """ + +
+
+
+

deep text

+
+
+
+ + """ + return Selector(html, adaptive=False) + + +class TestAncestorNavigation: + def test_iterancestors_returns_all_ancestors(self, nested_page): + """iterancestors() should yield every ancestor up to """ + target = nested_page.css("#target")[0] + ancestor_tags = [a.tag for a in target.iterancestors()] + # Expected order: p → article → section → div → body → html + assert ancestor_tags[:4] == ["p", "article", "section", "div"] + assert "body" in ancestor_tags + assert "html" in ancestor_tags + + def test_iterancestors_order_is_bottom_up(self, nested_page): + """iterancestors() should start from the immediate parent, not the root""" + target = nested_page.css("#target")[0] + first_ancestor = next(target.iterancestors()) + assert first_ancestor.attrib.get("id") == "level4" + + def test_find_ancestor_returns_first_match(self, nested_page): + """find_ancestor() should return the closest ancestor matching the predicate""" + target = nested_page.css("#target")[0] + # Looking for the nearest ancestor with class "card" + result = target.find_ancestor(lambda el: el.has_class("card")) + assert result is not None + assert result.attrib.get("id") == "level3" + + def test_find_ancestor_returns_none_when_not_found(self, nested_page): + """find_ancestor() should return None if no ancestor matches""" + target = nested_page.css("#target")[0] + result = target.find_ancestor(lambda el: el.has_class("nonexistent-class")) + assert result is None + + def test_iterancestors_on_text_node_is_empty(self, nested_page): + """iterancestors() on a text node should yield nothing (not raise)""" + text_node = nested_page.css("#target::text")[0] + ancestors = list(text_node.iterancestors()) + assert ancestors == [] + + def test_find_ancestor_on_root_element_returns_none(self, nested_page): + """find_ancestor() on the root element should return None gracefully""" + # html element has no ancestors + html_el = nested_page.css("html")[0] + result = html_el.find_ancestor(lambda el: True) + assert result is None diff --git a/tests/parser/test_find_similar_advanced.py b/tests/parser/test_find_similar_advanced.py new file mode 100644 index 0000000..480f155 --- /dev/null +++ b/tests/parser/test_find_similar_advanced.py @@ -0,0 +1,78 @@ +""" +Tests for Selector.find_similar() with non-default parameters. +Target file: tests/parser/test_general.py (append to TestSimilarElements class) +""" +import pytest +from scrapling import Selector + + +@pytest.fixture +def product_page(): + html = """ + +
+
+ Apple +
+
+ Banana +
+
+ Carrot +
+ +
+ Grape +
+
+ + """ + return Selector(html, adaptive=False) + + +class TestFindSimilarAdvanced: + def test_find_similar_default_finds_same_tag_siblings(self, product_page): + """find_similar() with defaults should find div.product siblings, not the section""" + first = product_page.css("div.product")[0] + similar = first.find_similar() + tags = [el.tag for el in similar] + assert all(t == "div" for t in tags), "Should only return
elements" + assert len(similar) == 2 # Banana and Carrot, not Grape (section) + + def test_find_similar_high_threshold_filters_more(self, product_page): + """A higher similarity_threshold should return fewer (or equal) results""" + first = product_page.css("div.product")[0] + low_threshold = first.find_similar(similarity_threshold=0.1) + high_threshold = first.find_similar(similarity_threshold=0.9) + assert len(high_threshold) <= len(low_threshold) + + def test_find_similar_match_text_excludes_different_text(self, product_page): + """match_text=True should factor in text content during similarity scoring""" + first = product_page.css("div.product")[0] # Apple + # With match_text=True and a high threshold, "Apple" vs "Banana"/"Carrot" text + # should reduce similarity scores — result count may drop + with_text = first.find_similar(similarity_threshold=0.8, match_text=True) + without_text = first.find_similar(similarity_threshold=0.8, match_text=False) + # match_text=True is stricter when text differs, so result should be <= without_text + assert len(with_text) <= len(without_text) + + def test_find_similar_ignore_attributes_affects_matching(self, product_page): + """Ignoring data-price should make more elements qualify as similar""" + first = product_page.css("div.product")[0] + # Ignore both data-price and data-category → only class matters → all 3 divs match + ignore_all_data = first.find_similar( + similarity_threshold=0.2, + ignore_attributes=["data-price", "data-category"] + ) + # Ignore nothing → data-category difference (fruit vs veggie) may reduce matches + ignore_nothing = first.find_similar( + similarity_threshold=0.9, + ignore_attributes=[] + ) + assert len(ignore_all_data) >= len(ignore_nothing) + + def test_find_similar_on_text_node_returns_empty(self, product_page): + """find_similar() on a text node should return empty Selectors without raising""" + text_node = product_page.css(".name::text")[0] + result = text_node.find_similar() + assert len(result) == 0 diff --git a/tests/parser/test_selectors_filter.py b/tests/parser/test_selectors_filter.py new file mode 100644 index 0000000..9612abc --- /dev/null +++ b/tests/parser/test_selectors_filter.py @@ -0,0 +1,64 @@ +""" +Tests for Selectors.filter() method edge cases. +Target file: tests/parser/test_parser_advanced.py (append to TestAdvancedSelectors class) +""" +import pytest +from scrapling import Selector, Selectors + + +@pytest.fixture +def page(): + html = """ + +
    +
  • Apple
  • +
  • Banana
  • +
  • Cherry
  • +
  • Durian
  • +
+ + """ + return Selector(html, adaptive=False) + + +class TestSelectorsFilter: + def test_filter_basic(self, page): + """filter() should return only elements matching the predicate""" + items = page.css("li.item") + expensive = items.filter(lambda el: int(el.attrib.get("data-value", 0)) >= 10) + assert len(expensive) == 2 + texts = expensive.getall() + assert any("Apple" in t for t in texts) + assert any("Cherry" in t for t in texts) + + def test_filter_returns_empty_selectors_when_no_match(self, page): + """filter() should return an empty Selectors (not None/exception) when nothing matches""" + items = page.css("li.item") + result = items.filter(lambda el: int(el.attrib.get("data-value", 0)) > 9999) + assert isinstance(result, Selectors) + assert len(result) == 0 + assert result.first is None + + def test_filter_all_pass(self, page): + """filter() with always-True predicate should return all elements""" + items = page.css("li.item") + result = items.filter(lambda el: True) + assert len(result) == len(items) + + def test_filter_chained(self, page): + """filter() should be chainable — apply two filters in sequence""" + items = page.css("li.item") + # First: value > 0, then: not disabled + result = ( + items + .filter(lambda el: int(el.attrib.get("data-value", 0)) > 0) + .filter(lambda el: not el.has_class("disabled")) + ) + assert len(result) == 3 # Apple, Banana, Cherry (Durian is disabled AND value=0) + + def test_filter_on_empty_selectors(self): + """filter() on an already-empty Selectors should not raise""" + empty = Selectors() + result = empty.filter(lambda el: True) + assert isinstance(result, Selectors) + assert len(result) == 0 From 5bf921b3087f900cc765202b7132918490f24a46 Mon Sep 17 00:00:00 2001 From: karesansui Date: Tue, 17 Mar 2026 00:53:52 +0900 Subject: [PATCH 05/11] fix: preserve HTTP method across retries in spider session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionManager.fetch() pops `method` from `_session_kwargs`, which mutates the original request dict. When the engine retries a blocked request via request.copy(), the copy no longer has `method`, so it defaults to GET. Steps to reproduce: 1. Yield Request(url, method="POST", data=...) 2. Target returns a response that triggers is_blocked() 3. Engine retries via request.copy() → second fetch uses GET Fix: copy the kwargs dict before popping, so the original request stays intact. --- scrapling/spiders/session.py | 6 ++-- tests/spiders/test_session.py | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/scrapling/spiders/session.py b/scrapling/spiders/session.py index cc07042..536be6d 100644 --- a/scrapling/spiders/session.py +++ b/scrapling/spiders/session.py @@ -112,10 +112,12 @@ class SessionManager: client = session._client if isinstance(client, _ASyncSessionLogic): + kwargs = request._session_kwargs.copy() + method = cast(SUPPORTED_HTTP_METHODS, kwargs.pop("method", "GET")) response = await client._make_request( - method=cast(SUPPORTED_HTTP_METHODS, request._session_kwargs.pop("method", "GET")), + method=method, url=request.url, - **request._session_kwargs, + **kwargs, ) else: # Sync session or other types - shouldn't happen in async context diff --git a/tests/spiders/test_session.py b/tests/spiders/test_session.py index c1eed5d..0c422ef 100644 --- a/tests/spiders/test_session.py +++ b/tests/spiders/test_session.py @@ -1,9 +1,12 @@ """Tests for the SessionManager class.""" +from unittest.mock import AsyncMock, PropertyMock + from scrapling.core._types import Any import pytest from scrapling.spiders.session import SessionManager +from scrapling.spiders.request import Request class MockSession: # type: ignore[type-arg] @@ -350,3 +353,57 @@ class TestSessionManagerIntegration: # After close - all inactive await manager.close() assert all(not s._is_alive for s in sessions) + + +class TestSessionManagerFetch: + """Test SessionManager fetch behavior.""" + + @pytest.mark.asyncio + async def test_fetch_preserves_request_method(self): + """Test that fetch does not mutate request._session_kwargs. + + Previously, fetch() used pop("method") which removed the method + key from the original request dict. This caused retried requests + (via request.copy()) to lose their HTTP method and fall back to GET. + """ + from scrapling.engines.static import _ASyncSessionLogic + from scrapling.fetchers import FetcherSession + from scrapling.engines.toolbelt.custom import Response + + mock_response = Response( + url="https://example.com", + content=b"ok", + status=200, + reason="OK", + cookies={}, + headers={"content-type": "text/html"}, + request_headers={}, + ) + mock_response.meta = {} + + mock_client = AsyncMock(spec=_ASyncSessionLogic) + mock_client._make_request = AsyncMock(return_value=mock_response) + + mock_session = AsyncMock(spec=FetcherSession) + mock_session._client = mock_client + mock_session._is_alive = True + + manager = SessionManager() + manager._sessions["default"] = mock_session + manager._default_session_id = "default" + manager._started = True + + request = Request("https://example.com", method="POST", data={"key": "value"}) + + assert request._session_kwargs["method"] == "POST" + + await manager.fetch(request) + + # method must still be present after fetch + assert "method" in request._session_kwargs + assert request._session_kwargs["method"] == "POST" + + # verify the correct method was passed to _make_request + mock_client._make_request.assert_called_once() + call_kwargs = mock_client._make_request.call_args + assert call_kwargs.kwargs["method"] == "POST" From cc6c0dbfb91c75b014634f4a3a70eb8bbb337c8b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 17 Mar 2026 17:46:47 +0200 Subject: [PATCH 06/11] fix: adjust test to the `_restore_from_checkpoint` fix --- tests/spiders/test_engine.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/spiders/test_engine.py b/tests/spiders/test_engine.py index ca768bf..b7bfd0f 100644 --- a/tests/spiders/test_engine.py +++ b/tests/spiders/test_engine.py @@ -613,8 +613,7 @@ class TestCheckpointMethods: @pytest.mark.asyncio async def test_restore_from_checkpoint_raises_when_disabled(self): engine = _make_engine() # no crawldir → checkpoint disabled - with pytest.raises(RuntimeError): - await engine._restore_from_checkpoint() + assert (await engine._restore_from_checkpoint()) is False # --------------------------------------------------------------------------- From 136c3897879634db2228afe14632a1e912d838ec Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 17 Mar 2026 21:53:17 +0200 Subject: [PATCH 07/11] fix(Texthandler): Replace `get_all` with `getall` to match the `Selector` class --- scrapling/core/custom_types.py | 6 +++--- tests/parser/test_parser_advanced.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scrapling/core/custom_types.py b/scrapling/core/custom_types.py index d06ac29..53255be 100644 --- a/scrapling/core/custom_types.py +++ b/scrapling/core/custom_types.py @@ -112,10 +112,10 @@ class TextHandler(str): def get(self, default=None): # pragma: no cover return self - def get_all(self): # pragma: no cover + def getall(self): # pragma: no cover return self - extract = get_all + extract = getall extract_first = get def json(self) -> Dict: @@ -279,7 +279,7 @@ class TextHandlers(List[TextHandler]): return self extract_first = get - get_all = extract + getall = extract class AttributesHandler(Mapping[str, _TextHandlerType]): diff --git a/tests/parser/test_parser_advanced.py b/tests/parser/test_parser_advanced.py index 969ccc3..39829bb 100644 --- a/tests/parser/test_parser_advanced.py +++ b/tests/parser/test_parser_advanced.py @@ -357,7 +357,7 @@ class TestTextHandlerAdvanced: """Test TextHandlers.extract() returns self""" handlers = TextHandlers([TextHandler("a"), TextHandler("b")]) assert handlers.extract() is handlers - assert handlers.get_all() is handlers + assert handlers.getall() is handlers class TestSelectorsAdvanced: From 1dc0b7a1bd553d005bf7eeee546da105d9f4baf6 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 17 Mar 2026 22:14:02 +0200 Subject: [PATCH 08/11] fix(fetchers/content): increase the default max number of retries and raise error on max retries Ref.: https://github.com/D4Vinci/Scrapling/pull/197#issuecomment-4077705587 --- scrapling/engines/toolbelt/convertor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scrapling/engines/toolbelt/convertor.py b/scrapling/engines/toolbelt/convertor.py index 1860153..8606003 100644 --- a/scrapling/engines/toolbelt/convertor.py +++ b/scrapling/engines/toolbelt/convertor.py @@ -187,11 +187,11 @@ class ResponseFactory: return history @classmethod - def _get_page_content(cls, page: SyncPage, max_retries: int = 10) -> str: + def _get_page_content(cls, page: SyncPage, max_retries: int = 20) -> str: """ A workaround for the Playwright issue with `page.content()` on Windows. Ref.: https://github.com/microsoft/playwright/issues/16108 :param page: The page to extract content from. - :param max_retries: Maximum number of retry attempts before returning empty string. + :param max_retries: Maximum number of retry attempts before raising `RuntimeError`. :return: """ for _ in range(max_retries): @@ -199,14 +199,14 @@ class ResponseFactory: return page.content() or "" except PlaywrightError: page.wait_for_timeout(500) - return "" + raise RuntimeError(f"Failed to retrieve the page content after retrying for {max_retries * 500}ms.") @classmethod - async def _get_async_page_content(cls, page: AsyncPage, max_retries: int = 10) -> str: + async def _get_async_page_content(cls, page: AsyncPage, max_retries: int = 20) -> str: """ A workaround for the Playwright issue with `page.content()` on Windows. Ref.: https://github.com/microsoft/playwright/issues/16108 :param page: The page to extract content from. - :param max_retries: Maximum number of retry attempts before returning empty string. + :param max_retries: Maximum number of retry attempts before raising `RuntimeError`. :return: """ for _ in range(max_retries): @@ -214,7 +214,7 @@ class ResponseFactory: return (await page.content()) or "" except PlaywrightError: await page.wait_for_timeout(500) - return "" + raise RuntimeError(f"Failed to retrieve the page content after retrying for {max_retries * 500}ms.") @classmethod async def from_async_playwright_response( From a4f31ae20553c8bd53d0be5126d8c4b14a811390 Mon Sep 17 00:00:00 2001 From: yetval Date: Thu, 19 Mar 2026 10:37:02 -0400 Subject: [PATCH 09/11] simple md fix --- .../Scrapling-Skill/references/fetching/dynamic.md | 2 +- .../Scrapling-Skill/references/spiders/architecture.md | 10 +++++----- .../references/spiders/getting-started.md | 2 +- .../Scrapling-Skill/references/spiders/sessions.md | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md index a87fdf6..1a4c96d 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md +++ b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md @@ -44,7 +44,7 @@ Instead of launching a browser locally (Chromium/Google Chrome), you can connect **Notes:** * There was a `stealth` option here, but it was moved to the `StealthyFetcher` class, as explained on the next page, with additional features since version 0.3.13. -* This makes it less confusing for new users, easier to maintain, and provides other benefits, as explained on the [StealthyFetcher page](fetching/stealthy.md). +* This makes it less confusing for new users, easier to maintain, and provides other benefits, as explained on the [StealthyFetcher page](stealthy.md). ## Full list of arguments All arguments for `DynamicFetcher` and its session classes: diff --git a/agent-skill/Scrapling-Skill/references/spiders/architecture.md b/agent-skill/Scrapling-Skill/references/spiders/architecture.md index 9e7586f..de95d24 100644 --- a/agent-skill/Scrapling-Skill/references/spiders/architecture.md +++ b/agent-skill/Scrapling-Skill/references/spiders/architecture.md @@ -11,8 +11,8 @@ Here's what happens step by step when you run a spider: 1. The **Spider** produces the first batch of `Request` objects. By default, it creates one request for each URL in `start_urls`, but you can override `start_requests()` for custom logic. 2. The **Scheduler** receives requests and places them in a priority queue, and creates fingerprints for them. Higher-priority requests are dequeued first. 3. The **Crawler Engine** asks the **Scheduler** to dequeue the next request, respecting concurrency limits (global and per-domain) and download delays. Once the **Crawler Engine** receives the request, it passes it to the **Session Manager**, which routes it to the correct session based on the request's `sid` (session ID). -4. The **session** fetches the page and returns a [Response](fetching/choosing.md#response-object) object to the **Crawler Engine**. The engine records statistics and checks for blocked responses. If the response is blocked, the engine retries the request up to `max_blocked_retries` times. Of course, the blocking detection and the retry logic for blocked requests can be customized. -5. The **Crawler Engine** passes the [Response](fetching/choosing.md#response-object) to the request's callback. The callback either yields a dictionary, which gets treated as a scraped item, or a follow-up request, which gets sent to the scheduler for queuing. +4. The **session** fetches the page and returns a [Response](../fetching/choosing.md#response-object) object to the **Crawler Engine**. The engine records statistics and checks for blocked responses. If the response is blocked, the engine retries the request up to `max_blocked_retries` times. Of course, the blocking detection and the retry logic for blocked requests can be customized. +5. The **Crawler Engine** passes the [Response](../fetching/choosing.md#response-object) to the request's callback. The callback either yields a dictionary, which gets treated as a scraped item, or a follow-up request, which gets sent to the scheduler for queuing. 6. The cycle repeats from step 2 until the scheduler is empty and no tasks are active, or the spider is paused. 7. If `crawldir` is set while starting the spider, the **Crawler Engine** periodically saves a checkpoint (pending requests + seen URLs set) to disk. On graceful shutdown (Ctrl+C), a final checkpoint is saved. The next time the spider runs with the same `crawldir`, it resumes from where it left off — skipping `start_requests()` and restoring the scheduler state. @@ -50,9 +50,9 @@ A priority queue with built-in URL deduplication. Requests are fingerprinted bas Manages one or more named session instances. Each session is one of: -- [FetcherSession](fetching/static.md) -- [AsyncDynamicSession](fetching/dynamic.md) -- [AsyncStealthySession](fetching/stealthy.md) +- [FetcherSession](../fetching/static.md) +- [AsyncDynamicSession](../fetching/dynamic.md) +- [AsyncStealthySession](../fetching/stealthy.md) When a request comes in, the Session Manager routes it to the correct session based on the request's `sid` field. Sessions can be started with the spider start (default) or lazily (started on the first use). diff --git a/agent-skill/Scrapling-Skill/references/spiders/getting-started.md b/agent-skill/Scrapling-Skill/references/spiders/getting-started.md index 1446779..2a9e6c8 100644 --- a/agent-skill/Scrapling-Skill/references/spiders/getting-started.md +++ b/agent-skill/Scrapling-Skill/references/spiders/getting-started.md @@ -25,7 +25,7 @@ Every spider needs three things: 2. **`start_urls`** — A list of URLs to start crawling from. 3. **`parse()`** — An async generator method that processes each response and yields results. -The `parse()` method processes each response. You use the same selection methods you'd use with Scrapling's [Selector](parsing/main_classes.md#selector)/[Response](fetching/choosing.md#response-object), and `yield` dictionaries to output scraped items. +The `parse()` method processes each response. You use the same selection methods you'd use with Scrapling's [Selector](../parsing/main_classes.md#selector)/[Response](../fetching/choosing.md#response-object), and `yield` dictionaries to output scraped items. ## Running the Spider diff --git a/agent-skill/Scrapling-Skill/references/spiders/sessions.md b/agent-skill/Scrapling-Skill/references/spiders/sessions.md index 82a21ac..761cea6 100644 --- a/agent-skill/Scrapling-Skill/references/spiders/sessions.md +++ b/agent-skill/Scrapling-Skill/references/spiders/sessions.md @@ -6,14 +6,14 @@ A spider can use multiple fetcher sessions simultaneously — for example, a fas A session is a pre-configured fetcher instance that stays alive for the duration of the crawl. Instead of creating a new connection or browser for every request, the spider reuses sessions, which is faster and more resource-efficient. -By default, every spider creates a single [FetcherSession](fetching/static.md). You can add more sessions or swap the default by overriding the `configure_sessions()` method, but you have to use the async version of each session only, as the table shows below: +By default, every spider creates a single [FetcherSession](../fetching/static.md). You can add more sessions or swap the default by overriding the `configure_sessions()` method, but you have to use the async version of each session only, as the table shows below: | Session Type | Use Case | |-------------------------------------------------|------------------------------------------| -| [FetcherSession](fetching/static.md) | Fast HTTP requests, no JavaScript | -| [AsyncDynamicSession](fetching/dynamic.md) | Browser automation, JavaScript rendering | -| [AsyncStealthySession](fetching/stealthy.md) | Anti-bot bypass, Cloudflare, etc. | +| [FetcherSession](../fetching/static.md) | Fast HTTP requests, no JavaScript | +| [AsyncDynamicSession](../fetching/dynamic.md) | Browser automation, JavaScript rendering | +| [AsyncStealthySession](../fetching/stealthy.md) | Anti-bot bypass, Cloudflare, etc. | ## Configuring Sessions From 3ed59c2a849567c1780b94a45c62936f1797e6d4 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Thu, 19 Mar 2026 17:01:05 +0200 Subject: [PATCH 10/11] fix(agent): update skill zip file with the latest fix --- agent-skill/Scrapling-Skill.zip | Bin 81634 -> 81637 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/agent-skill/Scrapling-Skill.zip b/agent-skill/Scrapling-Skill.zip index 0bbd64c8f80cba8803b3228803d58b9b8e0bdb74..fb76762e2e6687e3033bbc9e56a5483d6a14ff92 100644 GIT binary patch delta 14311 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

} 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 Date: Sun, 22 Mar 2026 16:48:26 +0200 Subject: [PATCH 11/11] docs: removing an old 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_KR.md | 1 - docs/README_RU.md | 1 - images/rapidproxy.jpg | Bin 4611 -> 0 bytes 10 files changed, 9 deletions(-) delete mode 100644 images/rapidproxy.jpg diff --git a/README.md b/README.md index dfb7d7f..ea56b15 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,6 @@ MySpider().start() - diff --git a/docs/README_AR.md b/docs/README_AR.md index 7573a10..68c6a51 100644 --- a/docs/README_AR.md +++ b/docs/README_AR.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_CN.md b/docs/README_CN.md index be91908..469b703 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_DE.md b/docs/README_DE.md index 88ebd0a..d5c30b9 100644 --- a/docs/README_DE.md +++ b/docs/README_DE.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_ES.md b/docs/README_ES.md index 959bc05..d32cb69 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_FR.md b/docs/README_FR.md index 9d4212a..09180d6 100644 --- a/docs/README_FR.md +++ b/docs/README_FR.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_JP.md b/docs/README_JP.md index dde6f5b..4b6a0c0 100644 --- a/docs/README_JP.md +++ b/docs/README_JP.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_KR.md b/docs/README_KR.md index dad94ae..5b29439 100644 --- a/docs/README_KR.md +++ b/docs/README_KR.md @@ -180,7 +180,6 @@ MySpider().start() - diff --git a/docs/README_RU.md b/docs/README_RU.md index 99b347a..6a97f17 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -183,7 +183,6 @@ MySpider().start() - diff --git a/images/rapidproxy.jpg b/images/rapidproxy.jpg deleted file mode 100644 index dee0b35bd0b39f7629c4650983f4119a00d3ccc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4611 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