604 Commits

Author SHA1 Message Date
Karim shoair 74c5848060 fix: apply the session-level proxy when no per-request proxy is given
The per-request proxy resolution never fell back to the session default, so FetcherSession(proxy=...) was silently ignored, and requests went direct. Same fix in the sync and async paths, with regression tests asserting on the proxy that reaches curl_cffi.

Closes #295
2026-06-07 18:35:18 +03:00
Karim shoair 9352ccec3f Merge branch 'dev' into fix/quoted-charset-encoding 2026-06-07 17:58:06 +03:00
Karim shoair 4ced6e4ac2 build: pump up version, browsers and deps 2026-06-07 16:16:23 +03:00
Karim shoair 3d96baf284 Merge branch 'dev' into fix/adaptive-autosave-indexerror 2026-06-07 15:53:47 +03:00
Karim shoair 9c0c857245 fix(parser): use max of both attribute counts in similarity scoring denominator
Candidates with fewer attributes than the original got inflated scores because the denominator counted candidate attributes only, while the extra-attributes penalty direction worked as intended. Using `max()` on both counts fixes the inflation while keeping the penalty.

Closes #322
2026-06-07 15:51:10 +03:00
Karim shoair 4f0a593b7d Merge branch 'dev' into fix/atomic-checkpoint-cache-writes 2026-06-07 15:49:01 +03:00
Ahmed Elshahat f0db3d7d14 fix(spiders): use os.replace for atomic checkpoint/cache writes on Windows
CheckpointManager.save() and ResponseCacheManager.put() write to a temp
file and then move it into place with Path.rename(). On Windows, os.rename
cannot overwrite an existing destination and raises FileExistsError
(WinError 183), so every write after the first one fails: checkpoint
saving raises and breaks resume, while the development response cache
swallows the error and keeps returning the stale entry.

Path.replace() (os.replace) overwrites the destination atomically on every
platform and behaves identically to rename() on POSIX, so this is a no-op
on Linux and macOS and only fixes the broken overwrite on Windows.

Add a regression test for the cache overwrite path; the checkpoint
overwrite is already covered by test_multiple_saves_overwrite.
2026-06-07 04:24:12 +03:00
Mubashir Rahim cd4cdc69e6 fix: prevent IndexError in adaptive relocation with auto_save
When `css()`/`xpath()` are called with both `adaptive=True` and
`auto_save=True`, the relocation branch guarded the re-save with
`if elements is not None`. However `relocate()` returns an empty
list (never `None`) when no candidate clears the `percentage`
threshold, so the guard always passed and `self.save(elements[0], ...)`
raised `IndexError: list index out of range`.

This crashes exactly when adaptive resilience is needed most: the page
structure changed enough that nothing matches above the threshold.

Fix: use a truthiness check (`if elements and auto_save`) so the
re-save is skipped when relocation yields nothing. The successful
relocation path (which re-saves the relocated element) is unchanged.

Added a regression test that fails before the fix (IndexError) and
passes after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:53:10 +05:00
Bortlesboat 6390c0af3d fix: parse quoted charset values in content-type headers
`ResponseFactory.__extract_browser_encoding` matched the charset with
`charset=([\w-]+)`, which stops at a quote character. RFC 7231 permits the
charset value to be a quoted-string (e.g. `content-type: text/html;
charset="ISO-8859-1"`), so for any quoted charset the regex failed to match
and the function silently fell back to the `utf-8` default. A page served as
quoted ISO-8859-1 / windows-1252 / Shift_JIS would then be decoded as UTF-8,
producing mojibake.

Allow an optional surrounding quote in the pattern (`charset=["']?([\w-]+)`)
so the value is captured without the quote. Unquoted headers are unaffected.

The existing `content_type_map` fixture in tests/fetchers/test_utils.py was
unused; add focused tests covering unquoted, quoted, and missing charsets.
2026-06-01 21:30:16 -04:00
ETM-Code c243c8aca5 feat: add --version flag to the CLI
- Add a `--version` flag to the main CLI group using click's `version_option`
- Prints `Scrapling, version <version>` and exits, sourcing the version from `scrapling.__version__`
- Add a CLI test asserting the flag's output

Closes #299
2026-05-30 11:37:22 +01:00
Karim shoair ebd7e0971c build: update deps and browser useragents 2026-05-11 03:18:26 +03:00
Karim shoair f7da15771b feat(spiders): Add SitemapSpider 2026-05-11 02:33:37 +03:00
Karim shoair f093d0cf12 feat(spiders): Add CrawlSpider and CrawlRule 2026-05-10 21:13:16 +03:00
Karim shoair 18e9121135 feat(spiders): Add pure URL discovery primitive 2026-05-10 21:12:36 +03:00
Karim shoair 333b6de0b5 fix(parser): change the default threshold and add warning 2026-05-02 19:58:53 +03:00
yetval 334b0f5538 fix: str(value) 2026-04-30 09:52:34 -04:00
yetval b17363502d fix: hash request kwargs and headers correctly 2026-04-25 22:13:38 -04:00
Karim shoair b6f0f2a7f2 build: pump up version and deps 2026-04-22 17:01:11 +02:00
Karim shoair 35032d6f5f fix: solving a bug with using configure on Fetcher 2026-04-22 15:31:18 +02:00
Karim shoair 78e388f75c feat: add new mcp tool to screenshot pages
Implements #244
2026-04-17 22:07:44 +02:00
Karim shoair 65421f0a16 Merge branch 'dev' into fix/block-ads-unexpected-kwarg 2026-04-17 20:45:00 +02:00
voidborne-d 76ba28efaa fix(static): exclude block_ads from HTTP request args
block_ads is a browser-engine parameter (used by PlayWright/Camoufox
fetchers for ad-domain blocking) and is not recognised by curl_cffi's
Session.request(). When the CLI's --ai-targeted flag sets block_ads=True,
_merge_request_args forwards it unfiltered, causing:

  TypeError: Session.request() got an unexpected keyword argument 'block_ads'

Add block_ads to the skip_keys set so it is stripped before the dict
reaches Session.request(), consistent with existing entries for
extra_headers and google_search.

Fixes #247
2026-04-16 13:10:59 +00:00
Yuval Dinodia 62b95a047d Merge branch 'dev' into fix/fetcher-session-state-corruption 2026-04-15 21:51:26 -04:00
yetval f4186ab998 fix: prevent FetcherSession state corruption and lazy session close crash 2026-04-15 21:48:24 -04:00
Karim shoair 614d136f8c build: pump up version and deps 2026-04-15 20:44:38 +02:00
Jules Omlor 00897dad2f feat(mcp): add optional session_id parameter to open_session
Allow users to specify a custom session_id when opening a browser session,
rather than always generating a random UUID. Useful for naming sessions
for easier management across multiple tool calls.

- Add session_id: Optional[str] = None parameter
- Validate session_id doesn't already exist before starting browser
- Fall back to uuid4().hex[:12] if not provided
- Add tests for custom session_id and duplicate detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 01:17:52 -04:00
Karim shoair 3c23436448 Merge branch 'dev' into fix/full-path-selector-duplicate-id 2026-04-13 12:03:15 +02:00
Karim shoair 5e13d3ece6 feat(browsers): add a pre-navigation hook to allow page setup
Solves #238
2026-04-13 03:57:27 +02:00
Karim shoair d19e861e45 fix: correct Seconds type alias to accept float values
The Seconds type was defined as `Annotated[int, float, Meta(ge=0)]` which per PEP 593 treated float as metadata, not a type. This caused passing float values like wait=1.5 to be rejected. Fixed by using Annotated[float, Meta(ge=0)] since int is a subtype of float.

Correct fix for #240

Co-Authored-By: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
2026-04-13 01:08:53 +02:00
Karim shoair 887eeee4c2 build: pump up version and deps 2026-04-12 18:47:13 +02:00
Karim shoair ad6fd52845 perf: force ad blocking on the MCP server and when the AI mode is activated on CLI 2026-04-12 18:11:02 +02:00
Karim shoair be28fe16ec feat(browsers): add new feature to enable DNS-over-HTTP to prevent DNS leaks 2026-04-12 18:03:03 +02:00
Karim shoair 0678ed1406 fix(shell): add missing parameters to the shell signature 2026-04-12 18:00:41 +02:00
Karim shoair d952db8ef8 feat(browsers): add a new feature to block ads
This is working by aborting all requests to known ads domains.
2026-04-12 17:59:00 +02:00
sjhddh 273c8c2fa0 fix: emit valid XPath node test for ID elements in full-path mode
Address review feedback: In full-path XPath generation, elements with
IDs were producing bare predicates like `[@id='x']` which creates
invalid XPath steps like `//body/[@id='main']`. Now emits `*[@id='x']`
for full-path mode (e.g. `//body/*[@id='main']/*[@id='target']`).

Short-path XPath mode unchanged — still uses `//*[@id='x']` prefix.

Also added XPath evaluation assertion to the regression test to verify
the generated selector actually selects the correct element.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 09:46:55 +02:00
sjhddh 1ac26733d8 fix: prevent duplicate ID segments in full-path selector generation
When generating full-path CSS/XPath selectors, elements with id
attributes had their selector appended twice — once in the id branch
(line 30) and again unconditionally (line 50).

This produced selectors like 'body > #main > #main > #target > #target'
instead of the correct 'body > #main > #target'.

Move the append into the else branch so it only fires for elements
without an id (elements with id already append in the if branch).

Includes 2 regression tests.
2026-04-12 01:25:45 +02:00
Karim shoair d1baf1fc46 feat(spiders): add a development mode 2026-04-07 04:08:54 +02:00
voidborne-d eaa0e8cae6 fix: save checkpoint before cancel_scope.cancel() on force-stop to prevent data loss
On force-stop (second Ctrl+C), cancel_scope.cancel() was called BEFORE
_save_checkpoint(). Since cancel_scope.cancel() causes all subsequent
awaits within the scope to raise Cancelled, the checkpoint write was
silently aborted:

1. _save_checkpoint() uses anyio.open_file + rename — both are await
   checkpoints that get cancelled immediately
2. self.paused never gets set to True (code after the aborted save)
3. The finally block sees 'not self.paused' and calls cleanup() which
   DELETES the previous checkpoint file

Result: a user who ran a long crawl, pressed Ctrl+C twice to force-stop,
loses their entire checkpoint irrecoverably. The old checkpoint (from
periodic saves or a previous graceful pause) is deleted, and the new
one was never written.

Fix: move the cancel_scope.cancel() call AFTER the checkpoint save.
The save completes normally, self.paused is set to True, and only then
does the scope get cancelled to abort in-flight tasks. The finally
block correctly sees paused=True and skips cleanup.

Adds 6 regression tests covering:
- Force-stop checkpoint preservation (core regression)
- Graceful pause still works
- Force-stop checkpoint is loadable
- Normal completion cleanup still works
- Force-stop without checkpoint system
- Existing checkpoint not deleted on force-stop
2026-04-05 19:11:58 +00:00
Karim shoair d0a19a6fe1 build: pump version up 2026-04-05 19:41:10 +02:00
Karim shoair e7f9adb40a feat(security): default follow_redirects to "safe" for SSRF protection
curl_cffi v0.15.0 introduced CurlFollow.SAFE, which follows redirects but rejects those targeting internal/private IPs (loopback, private networks, link-local). This is now the default for all HTTP fetchers, the MCP server, and the shell curl converter.

Added FollowRedirects type alias supporting all curl_cffi redirect
modes: bool, "safe", "all", "obeycode", "firstonly".
2026-04-05 18:41:50 +02:00
Karim shoair 9383bec14e style(spider): removing excessive docstrings and unifying the style with the rest of the repo 2026-04-05 03:43:21 +02:00
Karim shoair 070338cf24 fix(spider): only allocate _domain_delays when robots_txt_obey is enabled 2026-04-05 02:36:15 +02:00
Karim shoair afaf68e7d5 fix(spider robots): removing dead code 2026-04-05 02:32:33 +02:00
Karim shoair ea2dd7866b refactor(spiders): Make Robots.txt compliance turned off by default
Scrapy is turning it off by default
2026-04-05 01:55:17 +02:00
Karim shoair 556a90f645 refactor(spider): prefetch robots.txt from start_urls only
Stop using allowed_domains for robots.txt prefetch since bare domain strings have no scheme info.
Domains discovered mid-crawl via requests still fetch robots.txt lazily.
2026-04-05 01:43:06 +02:00
Karim shoair ec487d37e8 fix(spider): Make delay in robots file don't affect user's concurrency settings 2026-04-05 01:39:05 +02:00
Karim shoair af83a11aa7 fix(spider robots): solve multiple issues with cache prefetch 2026-04-05 00:09:17 +02:00
Karim shoair 854daac794 style(spiders robots feat): Adjustments for maintainability 2026-04-04 21:06:35 +02:00
Abdullah a134fdb8cc feat(spiders): enable robots.txt compliance by default
robots_txt_obey now defaults to True. Spiders must explicitly opt out
with robots_txt_obey = False rather than opt in, making ethical
crawling the default behaviour.

File: scrapling/spiders/spider.py
2026-04-04 03:10:17 +02:00
Abdullah a86e9709ea feat(spiders): pre-warm robots.txt cache before crawl loop starts
Previously robots.txt was fetched lazily on the first request per
domain, causing early concurrent requests to each stall waiting for
the same network fetch. The cache is now warmed before the crawl loop
starts, making all subsequent robots.txt lookups a local read.

- RobotsTxtManager gains a prefetch(urls, sid) method that fetches all domains concurrently via a task group
- CrawlerEngine._prefetch_robots_txt() is called after on_start():
  uses allowed_domains if configured, otherwise falls back to unique
  domains extracted from start_urls
- Mid-crawl domain discovery (not covered by prefetch) still fetches
  lazily; two concurrent callbacks on the same new domain can each
  trigger a fetch — accepted tradeoff, documented in _get_domain_delay

Files: scrapling/spiders/robotstxt.py, scrapling/spiders/engine.py, tests/spiders/test_engine.py
2026-04-04 03:00:15 +02:00