From 7c0c23fd33b0f45c4aef7c3b3269861b732bd56b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 02:48:34 +0200 Subject: [PATCH 01/19] feat(unified import): Make one import for all fetchers Why I didn't think of that from the start? IDK --- scrapling/engines/toolbelt/custom.py | 92 +++++++++++++++++++--------- scrapling/fetchers.py | 65 ++++++++++++-------- 2 files changed, 103 insertions(+), 54 deletions(-) diff --git a/scrapling/engines/toolbelt/custom.py b/scrapling/engines/toolbelt/custom.py index c91a3a8..a5ec27a 100644 --- a/scrapling/engines/toolbelt/custom.py +++ b/scrapling/engines/toolbelt/custom.py @@ -105,41 +105,77 @@ class Response(Adaptor): class BaseFetcher: - def __init__( - self, huge_tree: bool = True, keep_comments: Optional[bool] = False, auto_match: Optional[bool] = True, - storage: Any = SQLiteStorageSystem, storage_args: Optional[Dict] = None, - automatch_domain: Optional[str] = None, keep_cdata: Optional[bool] = False, - ): - """Arguments below are the same from the Adaptor class so you can pass them directly, the rest of Adaptor's arguments - are detected and passed automatically from the Fetcher based on the response for accessibility. + __slots__ = () + huge_tree: bool = True + auto_match: Optional[bool] = True + storage: Any = SQLiteStorageSystem + keep_cdata: Optional[bool] = False + storage_args: Optional[Dict] = None + keep_comments: Optional[bool] = False + automatch_domain: Optional[str] = None + parser_keywords: Tuple = ('huge_tree', 'auto_match', 'storage', 'keep_cdata', 'storage_args', 'keep_comments', 'automatch_domain',) # Left open for the user - :param huge_tree: Enabled by default, should always be enabled when parsing large HTML documents. This controls - libxml2 feature that forbids parsing certain large documents to protect from possible memory exhaustion. - :param keep_comments: While parsing the HTML body, drop comments or not. Disabled by default for obvious reasons - :param keep_cdata: While parsing the HTML body, drop cdata or not. Disabled by default for cleaner HTML. - :param auto_match: Globally turn-off the auto-match feature in all functions, this argument takes higher - priority over all auto-match related arguments/functions in the class. - :param storage: The storage class to be passed for auto-matching functionalities, see ``Docs`` for more info. - :param storage_args: A dictionary of ``argument->value`` pairs to be passed for the storage class. - If empty, default values will be used. - :param automatch_domain: For cases where you want to automatch selectors across different websites as if they were on the same website, use this argument to unify them. - Otherwise, the domain of the request is used by default. + def __init__(self, *args, **kwargs): + # For backward-compatibility before 0.2.99 + args_str = ", ".join(args) or '' + kwargs_str = ", ".join(f'{k}={v}' for k, v in kwargs.items()) or '' + if args_str: + args_str += ', ' + + log.warning(f'This logic is deprecated now and it will be removed with v0.3. Use `{self.__class__.__name__}.configure({args_str}{kwargs_str})` instead before fetching') + pass + + @classmethod + def display_config(cls): + return dict( + huge_tree=cls.huge_tree, + keep_comments=cls.keep_comments, + keep_cdata=cls.keep_cdata, + auto_match=cls.auto_match, + storage=cls.storage, + storage_args=cls.storage_args, + automatch_domain=cls.automatch_domain, + ) + + @classmethod + def configure(cls, **kwargs): + """Setup multiple arguments for the parser at once + + :param kwargs: The keywords can be any arguments of the following: huge_tree, keep_comments, keep_cdata, auto_match, storage, storage_args, automatch_domain """ + for key, value in kwargs.items(): + key = key.strip().lower() + if hasattr(cls, key): + if key in cls.parser_keywords: + setattr(cls, key, value) + else: + # Yup, no fun allowed LOL + raise AttributeError(f'Unknown parser argument: "{key}"; maybe you meant {cls.parser_keywords}?') + else: + raise ValueError(f'Unknown parser argument: "{key}"; maybe you meant {cls.parser_keywords}?') + + if not kwargs: + raise AttributeError(f'You must pass a keyword to configure, current keywords: {cls.parser_keywords}?') + + @classmethod + def _generate_parser_arguments(cls) -> Dict: # Adaptor class parameters # I won't validate Adaptor's class parameters here again, I will leave it to be validated later - self.adaptor_arguments = dict( - huge_tree=huge_tree, - keep_comments=keep_comments, - keep_cdata=keep_cdata, - auto_match=auto_match, - storage=storage, - storage_args=storage_args + parser_arguments = dict( + huge_tree=cls.huge_tree, + keep_comments=cls.keep_comments, + keep_cdata=cls.keep_cdata, + auto_match=cls.auto_match, + storage=cls.storage, + storage_args=cls.storage_args ) - if automatch_domain: - if type(automatch_domain) is not str: + if cls.automatch_domain: + if type(cls.automatch_domain) is not str: log.warning('[Ignored] The argument "automatch_domain" must be of string type') else: - self.adaptor_arguments.update({'automatch_domain': automatch_domain}) + parser_arguments.update({'automatch_domain': cls.automatch_domain}) + + return parser_arguments class StatusText: diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 4dadfe0..7134d7d 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -10,8 +10,9 @@ class Fetcher(BaseFetcher): Any additional keyword arguments passed to the methods below are passed to the respective httpx's method directly. """ + @classmethod def get( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP GET request for you but with some added flavors. @@ -25,12 +26,13 @@ class Fetcher(BaseFetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.get()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).get(**kwargs) return response_object + @classmethod def post( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP POST request for you but with some added flavors. @@ -44,12 +46,13 @@ class Fetcher(BaseFetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.post()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).post(**kwargs) return response_object + @classmethod def put( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP PUT request for you but with some added flavors. @@ -64,12 +67,13 @@ class Fetcher(BaseFetcher): :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).put(**kwargs) return response_object + @classmethod def delete( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP DELETE request for you but with some added flavors. @@ -83,14 +87,15 @@ class Fetcher(BaseFetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.delete()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).delete(**kwargs) return response_object class AsyncFetcher(Fetcher): + @classmethod async def get( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP GET request for you but with some added flavors. @@ -104,12 +109,13 @@ class AsyncFetcher(Fetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.get()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_get(**kwargs) return response_object + @classmethod async def post( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP POST request for you but with some added flavors. @@ -123,12 +129,13 @@ class AsyncFetcher(Fetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.post()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_post(**kwargs) return response_object + @classmethod async def put( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP PUT request for you but with some added flavors. @@ -142,12 +149,13 @@ class AsyncFetcher(Fetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.put()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_put(**kwargs) return response_object + @classmethod async def delete( - self, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, + cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: """Make basic HTTP DELETE request for you but with some added flavors. @@ -161,7 +169,7 @@ class AsyncFetcher(Fetcher): :param kwargs: Any additional keyword arguments are passed directly to `httpx.delete()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(self.adaptor_arguments.items()) + adaptor_arguments = tuple(cls._generate_parser_arguments().items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_delete(**kwargs) return response_object @@ -172,8 +180,9 @@ class StealthyFetcher(BaseFetcher): It works as real browsers passing almost all online tests/protections based on Camoufox. Other added flavors include setting the faked OS fingerprints to match the user's OS and the referer of every request is set as if this request came from Google's search of this URL's domain. """ + @classmethod def fetch( - self, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, + cls, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, @@ -226,12 +235,13 @@ class StealthyFetcher(BaseFetcher): extra_headers=extra_headers, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=self.adaptor_arguments, + adaptor_arguments=cls._generate_parser_arguments(), ) return engine.fetch(url) + @classmethod async def async_fetch( - self, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, + cls, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, @@ -284,7 +294,7 @@ class StealthyFetcher(BaseFetcher): extra_headers=extra_headers, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=self.adaptor_arguments, + adaptor_arguments=cls._generate_parser_arguments(), ) return await engine.async_fetch(url) @@ -305,8 +315,9 @@ class PlayWrightFetcher(BaseFetcher): > Note that these are the main options with PlayWright but it can be mixed together. """ + @classmethod def fetch( - self, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, + cls, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, page_action: Optional[Callable] = None, wait_selector: Optional[str] = None, wait_selector_state: SelectorWaitStates = 'attached', hide_canvas: bool = False, disable_webgl: bool = False, extra_headers: Optional[Dict[str, str]] = None, google_search: bool = True, @@ -361,12 +372,13 @@ class PlayWrightFetcher(BaseFetcher): nstbrowser_config=nstbrowser_config, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=self.adaptor_arguments, + adaptor_arguments=cls._generate_parser_arguments(), ) return engine.fetch(url) + @classmethod async def async_fetch( - self, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, + cls, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, page_action: Optional[Callable] = None, wait_selector: Optional[str] = None, wait_selector_state: SelectorWaitStates = 'attached', hide_canvas: bool = False, disable_webgl: bool = False, extra_headers: Optional[Dict[str, str]] = None, google_search: bool = True, @@ -421,12 +433,13 @@ class PlayWrightFetcher(BaseFetcher): nstbrowser_config=nstbrowser_config, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=self.adaptor_arguments, + adaptor_arguments=cls._generate_parser_arguments(), ) return await engine.async_fetch(url) class CustomFetcher(BaseFetcher): - def fetch(self, url: str, browser_engine, **kwargs) -> Response: - engine = check_if_engine_usable(browser_engine)(adaptor_arguments=self.adaptor_arguments, **kwargs) + @classmethod + def fetch(cls, url: str, browser_engine, **kwargs) -> Response: + engine = check_if_engine_usable(browser_engine)(adaptor_arguments=cls._generate_parser_arguments(), **kwargs) return engine.fetch(url) From 416bd919badd8135327c24ab19137f6b7c91ad7b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 02:49:02 +0200 Subject: [PATCH 02/19] fix: backward-compatibility with the new import logic --- scrapling/defaults.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scrapling/defaults.py b/scrapling/defaults.py index 4098f76..903cf6f 100644 --- a/scrapling/defaults.py +++ b/scrapling/defaults.py @@ -1,19 +1,25 @@ -# If you are going to use Fetchers with the default settings, import them from this file instead for a cleaner looking code +# Left this file for backward-compatibility before 0.2.99 +from scrapling.core.utils import log + # A lightweight approach to create lazy loader for each import for backward compatibility # This will reduces initial memory footprint significantly (only loads what's used) def __getattr__(name): if name == 'Fetcher': from scrapling.fetchers import Fetcher as cls - return cls() + log.warning('This import is deprecated now and it will be removed with v0.3. Use `from scrapling.fetchers import Fetcher` instead') + return cls elif name == 'AsyncFetcher': from scrapling.fetchers import AsyncFetcher as cls - return cls() + log.warning('This import is deprecated now and it will be removed with v0.3. Use `from scrapling.fetchers import AsyncFetcher` instead') + return cls elif name == 'StealthyFetcher': from scrapling.fetchers import StealthyFetcher as cls - return cls() + log.warning('This import is deprecated now and it will be removed with v0.3. Use `from scrapling.fetchers import StealthyFetcher` instead') + return cls elif name == 'PlayWrightFetcher': from scrapling.fetchers import PlayWrightFetcher as cls - return cls() + log.warning('This import is deprecated now and it will be removed with v0.3. Use `from scrapling.fetchers import PlayWrightFetcher` instead') + return cls else: raise AttributeError(f"module 'scrapling' has no attribute '{name}'") From d7ef555d99585a4485e81ec9778003bfbddfae6d Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 03:01:42 +0200 Subject: [PATCH 03/19] docs: Updating the README to reflect the new changes --- README.md | 56 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 88a7fda..3eebf98 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Dealing with failing web scrapers due to anti-bot protections or website changes Scrapling is a high-performance, intelligent web scraping library for Python that automatically adapts to website changes while significantly outperforming popular alternatives. For both beginners and experts, Scrapling provides powerful features while maintaining simplicity. ```python ->> from scrapling.defaults import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher +>> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher # Fetch websites' source under the radar! >> page = StealthyFetcher.fetch('https://example.com', headless=True, network_idle=True) >> print(page.status) @@ -200,18 +200,24 @@ Fetchers are interfaces built on top of other libraries with added features that ### Features You might be slightly confused by now so let me clear things up. All fetcher-type classes are imported in the same way ```python -from scrapling.fetchers import Fetcher, StealthyFetcher, PlayWrightFetcher +>>> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher ``` -All of them can take these initialization arguments: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class. +then use it right away without initializing like this and it will use the default parser settings: +```python +>>> page = StealthyFetcher.fetch('https://example.com') +``` +If you want to configure the parser (Adaptor class) that will be used on the response before returning it for you then do this first: +```python +>>> StealthyFetcher.configure(auto_match=True, keep_comments=False) +``` +or +```python +>>> StealthyFetcher.auto_match = True +>>> StealthyFetcher.keep_comments = False +``` +Then continue your code as normal. -If you don't want to pass arguments to the generated `Adaptor` object and want to use the default values, you can use this import instead for cleaner code: -```python -from scrapling.defaults import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher -``` -then use it right away without initializing like: -```python -page = StealthyFetcher.fetch('https://example.com') -``` +The available configuration arguments are: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class. Also, the `Response` object returned from all fetchers is the same as the `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, `history`, and `request_headers`. All `cookies`, `headers`, and `request_headers` are always of type `dictionary`. > [!NOTE] @@ -225,26 +231,26 @@ For all methods, you have `stealthy_headers` which makes `Fetcher` create and us You can route all traffic (HTTP and HTTPS) to a proxy for any of these methods in this format `http://username:password@localhost:8030` ```python ->> page = Fetcher().get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) ->> page = Fetcher().post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') ->> page = Fetcher().put('https://httpbin.org/put', data={'key': 'value'}) ->> page = Fetcher().delete('https://httpbin.org/delete') +>> page = Fetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) +>> page = Fetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') +>> page = Fetcher.put('https://httpbin.org/put', data={'key': 'value'}) +>> page = Fetcher.delete('https://httpbin.org/delete') ``` For Async requests, you will just replace the import like below: ```python >> from scrapling.fetchers import AsyncFetcher ->> page = await AsyncFetcher().get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) ->> page = await AsyncFetcher().post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') ->> page = await AsyncFetcher().put('https://httpbin.org/put', data={'key': 'value'}) ->> page = await AsyncFetcher().delete('https://httpbin.org/delete') +>> page = await AsyncFetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) +>> page = await AsyncFetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') +>> page = await AsyncFetcher.put('https://httpbin.org/put', data={'key': 'value'}) +>> page = await AsyncFetcher.delete('https://httpbin.org/delete') ``` ### StealthyFetcher This class is built on top of [Camoufox](https://github.com/daijro/camoufox), bypassing most anti-bot protections by default. Scrapling adds extra layers of flavors and configurations to increase performance and undetectability even further. ```python ->> page = StealthyFetcher().fetch('https://www.browserscan.net/bot-detection') # Running headless by default +>> page = StealthyFetcher.fetch('https://www.browserscan.net/bot-detection') # Running headless by default >> page.status == 200 True ->> page = await StealthyFetcher().async_fetch('https://www.browserscan.net/bot-detection') # the async version of fetch +>> page = await StealthyFetcher.async_fetch('https://www.browserscan.net/bot-detection') # the async version of fetch >> page.status == 200 True ``` @@ -281,10 +287,10 @@ This list isn't final so expect a lot more additions and flexibility to be added ### PlayWrightFetcher This class is built on top of [Playwright](https://playwright.dev/python/) which currently provides 4 main run options but they can be mixed as you want. ```python ->> page = PlayWrightFetcher().fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # Vanilla Playwright option +>> page = PlayWrightFetcher.fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # Vanilla Playwright option >> page.css_first("#search a::attr(href)") 'https://github.com/D4Vinci/Scrapling' ->> page = await PlayWrightFetcher().async_fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # the async version of fetch +>> page = await PlayWrightFetcher.async_fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # the async version of fetch >> page.css_first("#search a::attr(href)") 'https://github.com/D4Vinci/Scrapling' ``` @@ -386,7 +392,7 @@ You can search for a specific ancestor of an element that satisfies a function, ### Content-based Selection & Finding Similar Elements You can select elements by their text content in multiple ways, here's a full example on another website: ```python ->>> page = Fetcher().get('https://books.toscrape.com/index.html') +>>> page = Fetcher.get('https://books.toscrape.com/index.html') >>> page.find_by_text('Tipping the Velvet') # Find the first element whose text fully matches this text @@ -566,7 +572,7 @@ Examples to clear any confusion :) ```python >> from scrapling.fetchers import Fetcher ->> page = Fetcher().get('https://quotes.toscrape.com/') +>> page = Fetcher.get('https://quotes.toscrape.com/') # Find all elements with tag name `div`. >> page.find_all('div') [
, From 7ca8aaf2dfb6660728c45cfd106d5ba1502bc2e6 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 03:56:56 +0200 Subject: [PATCH 04/19] feat(custom_config): The ability to pass custom parser config to requests --- scrapling/fetchers.py | 116 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 7134d7d..643c845 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -13,7 +13,7 @@ class Fetcher(BaseFetcher): @classmethod def get( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP GET request for you but with some added flavors. :param url: Target url. @@ -23,17 +23,23 @@ class Fetcher(BaseFetcher): create a referer header as if this request had came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.get()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).get(**kwargs) return response_object @classmethod def post( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP POST request for you but with some added flavors. :param url: Target url. @@ -43,17 +49,23 @@ class Fetcher(BaseFetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.post()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).post(**kwargs) return response_object @classmethod def put( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP PUT request for you but with some added flavors. :param url: Target url @@ -63,18 +75,24 @@ class Fetcher(BaseFetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.put()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).put(**kwargs) return response_object @classmethod def delete( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP DELETE request for you but with some added flavors. :param url: Target url @@ -84,10 +102,16 @@ class Fetcher(BaseFetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.delete()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries, adaptor_arguments=adaptor_arguments).delete(**kwargs) return response_object @@ -96,7 +120,7 @@ class AsyncFetcher(Fetcher): @classmethod async def get( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP GET request for you but with some added flavors. :param url: Target url. @@ -106,17 +130,23 @@ class AsyncFetcher(Fetcher): create a referer header as if this request had came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.get()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_get(**kwargs) return response_object @classmethod async def post( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP POST request for you but with some added flavors. :param url: Target url. @@ -126,17 +156,23 @@ class AsyncFetcher(Fetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.post()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_post(**kwargs) return response_object @classmethod async def put( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP PUT request for you but with some added flavors. :param url: Target url @@ -146,17 +182,23 @@ class AsyncFetcher(Fetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.put()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_put(**kwargs) return response_object @classmethod async def delete( cls, url: str, follow_redirects: bool = True, timeout: Optional[Union[int, float]] = 10, stealthy_headers: bool = True, - proxy: Optional[str] = None, retries: Optional[int] = 3, **kwargs: Dict) -> Response: + proxy: Optional[str] = None, retries: Optional[int] = 3, custom_config: Dict = None, **kwargs: Dict) -> Response: """Make basic HTTP DELETE request for you but with some added flavors. :param url: Target url @@ -166,10 +208,16 @@ class AsyncFetcher(Fetcher): create a referer header as if this request came from Google's search of this URL's domain. :param proxy: A string of a proxy to use for http and https requests, the format accepted is `http://username:password@localhost:8030` :param retries: The number of retries to do through httpx if the request failed for any reason. The default is 3 retries. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :param kwargs: Any additional keyword arguments are passed directly to `httpx.delete()` function so check httpx documentation for details. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ - adaptor_arguments = tuple(cls._generate_parser_arguments().items()) + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + + adaptor_arguments = tuple({**cls._generate_parser_arguments(), **custom_config}.items()) response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_delete(**kwargs) return response_object @@ -187,6 +235,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, + custom_config: Dict = None ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -214,8 +263,14 @@ class StealthyFetcher(BaseFetcher): :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + engine = CamoufoxEngine( proxy=proxy, geoip=geoip, @@ -235,7 +290,7 @@ class StealthyFetcher(BaseFetcher): extra_headers=extra_headers, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=cls._generate_parser_arguments(), + adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, ) return engine.fetch(url) @@ -246,6 +301,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, + custom_config: Dict = None ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -273,8 +329,14 @@ class StealthyFetcher(BaseFetcher): :param google_search: Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + engine = CamoufoxEngine( proxy=proxy, geoip=geoip, @@ -294,7 +356,7 @@ class StealthyFetcher(BaseFetcher): extra_headers=extra_headers, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=cls._generate_parser_arguments(), + adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, ) return await engine.async_fetch(url) @@ -325,6 +387,7 @@ class PlayWrightFetcher(BaseFetcher): stealth: bool = False, real_chrome: bool = False, cdp_url: Optional[str] = None, nstbrowser_mode: bool = False, nstbrowser_config: Optional[Dict] = None, + custom_config: Dict = None ) -> Response: """Opens up a browser and do your request based on your chosen options below. @@ -350,8 +413,14 @@ class PlayWrightFetcher(BaseFetcher): :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. :param nstbrowser_mode: Enables NSTBrowser mode, it have to be used with `cdp_url` argument or it will get completely ignored. :param nstbrowser_config: The config you want to send with requests to the NSTBrowser. If left empty, Scrapling defaults to an optimized NSTBrowser's docker browserless config. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + engine = PlaywrightEngine( proxy=proxy, locale=locale, @@ -372,7 +441,7 @@ class PlayWrightFetcher(BaseFetcher): nstbrowser_config=nstbrowser_config, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=cls._generate_parser_arguments(), + adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, ) return engine.fetch(url) @@ -386,6 +455,7 @@ class PlayWrightFetcher(BaseFetcher): stealth: bool = False, real_chrome: bool = False, cdp_url: Optional[str] = None, nstbrowser_mode: bool = False, nstbrowser_config: Optional[Dict] = None, + custom_config: Dict = None ) -> Response: """Opens up a browser and do your request based on your chosen options below. @@ -411,8 +481,14 @@ class PlayWrightFetcher(BaseFetcher): :param cdp_url: Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. :param nstbrowser_mode: Enables NSTBrowser mode, it have to be used with `cdp_url` argument or it will get completely ignored. :param nstbrowser_config: The config you want to send with requests to the NSTBrowser. If left empty, Scrapling defaults to an optimized NSTBrowser's docker browserless config. + :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ + if not custom_config: + custom_config = {} + elif not isinstance(custom_config, dict): + ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") + engine = PlaywrightEngine( proxy=proxy, locale=locale, @@ -433,7 +509,7 @@ class PlayWrightFetcher(BaseFetcher): nstbrowser_config=nstbrowser_config, disable_resources=disable_resources, wait_selector_state=wait_selector_state, - adaptor_arguments=cls._generate_parser_arguments(), + adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, ) return await engine.async_fetch(url) From a0952dca8fd72621c270f3dec94742c3f66fda2b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 05:28:28 +0200 Subject: [PATCH 05/19] docs: Updating the README to reflect the new changes --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eebf98..7353659 100644 --- a/README.md +++ b/README.md @@ -217,11 +217,17 @@ or ``` Then continue your code as normal. -The available configuration arguments are: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class. +The available configuration arguments are: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class. You can display the current configuration anytime by running `.display_config()` Also, the `Response` object returned from all fetchers is the same as the `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, `history`, and `request_headers`. All `cookies`, `headers`, and `request_headers` are always of type `dictionary`. > [!NOTE] > The `auto_match` argument is enabled by default which is the one you should care about the most as you will see later. + +#### Set parser config per request +As you probably understood, the logic above for setting the parser config will work globally for all requests/fetches done through that class and it's intended. + +If your use case requires you to use different config for each request/fetch, then you can pass a dictionary to the request method (`fetch`/`get`/`post`/...) to an argument named `custom_config`. + ### Fetcher This class is built on top of [httpx](https://www.python-httpx.org/) with additional configuration options, here you can do `GET`, `POST`, `PUT`, and `DELETE` requests. From d33e738c53f4bc460f4c4fe9091e3689e5c9ea3b Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 05:36:05 +0200 Subject: [PATCH 06/19] feat(StealthyFetcher): The ability to pass additional arguments to Camoufox --- scrapling/engines/camo.py | 4 ++++ scrapling/fetchers.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scrapling/engines/camo.py b/scrapling/engines/camo.py index 7893526..246c308 100644 --- a/scrapling/engines/camo.py +++ b/scrapling/engines/camo.py @@ -22,6 +22,7 @@ class CamoufoxEngine: proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, adaptor_arguments: Dict = None, + **additional_arguments: Dict ): """An engine that utilizes Camoufox library, check the `StealthyFetcher` class for more documentation. @@ -48,6 +49,7 @@ class CamoufoxEngine: :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param adaptor_arguments: The arguments that will be passed in the end while creating the final Adaptor's class. + :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. """ self.headless = headless self.block_images = bool(block_images) @@ -60,6 +62,7 @@ class CamoufoxEngine: self.disable_ads = bool(disable_ads) self.geoip = bool(geoip) self.extra_headers = extra_headers or {} + self.additional_arguments = additional_arguments self.proxy = construct_proxy_dict(proxy) self.addons = addons or [] self.humanize = humanize @@ -92,6 +95,7 @@ class CamoufoxEngine: "block_webrtc": self.block_webrtc, "block_images": self.block_images, # Careful! it makes some websites doesn't finish loading at all like stackoverflow even in headful "os": None if self.os_randomize else get_os_name(), + **self.additional_arguments } def _process_response_history(self, first_response): diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 643c845..7728402 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -235,7 +235,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, - custom_config: Dict = None + custom_config: Dict = None, **additional_arguments: Dict ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -264,6 +264,7 @@ class StealthyFetcher(BaseFetcher): :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. + :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ if not custom_config: @@ -291,6 +292,7 @@ class StealthyFetcher(BaseFetcher): disable_resources=disable_resources, wait_selector_state=wait_selector_state, adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, + **additional_arguments ) return engine.fetch(url) @@ -301,7 +303,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, - custom_config: Dict = None + custom_config: Dict = None, **additional_arguments: Dict ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -330,6 +332,7 @@ class StealthyFetcher(BaseFetcher): :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. + :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ if not custom_config: @@ -357,6 +360,7 @@ class StealthyFetcher(BaseFetcher): disable_resources=disable_resources, wait_selector_state=wait_selector_state, adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, + **additional_arguments ) return await engine.async_fetch(url) From 8a2ac942431098c38755c19846e89b00d3c7178f Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 05:41:03 +0200 Subject: [PATCH 07/19] docs: Updating the benchmarks table with current numbers All libraries are updated to the latest version --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7353659..40926c7 100644 --- a/README.md +++ b/README.md @@ -161,25 +161,25 @@ Here are benchmarks comparing Scrapling to popular Python libraries in two tests | # | Library | Time (ms) | vs Scrapling | |---|:-----------------:|:---------:|:------------:| -| 1 | Scrapling | 5.44 | 1.0x | -| 2 | Parsel/Scrapy | 5.53 | 1.017x | -| 3 | Raw Lxml | 6.76 | 1.243x | -| 4 | PyQuery | 21.96 | 4.037x | -| 5 | Selectolax | 67.12 | 12.338x | -| 6 | BS4 with Lxml | 1307.03 | 240.263x | -| 7 | MechanicalSoup | 1322.64 | 243.132x | -| 8 | BS4 with html5lib | 3373.75 | 620.175x | +| 1 | Scrapling | 5.55 | 1.0x | +| 2 | Parsel/Scrapy | 5.67 | 1.022x | +| 3 | Raw Lxml | 6.69 | 1.205x | +| 4 | PyQuery | 20.84 | 3.755x | +| 5 | Selectolax | 84.41 | 15.209x | +| 6 | BS4 with Lxml | 1313.45 | 236.658x | +| 7 | MechanicalSoup | 1313.66 | 236.695x | +| 8 | BS4 with html5lib | 3383.27 | 609.598x | -As you see, Scrapling is on par with Scrapy and slightly faster than Lxml which both libraries are built on top of. These are the closest results to Scrapling. PyQuery is also built on top of Lxml but still, Scrapling is 4 times faster. +As you see, Scrapling is on par with Scrapy and slightly faster than Lxml which both libraries are built on top of. These are the closest results to Scrapling. PyQuery is also built on top of Lxml but still, Scrapling is ~4 times faster. ### Extraction By Text Speed Test | Library | Time (ms) | vs Scrapling | |:-----------:|:---------:|:------------:| -| Scrapling | 2.51 | 1.0x | -| AutoScraper | 11.41 | 4.546x | +| Scrapling | 2.35 | 1.0x | +| AutoScraper | 11.44 | 4.868x | -Scrapling can find elements with more methods and it returns full element `Adaptor` objects not only the text like AutoScraper. So, to make this test fair, both libraries will extract an element with text, find similar elements, and then extract the text content for all of them. As you see, Scrapling is still 4.5 times faster at the same task. +Scrapling can find elements with more methods and it returns full element `Adaptor` objects not only the text like AutoScraper. So, to make this test fair, both libraries will extract an element with text, find similar elements, and then extract the text content for all of them. As you see, Scrapling is still 4.8 times faster at the same task. > All benchmarks' results are an average of 100 runs. See our [benchmarks.py](https://github.com/D4Vinci/Scrapling/blob/main/benchmarks.py) for methodology and to run your comparisons. From 7025a7910ed58984baf9a0f7cedd690018ac1536 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 05:44:45 +0200 Subject: [PATCH 08/19] build: Pump up the version --- scrapling/__init__.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scrapling/__init__.py b/scrapling/__init__.py index feda050..ee918f1 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -1,6 +1,6 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" -__version__ = "0.2.98" +__version__ = "0.2.99" __copyright__ = "Copyright (c) 2024 Karim Shoair" diff --git a/setup.cfg b/setup.cfg index 35d934c..17c82bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scrapling -version = 0.2.98 +version = 0.2.99 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 again! diff --git a/setup.py b/setup.py index b060b9b..99357cf 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setup( name="scrapling", - version="0.2.98", + version="0.2.99", description="""Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy again! In an internet filled with complications, it simplifies web scraping, even when websites' design changes, while providing impressive speed that surpasses almost all alternatives.""", long_description=long_description, From fec82156312842a52ac5956e55891fa4ddbec4df Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 05:56:49 +0200 Subject: [PATCH 09/19] docs: Clearer docs for recent changes --- scrapling/engines/toolbelt/custom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapling/engines/toolbelt/custom.py b/scrapling/engines/toolbelt/custom.py index a5ec27a..90c6861 100644 --- a/scrapling/engines/toolbelt/custom.py +++ b/scrapling/engines/toolbelt/custom.py @@ -122,7 +122,7 @@ class BaseFetcher: if args_str: args_str += ', ' - log.warning(f'This logic is deprecated now and it will be removed with v0.3. Use `{self.__class__.__name__}.configure({args_str}{kwargs_str})` instead before fetching') + log.warning(f'This logic is deprecated now, and have no effect; It will be removed with v0.3. Use `{self.__class__.__name__}.configure({args_str}{kwargs_str})` instead before fetching') pass @classmethod @@ -139,7 +139,7 @@ class BaseFetcher: @classmethod def configure(cls, **kwargs): - """Setup multiple arguments for the parser at once + """Set multiple arguments for the parser at once globally :param kwargs: The keywords can be any arguments of the following: huge_tree, keep_comments, keep_cdata, auto_match, storage, storage_args, automatch_domain """ From 811cd2d9b0096e8fdec1f53addebd5412454d121 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 19:25:19 +0200 Subject: [PATCH 10/19] refactor: make auto_match disabled by default --- scrapling/engines/toolbelt/custom.py | 2 +- scrapling/parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapling/engines/toolbelt/custom.py b/scrapling/engines/toolbelt/custom.py index 90c6861..eb01345 100644 --- a/scrapling/engines/toolbelt/custom.py +++ b/scrapling/engines/toolbelt/custom.py @@ -107,7 +107,7 @@ class Response(Adaptor): class BaseFetcher: __slots__ = () huge_tree: bool = True - auto_match: Optional[bool] = True + auto_match: Optional[bool] = False storage: Any = SQLiteStorageSystem keep_cdata: Optional[bool] = False storage_args: Optional[Dict] = None diff --git a/scrapling/parser.py b/scrapling/parser.py index 98e0864..b3967ec 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -39,7 +39,7 @@ class Adaptor(SelectorsGeneration): root: Optional[html.HtmlElement] = None, keep_comments: Optional[bool] = False, keep_cdata: Optional[bool] = False, - auto_match: Optional[bool] = True, + auto_match: Optional[bool] = False, storage: Any = SQLiteStorageSystem, storage_args: Optional[Dict] = None, **kwargs From 8551d7eeeaed9c3895ba5a5001d521e4ac68b60e Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 19:25:49 +0200 Subject: [PATCH 11/19] docs: Update README reflect current version accurately --- README.md | 101 +++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 40926c7..9e3f379 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Scrapling is a high-performance, intelligent web scraping library for Python tha ```python >> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher +>> StealthyFetcher.auto_match = True # Fetch websites' source under the radar! >> page = StealthyFetcher.fetch('https://example.com', headless=True, network_idle=True) >> print(page.status) @@ -50,45 +51,48 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL --- ## Table of content - * [Key Features](#key-features) - * [Fetch websites as you prefer](#fetch-websites-as-you-prefer-with-async-support) - * [Adaptive Scraping](#adaptive-scraping) - * [Performance](#performance) - * [Developing Experience](#developing-experience) - * [Getting Started](#getting-started) - * [Parsing Performance](#parsing-performance) - * [Text Extraction Speed Test (5000 nested elements).](#text-extraction-speed-test-5000-nested-elements) - * [Extraction By Text Speed Test](#extraction-by-text-speed-test) - * [Installation](#installation) - * [Fetching Websites](#fetching-websites) - * [Features](#features) - * [Fetcher class](#fetcher) - * [StealthyFetcher class](#stealthyfetcher) - * [PlayWrightFetcher class](#playwrightfetcher) - * [Advanced Parsing Features](#advanced-parsing-features) - * [Smart Navigation](#smart-navigation) - * [Content-based Selection & Finding Similar Elements](#content-based-selection--finding-similar-elements) - * [Handling Structural Changes](#handling-structural-changes) - * [Real World Scenario](#real-world-scenario) - * [Find elements by filters](#find-elements-by-filters) - * [Is That All?](#is-that-all) - * [More Advanced Usage](#more-advanced-usage) - * [⚑ Enlightening Questions and FAQs](#-enlightening-questions-and-faqs) - * [How does auto-matching work?](#how-does-auto-matching-work) - * [How does the auto-matching work if I didn't pass a URL while initializing the Adaptor object?](#how-does-the-auto-matching-work-if-i-didnt-pass-a-url-while-initializing-the-adaptor-object) - * [If all things about an element can change or get removed, what are the unique properties to be saved?](#if-all-things-about-an-element-can-change-or-get-removed-what-are-the-unique-properties-to-be-saved) - * [I have enabled the `auto_save`/`auto_match` parameter while selecting and it got completely ignored with a warning message](#i-have-enabled-the-auto_saveauto_match-parameter-while-selecting-and-it-got-completely-ignored-with-a-warning-message) - * [I have done everything as the docs but the auto-matching didn't return anything, what's wrong?](#i-have-done-everything-as-the-docs-but-the-auto-matching-didnt-return-anything-whats-wrong) - * [Can Scrapling replace code built on top of BeautifulSoup4?](#can-scrapling-replace-code-built-on-top-of-beautifulsoup4) - * [Can Scrapling replace code built on top of AutoScraper?](#can-scrapling-replace-code-built-on-top-of-autoscraper) - * [Is Scrapling thread-safe?](#is-scrapling-thread-safe) - * [More Sponsors!](#more-sponsors) - * [Contributing](#contributing) - * [Disclaimer for Scrapling Project](#disclaimer-for-scrapling-project) - * [License](#license) - * [Acknowledgments](#acknowledgments) - * [Thanks and References](#thanks-and-references) - * [Known Issues](#known-issues) +* [Key Features](#key-features) + * [Fetch websites as you prefer with async support](#fetch-websites-as-you-prefer-with-async-support) + * [Adaptive Scraping](#adaptive-scraping) + * [High Performance](#high-performance) + * [Developer Friendly](#developer-friendly) +* [Getting Started](#getting-started) +* [Parsing Performance](#parsing-performance) + * [Text Extraction Speed Test (5000 nested elements).](#text-extraction-speed-test-5000-nested-elements) + * [Extraction By Text Speed Test](#extraction-by-text-speed-test) +* [Installation](#installation) +* [Fetching Websites](#fetching-websites) + * [Features](#features) + * [Set parser config per request](#set-parser-config-per-request) + * [Fetcher](#fetcher) + * [StealthyFetcher](#stealthyfetcher) + * [The complete list of arguments](#the-complete-list-of-arguments) + * [PlayWrightFetcher](#playwrightfetcher) + * [The complete list of arguments](#the-complete-list-of-arguments-1) +* [Advanced Parsing Features](#advanced-parsing-features) + * [Smart Navigation](#smart-navigation) + * [Content-based Selection & Finding Similar Elements](#content-based-selection--finding-similar-elements) + * [Handling Structural Changes](#handling-structural-changes) + * [Real-World Scenario](#real-world-scenario) + * [Find elements by filters](#find-elements-by-filters) + * [Is That All?](#is-that-all) +* [More Advanced Usage](#more-advanced-usage) +* [⚑ Enlightening Questions and FAQs](#-enlightening-questions-and-faqs) + * [How does auto-matching work?](#how-does-auto-matching-work) + * [How does the auto-matching work if I didn't pass a URL while initializing the Adaptor object?](#how-does-the-auto-matching-work-if-i-didnt-pass-a-url-while-initializing-the-adaptor-object) + * [If all things about an element can change or get removed, what are the unique properties to be saved?](#if-all-things-about-an-element-can-change-or-get-removed-what-are-the-unique-properties-to-be-saved) + * [I have enabled the `auto_save`/`auto_match` parameter while selecting and it got completely ignored with a warning message](#i-have-enabled-the-auto_saveauto_match-parameter-while-selecting-and-it-got-completely-ignored-with-a-warning-message) + * [I have done everything as the docs but the auto-matching didn't return anything, what's wrong?](#i-have-done-everything-as-the-docs-but-the-auto-matching-didnt-return-anything-whats-wrong) + * [Can Scrapling replace code built on top of BeautifulSoup4?](#can-scrapling-replace-code-built-on-top-of-beautifulsoup4) + * [Can Scrapling replace code built on top of AutoScraper?](#can-scrapling-replace-code-built-on-top-of-autoscraper) + * [Is Scrapling thread-safe?](#is-scrapling-thread-safe) +* [More Sponsors!](#more-sponsors) +* [Contributing](#contributing) +* [Disclaimer for Scrapling Project](#disclaimer-for-scrapling-project) +* [License](#license) +* [Acknowledgments](#acknowledgments) +* [Thanks and References](#thanks-and-references) +* [Known Issues](#known-issues) ## Key Features @@ -120,10 +124,8 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL ```python from scrapling.fetchers import Fetcher -fetcher = Fetcher(auto_match=False) - # Do http GET request to a web page and create an Adaptor instance -page = fetcher.get('https://quotes.toscrape.com/', stealthy_headers=True) +page = Fetcher.get('https://quotes.toscrape.com/', stealthy_headers=True) # Get all text content from all HTML tags in the page except `script` and `style` tags page.get_all_text(ignore_tags=('script', 'style')) @@ -221,7 +223,7 @@ The available configuration arguments are: `auto_match`, `huge_tree`, `keep_comm Also, the `Response` object returned from all fetchers is the same as the `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, `history`, and `request_headers`. All `cookies`, `headers`, and `request_headers` are always of type `dictionary`. > [!NOTE] -> The `auto_match` argument is enabled by default which is the one you should care about the most as you will see later. +> The `auto_match` argument is disabled by default, you need to enable it to use that feature. #### Set parser config per request As you probably understood, the logic above for setting the parser config will work globally for all requests/fetches done through that class and it's intended. @@ -262,7 +264,7 @@ True ``` > Note: all requests done by this fetcher are waiting by default for all JS to be fully loaded and executed so you don't have to :) -
For the sake of simplicity, expand this for the complete list of arguments +#### The complete list of arguments | Argument | Description | Optional | |:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| @@ -286,7 +288,6 @@ True | os_randomize | If enabled, Scrapling will randomize the OS fingerprints used. The default is Scrapling matching the fingerprints with the current OS. | βœ”οΈ | | wait_selector_state | The state to wait for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | -
This list isn't final so expect a lot more additions and flexibility to be added in the next versions! @@ -316,7 +317,7 @@ Using this Fetcher class, you can make requests with: Add that to a lot of controlling/hiding options as you will see in the arguments list below. -
Expand this for the complete list of arguments +#### The complete list of arguments | Argument | Description | Optional | |:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| @@ -341,7 +342,6 @@ Add that to a lot of controlling/hiding options as you will see in the arguments | nstbrowser_mode | Enables NSTBrowser mode, **it have to be used with `cdp_url` argument or it will get completely ignored.** | βœ”οΈ | | nstbrowser_config | The config you want to send with requests to the NSTBrowser. _If left empty, Scrapling defaults to an optimized NSTBrowser's docker browserless config._ | βœ”οΈ | -
This list isn't final so expect a lot more additions and flexibility to be added in the next versions! @@ -500,7 +500,7 @@ The selector will no longer function and your code needs maintenance. That's whe ```python from scrapling.parser import Adaptor # Before the change -page = Adaptor(page_source, url='example.com') +page = Adaptor(page_source, auto_match=True, url='example.com') element = page.css('#p1' auto_save=True) if not element: # One day website changes? element = page.css('#p1', auto_match=True) # Scrapling still finds it! @@ -520,12 +520,13 @@ Now let's test the same selector in both versions >> selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a' >> old_url = "https://web.archive.org/web/20100102003420/http://stackoverflow.com/" >> new_url = "https://stackoverflow.com/" +>> Fetcher.configure(auto_match=True, automatch_domain='stackoverflow.com') >> ->> page = Fetcher(automatch_domain='stackoverflow.com').get(old_url, timeout=30) +>> page = Fetcher.get(old_url, timeout=30) >> element1 = page.css_first(selector, auto_save=True) >> >> # Same selector but used in the updated website ->> page = Fetcher(automatch_domain="stackoverflow.com").get(new_url) +>> page = Fetcher.get(new_url) >> element2 = page.css_first(selector, auto_match=True) >> >> if element1.text == element2.text: From c36baf7b4ce1895c56fb31d08eb187947d4bfcdb Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Wed, 26 Mar 2025 21:56:04 +0200 Subject: [PATCH 12/19] test: Update tests to use current configuration --- tests/fetchers/async/test_camoufox.py | 4 +++- tests/fetchers/async/test_httpx.py | 4 +++- tests/fetchers/async/test_playwright.py | 4 +++- tests/fetchers/sync/test_camoufox.py | 4 +++- tests/fetchers/sync/test_httpx.py | 4 +++- tests/fetchers/sync/test_playwright.py | 4 +++- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/fetchers/async/test_camoufox.py b/tests/fetchers/async/test_camoufox.py index f2f495d..937ec43 100644 --- a/tests/fetchers/async/test_camoufox.py +++ b/tests/fetchers/async/test_camoufox.py @@ -3,13 +3,15 @@ import pytest_httpbin from scrapling import StealthyFetcher +StealthyFetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin @pytest.mark.asyncio class TestStealthyFetcher: @pytest.fixture(scope="class") def fetcher(self): - return StealthyFetcher(auto_match=False) + return StealthyFetcher @pytest.fixture(scope="class") def urls(self, httpbin): diff --git a/tests/fetchers/async/test_httpx.py b/tests/fetchers/async/test_httpx.py index 67cc037..64b7fae 100644 --- a/tests/fetchers/async/test_httpx.py +++ b/tests/fetchers/async/test_httpx.py @@ -3,13 +3,15 @@ import pytest_httpbin from scrapling.fetchers import AsyncFetcher +AsyncFetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin @pytest.mark.asyncio class TestAsyncFetcher: @pytest.fixture(scope="class") def fetcher(self): - return AsyncFetcher(auto_match=True) + return AsyncFetcher @pytest.fixture(scope="class") def urls(self, httpbin): diff --git a/tests/fetchers/async/test_playwright.py b/tests/fetchers/async/test_playwright.py index cf29ebf..ff50d09 100644 --- a/tests/fetchers/async/test_playwright.py +++ b/tests/fetchers/async/test_playwright.py @@ -3,12 +3,14 @@ import pytest_httpbin from scrapling import PlayWrightFetcher +PlayWrightFetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin class TestPlayWrightFetcherAsync: @pytest.fixture def fetcher(self): - return PlayWrightFetcher(auto_match=False) + return PlayWrightFetcher @pytest.fixture def urls(self, httpbin): diff --git a/tests/fetchers/sync/test_camoufox.py b/tests/fetchers/sync/test_camoufox.py index 33800f4..c613c58 100644 --- a/tests/fetchers/sync/test_camoufox.py +++ b/tests/fetchers/sync/test_camoufox.py @@ -3,13 +3,15 @@ import pytest_httpbin from scrapling import StealthyFetcher +StealthyFetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin class TestStealthyFetcher: @pytest.fixture(scope="class") def fetcher(self): """Fixture to create a StealthyFetcher instance for the entire test class""" - return StealthyFetcher(auto_match=False) + return StealthyFetcher @pytest.fixture(autouse=True) def setup_urls(self, httpbin): diff --git a/tests/fetchers/sync/test_httpx.py b/tests/fetchers/sync/test_httpx.py index 9f5ca80..0a1abd8 100644 --- a/tests/fetchers/sync/test_httpx.py +++ b/tests/fetchers/sync/test_httpx.py @@ -3,13 +3,15 @@ import pytest_httpbin from scrapling import Fetcher +Fetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin class TestFetcher: @pytest.fixture(scope="class") def fetcher(self): """Fixture to create a Fetcher instance for the entire test class""" - return Fetcher(auto_match=False) + return Fetcher @pytest.fixture(autouse=True) def setup_urls(self, httpbin): diff --git a/tests/fetchers/sync/test_playwright.py b/tests/fetchers/sync/test_playwright.py index f683402..c256d27 100644 --- a/tests/fetchers/sync/test_playwright.py +++ b/tests/fetchers/sync/test_playwright.py @@ -3,6 +3,8 @@ import pytest_httpbin from scrapling import PlayWrightFetcher +PlayWrightFetcher.auto_match = True + @pytest_httpbin.use_class_based_httpbin class TestPlayWrightFetcher: @@ -10,7 +12,7 @@ class TestPlayWrightFetcher: @pytest.fixture(scope="class") def fetcher(self): """Fixture to create a StealthyFetcher instance for the entire test class""" - return PlayWrightFetcher(auto_match=False) + return PlayWrightFetcher @pytest.fixture(autouse=True) def setup_urls(self, httpbin): From eb4ca2c6a4a42ceda72abc32cf11adc017a27de1 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 02:24:43 +0200 Subject: [PATCH 13/19] fix(StealthyFetcher): change `additional_arguments` to a dictionary argument --- scrapling/engines/camo.py | 6 +++--- scrapling/fetchers.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scrapling/engines/camo.py b/scrapling/engines/camo.py index 246c308..a3f1720 100644 --- a/scrapling/engines/camo.py +++ b/scrapling/engines/camo.py @@ -22,7 +22,7 @@ class CamoufoxEngine: proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, adaptor_arguments: Dict = None, - **additional_arguments: Dict + additional_arguments: Dict = None ): """An engine that utilizes Camoufox library, check the `StealthyFetcher` class for more documentation. @@ -49,7 +49,7 @@ class CamoufoxEngine: :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param adaptor_arguments: The arguments that will be passed in the end while creating the final Adaptor's class. - :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. + :param additional_arguments: Additional arguments to be passed to Camoufox as additional settings and it takes higher priority than Scrapling's settings. """ self.headless = headless self.block_images = bool(block_images) @@ -62,7 +62,7 @@ class CamoufoxEngine: self.disable_ads = bool(disable_ads) self.geoip = bool(geoip) self.extra_headers = extra_headers or {} - self.additional_arguments = additional_arguments + self.additional_arguments = additional_arguments or {} self.proxy = construct_proxy_dict(proxy) self.addons = addons or [] self.humanize = humanize diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 7728402..3848ca6 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -235,7 +235,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, - custom_config: Dict = None, **additional_arguments: Dict + custom_config: Dict = None, additional_arguments: Dict = None ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -264,7 +264,7 @@ class StealthyFetcher(BaseFetcher): :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. - :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. + :param additional_arguments: Additional arguments to be passed to Camoufox as additional settings and it takes higher priority than Scrapling's settings. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ if not custom_config: @@ -292,7 +292,7 @@ class StealthyFetcher(BaseFetcher): disable_resources=disable_resources, wait_selector_state=wait_selector_state, adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, - **additional_arguments + additional_arguments=additional_arguments or {} ) return engine.fetch(url) @@ -303,7 +303,7 @@ class StealthyFetcher(BaseFetcher): timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, - custom_config: Dict = None, **additional_arguments: Dict + custom_config: Dict = None, additional_arguments: Dict = None ) -> Response: """ Opens up a browser and do your request based on your chosen options below. @@ -332,7 +332,7 @@ class StealthyFetcher(BaseFetcher): :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ :param proxy: The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. :param custom_config: A dictionary of custom parser arguments to use with this request. Any argument passed will override any class parameters values. - :param additional_arguments: Any Additional arguments will be passed to Camoufox as additional settings and takes higher priority than Scrapling's settings. + :param additional_arguments: Additional arguments to be passed to Camoufox as additional settings and it takes higher priority than Scrapling's settings. :return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers` """ if not custom_config: @@ -360,7 +360,7 @@ class StealthyFetcher(BaseFetcher): disable_resources=disable_resources, wait_selector_state=wait_selector_state, adaptor_arguments={**cls._generate_parser_arguments(), **custom_config}, - **additional_arguments + additional_arguments=additional_arguments or {} ) return await engine.async_fetch(url) From 128f7b9d6f350cecc24c0b32c22593ddb533b6ec Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 02:54:46 +0200 Subject: [PATCH 14/19] feat(PlayWrightFetcher): adding the option to `sleep` after fetch before closing the page --- scrapling/engines/pw.py | 5 +++++ scrapling/fetchers.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/scrapling/engines/pw.py b/scrapling/engines/pw.py index 0c71fec..5b55a95 100644 --- a/scrapling/engines/pw.py +++ b/scrapling/engines/pw.py @@ -21,6 +21,7 @@ class PlaywrightEngine: useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, + wait: Optional[int] = 0, page_action: Callable = None, wait_selector: Optional[str] = None, locale: Optional[str] = 'en-US', @@ -46,6 +47,7 @@ class PlaywrightEngine: :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. :param locale: Set the locale for the browser if wanted. The default value is `en-US`. @@ -76,6 +78,7 @@ class PlaywrightEngine: self.cdp_url = cdp_url self.useragent = useragent self.timeout = check_type_validity(timeout, [int, float], 30000) + self.wait = check_type_validity(wait, [int, float], 0) if page_action is not None: if callable(page_action): self.page_action = page_action @@ -289,6 +292,7 @@ class PlaywrightEngine: except Exception as e: log.error(f"Error waiting for selector {self.wait_selector}: {e}") + page.wait_for_timeout(self.wait) # In case we didn't catch a document type somehow final_response = final_response if final_response else first_response if not final_response: @@ -392,6 +396,7 @@ class PlaywrightEngine: except Exception as e: log.error(f"Error waiting for selector {self.wait_selector}: {e}") + await page.wait_for_timeout(self.wait) # In case we didn't catch a document type somehow final_response = final_response if final_response else first_response if not final_response: diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 3848ca6..84596cd 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -384,7 +384,7 @@ class PlayWrightFetcher(BaseFetcher): @classmethod def fetch( cls, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, - useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, + useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, wait: Optional[int] = 0, page_action: Optional[Callable] = None, wait_selector: Optional[str] = None, wait_selector_state: SelectorWaitStates = 'attached', hide_canvas: bool = False, disable_webgl: bool = False, extra_headers: Optional[Dict[str, str]] = None, google_search: bool = True, proxy: Optional[Union[str, Dict[str, str]]] = None, locale: Optional[str] = 'en-US', @@ -402,7 +402,8 @@ class PlayWrightFetcher(BaseFetcher): This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. - :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 + :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param locale: Set the locale for the browser if wanted. The default value is `en-US`. :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. @@ -426,6 +427,7 @@ class PlayWrightFetcher(BaseFetcher): ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") engine = PlaywrightEngine( + wait=wait, proxy=proxy, locale=locale, timeout=timeout, @@ -452,7 +454,7 @@ class PlayWrightFetcher(BaseFetcher): @classmethod async def async_fetch( cls, url: str, headless: Union[bool, str] = True, disable_resources: bool = None, - useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, + useragent: Optional[str] = None, network_idle: bool = False, timeout: Optional[float] = 30000, wait: Optional[int] = 0, page_action: Optional[Callable] = None, wait_selector: Optional[str] = None, wait_selector_state: SelectorWaitStates = 'attached', hide_canvas: bool = False, disable_webgl: bool = False, extra_headers: Optional[Dict[str, str]] = None, google_search: bool = True, proxy: Optional[Union[str, Dict[str, str]]] = None, locale: Optional[str] = 'en-US', @@ -470,7 +472,8 @@ class PlayWrightFetcher(BaseFetcher): This can help save your proxy usage but be careful with this option as it makes some websites never finish loading. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. - :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 + :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param locale: Set the locale for the browser if wanted. The default value is `en-US`. :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. @@ -494,6 +497,7 @@ class PlayWrightFetcher(BaseFetcher): ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") engine = PlaywrightEngine( + wait=wait, proxy=proxy, locale=locale, timeout=timeout, From d6a343440284455841dab6321ec25b2305e357f5 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 02:57:01 +0200 Subject: [PATCH 15/19] fix(PlayWrightFetcher): fix redirection issue with async_fetch --- scrapling/engines/pw.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/scrapling/engines/pw.py b/scrapling/engines/pw.py index 5b55a95..b94043f 100644 --- a/scrapling/engines/pw.py +++ b/scrapling/engines/pw.py @@ -223,6 +223,38 @@ class PlaywrightEngine: return history + async def _async_process_response_history(self, first_response): + """Process response history to build a list of Response objects""" + history = [] + current_request = first_response.request.redirected_from + + try: + while current_request: + try: + current_response = await current_request.response() + history.insert(0, Response( + url=current_request.url, + # using current_response.text() will trigger "Error: Response.text: Response body is unavailable for redirect responses" + text='', + body=b'', + status=current_response.status if current_response else 301, + reason=(current_response.status_text or StatusText.get(current_response.status)) if current_response else StatusText.get(301), + encoding=current_response.headers.get('content-type', '') or 'utf-8', + cookies={}, + headers=await current_response.all_headers() if current_response else {}, + request_headers=await current_request.all_headers(), + **self.adaptor_arguments + )) + except Exception as e: + log.error(f"Error processing redirect: {e}") + break + + current_request = current_request.redirected_from + except Exception as e: + log.error(f"Error processing response history: {e}") + + return history + def fetch(self, url: str) -> Response: """Opens up the browser and do your request based on your chosen options. @@ -407,7 +439,7 @@ class PlaywrightEngine: # PlayWright API sometimes give empty status text for some reason! status_text = final_response.status_text or StatusText.get(final_response.status) - history = self._process_response_history(first_response) + history = await self._async_process_response_history(first_response) try: page_content = await page.content() except Exception as e: From 8f5d66f678e0606d7df6bcc10630fcf608f4f912 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 02:58:10 +0200 Subject: [PATCH 16/19] fix(StealthyFetcher): fix redirection issue with async_fetch --- scrapling/engines/camo.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/scrapling/engines/camo.py b/scrapling/engines/camo.py index a3f1720..10c757c 100644 --- a/scrapling/engines/camo.py +++ b/scrapling/engines/camo.py @@ -130,6 +130,38 @@ class CamoufoxEngine: return history + async def _async_process_response_history(self, first_response): + """Process response history to build a list of Response objects""" + history = [] + current_request = first_response.request.redirected_from + + try: + while current_request: + try: + current_response = await current_request.response() + history.insert(0, Response( + url=current_request.url, + # using current_response.text() will trigger "Error: Response.text: Response body is unavailable for redirect responses" + text='', + body=b'', + status=current_response.status if current_response else 301, + reason=(current_response.status_text or StatusText.get(current_response.status)) if current_response else StatusText.get(301), + encoding=current_response.headers.get('content-type', '') or 'utf-8', + cookies={}, + headers=await current_response.all_headers() if current_response else {}, + request_headers=await current_request.all_headers(), + **self.adaptor_arguments + )) + except Exception as e: + log.error(f"Error processing redirect: {e}") + break + + current_request = current_request.redirected_from + except Exception as e: + log.error(f"Error processing response history: {e}") + + return history + def fetch(self, url: str) -> Response: """Opens up the browser and do your request based on your chosen options. @@ -277,7 +309,7 @@ class CamoufoxEngine: # PlayWright API sometimes give empty status text for some reason! status_text = final_response.status_text or StatusText.get(final_response.status) - history = self._process_response_history(first_response) + history = await self._async_process_response_history(first_response) try: page_content = await page.content() except Exception as e: From a79bf3cc9dd028a46ff46bce3b3f7780b4881aea Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 03:08:48 +0200 Subject: [PATCH 17/19] feat(StealthyFetcher): adding the `wait` option This will make fetcher sleep after fetch before closing the page --- scrapling/engines/camo.py | 6 +++++- scrapling/fetchers.py | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scrapling/engines/camo.py b/scrapling/engines/camo.py index 10c757c..54d9555 100644 --- a/scrapling/engines/camo.py +++ b/scrapling/engines/camo.py @@ -16,7 +16,7 @@ from scrapling.engines.toolbelt import (Response, StatusText, class CamoufoxEngine: def __init__( self, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, - block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, humanize: Union[bool, float] = True, + block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, humanize: Union[bool, float] = True, wait: Optional[int] = 0, timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, addons: Optional[List[str]] = None, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, @@ -39,6 +39,7 @@ class CamoufoxEngine: :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param disable_ads: Disabled by default, this installs `uBlock Origin` addon on the browser if enabled. :param os_randomize: If enabled, Scrapling will randomize the OS fingerprints used. The default is Scrapling matching the fingerprints with the current OS. + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. @@ -67,6 +68,7 @@ class CamoufoxEngine: self.addons = addons or [] self.humanize = humanize self.timeout = check_type_validity(timeout, [int, float], 30000) + self.wait = check_type_validity(wait, [int, float], 0) # Page action callable validation self.page_action = None @@ -213,6 +215,7 @@ class CamoufoxEngine: except Exception as e: log.error(f"Error waiting for selector {self.wait_selector}: {e}") + page.wait_for_timeout(self.wait) # In case we didn't catch a document type somehow final_response = final_response if final_response else first_response if not final_response: @@ -299,6 +302,7 @@ class CamoufoxEngine: except Exception as e: log.error(f"Error waiting for selector {self.wait_selector}: {e}") + await page.wait_for_timeout(self.wait) # In case we didn't catch a document type somehow final_response = final_response if final_response else first_response if not final_response: diff --git a/scrapling/fetchers.py b/scrapling/fetchers.py index 84596cd..ad0e94c 100644 --- a/scrapling/fetchers.py +++ b/scrapling/fetchers.py @@ -231,7 +231,7 @@ class StealthyFetcher(BaseFetcher): @classmethod def fetch( cls, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, - block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, + block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, wait: Optional[int] = 0, timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, @@ -256,7 +256,8 @@ class StealthyFetcher(BaseFetcher): It will also calculate and spoof the browser's language based on the distribution of language speakers in the target region. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param os_randomize: If enabled, Scrapling will randomize the OS fingerprints used. The default is Scrapling matching the fingerprints with the current OS. - :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 + :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. :param wait_selector_state: The state to wait for the selector given with `wait_selector`. Default state is `attached`. @@ -273,6 +274,7 @@ class StealthyFetcher(BaseFetcher): ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") engine = CamoufoxEngine( + wait=wait, proxy=proxy, geoip=geoip, addons=addons, @@ -299,7 +301,7 @@ class StealthyFetcher(BaseFetcher): @classmethod async def async_fetch( cls, url: str, headless: Union[bool, Literal['virtual']] = True, block_images: bool = False, disable_resources: bool = False, - block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, + block_webrtc: bool = False, allow_webgl: bool = True, network_idle: bool = False, addons: Optional[List[str]] = None, wait: Optional[int] = 0, timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, humanize: Optional[Union[bool, float]] = True, wait_selector_state: SelectorWaitStates = 'attached', google_search: bool = True, extra_headers: Optional[Dict[str, str]] = None, proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: bool = False, disable_ads: bool = False, geoip: bool = False, @@ -325,6 +327,7 @@ class StealthyFetcher(BaseFetcher): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param os_randomize: If enabled, Scrapling will randomize the OS fingerprints used. The default is Scrapling matching the fingerprints with the current OS. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000 + :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning `Response` object. :param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. :param wait_selector: Wait for a specific css selector to be in a specific state. :param wait_selector_state: The state to wait for the selector given with `wait_selector`. Default state is `attached`. @@ -341,6 +344,7 @@ class StealthyFetcher(BaseFetcher): ValueError(f"The custom parser config must be of type dictionary, got {cls.__class__}") engine = CamoufoxEngine( + wait=wait, proxy=proxy, geoip=geoip, addons=addons, From 8833a0916e05760922f320ea1fe86080b31c4389 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 06:05:02 +0200 Subject: [PATCH 18/19] build: update docs URL --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 99357cf..bc6c41b 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup( python_requires=">=3.9", url="https://github.com/D4Vinci/Scrapling", project_urls={ - "Documentation": "https://github.com/D4Vinci/Scrapling/tree/main/docs", # For now + "Documentation": "https://scrapling.readthedocs.io/en/latest/", "Source": "https://github.com/D4Vinci/Scrapling", "Tracker": "https://github.com/D4Vinci/Scrapling/issues", } From cf81e4e1a9c05e6f3f479670b1c36535475143a7 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Tue, 8 Apr 2025 06:06:12 +0200 Subject: [PATCH 19/19] docs: Update README file --- README.md | 743 ++++++------------------------------------------------ 1 file changed, 76 insertions(+), 667 deletions(-) diff --git a/README.md b/README.md index 9e3f379..c1ac3b6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,36 @@ # πŸ•·οΈ Scrapling: Undetectable, Lightning-Fast, and Easy Web Scraping with Python -[![Tests](https://github.com/D4Vinci/Scrapling/actions/workflows/tests.yml/badge.svg)](https://github.com/D4Vinci/Scrapling/actions/workflows/tests.yml) [![PyPI version](https://badge.fury.io/py/Scrapling.svg)](https://badge.fury.io/py/Scrapling) [![Supported Python versions](https://img.shields.io/pypi/pyversions/scrapling.svg)](https://pypi.org/project/scrapling/) [![PyPI Downloads](https://static.pepy.tech/badge/scrapling)](https://pepy.tech/project/scrapling) +

+ + Tests + + PyPI version + + PyPI Downloads +
+ + Supported Python versions +

+

+ + Installation + + Β· + + Overview + + Β· + + Selection methods + + Β· + + Choosing a fetcher + + Β· + + Migrating from Beautifulsoup + +

Dealing with failing web scrapers due to anti-bot protections or website changes? Meet Scrapling. @@ -21,7 +52,7 @@ Scrapling is a high-performance, intelligent web scraping library for Python tha [Scrapeless Deep SerpApi](https://www.scrapeless.com/en/product/deep-serp-api?utm_source=website&utm_medium=ads&utm_campaign=scraping&utm_term=d4vinci) From $0.10 per 1,000 queries with a 1-2 second response time! -Deep SerpApi is a dedicated search engine designed for large language models (LLMs) and AI agents, aiming to provide real-time, accurate and unbiased information to help AI applications retrieve and process data efficiently. +Deep SerpApi is a dedicated search engine designed for large language models (LLMs) and AI agents. It aims to provide real-time, accurate, and unbiased information to help AI applications retrieve and process data efficiently. - covering 20+ Google SERP scenarios and mainstream search engines. - support real-time data updates to ensure real-time and accurate information. - It can integrate information from all available online channels and search engines. @@ -50,62 +81,18 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL [![Evomi Banner](https://my.evomi.com/images/brand/cta.png)](https://evomi.com?utm_source=github&utm_medium=banner&utm_campaign=d4vinci-scrapling) --- -## Table of content -* [Key Features](#key-features) - * [Fetch websites as you prefer with async support](#fetch-websites-as-you-prefer-with-async-support) - * [Adaptive Scraping](#adaptive-scraping) - * [High Performance](#high-performance) - * [Developer Friendly](#developer-friendly) -* [Getting Started](#getting-started) -* [Parsing Performance](#parsing-performance) - * [Text Extraction Speed Test (5000 nested elements).](#text-extraction-speed-test-5000-nested-elements) - * [Extraction By Text Speed Test](#extraction-by-text-speed-test) -* [Installation](#installation) -* [Fetching Websites](#fetching-websites) - * [Features](#features) - * [Set parser config per request](#set-parser-config-per-request) - * [Fetcher](#fetcher) - * [StealthyFetcher](#stealthyfetcher) - * [The complete list of arguments](#the-complete-list-of-arguments) - * [PlayWrightFetcher](#playwrightfetcher) - * [The complete list of arguments](#the-complete-list-of-arguments-1) -* [Advanced Parsing Features](#advanced-parsing-features) - * [Smart Navigation](#smart-navigation) - * [Content-based Selection & Finding Similar Elements](#content-based-selection--finding-similar-elements) - * [Handling Structural Changes](#handling-structural-changes) - * [Real-World Scenario](#real-world-scenario) - * [Find elements by filters](#find-elements-by-filters) - * [Is That All?](#is-that-all) -* [More Advanced Usage](#more-advanced-usage) -* [⚑ Enlightening Questions and FAQs](#-enlightening-questions-and-faqs) - * [How does auto-matching work?](#how-does-auto-matching-work) - * [How does the auto-matching work if I didn't pass a URL while initializing the Adaptor object?](#how-does-the-auto-matching-work-if-i-didnt-pass-a-url-while-initializing-the-adaptor-object) - * [If all things about an element can change or get removed, what are the unique properties to be saved?](#if-all-things-about-an-element-can-change-or-get-removed-what-are-the-unique-properties-to-be-saved) - * [I have enabled the `auto_save`/`auto_match` parameter while selecting and it got completely ignored with a warning message](#i-have-enabled-the-auto_saveauto_match-parameter-while-selecting-and-it-got-completely-ignored-with-a-warning-message) - * [I have done everything as the docs but the auto-matching didn't return anything, what's wrong?](#i-have-done-everything-as-the-docs-but-the-auto-matching-didnt-return-anything-whats-wrong) - * [Can Scrapling replace code built on top of BeautifulSoup4?](#can-scrapling-replace-code-built-on-top-of-beautifulsoup4) - * [Can Scrapling replace code built on top of AutoScraper?](#can-scrapling-replace-code-built-on-top-of-autoscraper) - * [Is Scrapling thread-safe?](#is-scrapling-thread-safe) -* [More Sponsors!](#more-sponsors) -* [Contributing](#contributing) -* [Disclaimer for Scrapling Project](#disclaimer-for-scrapling-project) -* [License](#license) -* [Acknowledgments](#acknowledgments) -* [Thanks and References](#thanks-and-references) -* [Known Issues](#known-issues) - ## Key Features ### Fetch websites as you prefer with async support - **HTTP Requests**: Fast and stealthy HTTP requests with the `Fetcher` class. - **Dynamic Loading & Automation**: Fetch dynamic websites with the `PlayWrightFetcher` class through your real browser, Scrapling's stealth mode, Playwright's Chrome browser, or [NSTbrowser](https://app.nstbrowser.io/r/1vO5e5)'s browserless! -- **Anti-bot Protections Bypass**: Easily bypass protections with `StealthyFetcher` and `PlayWrightFetcher` classes. +- **Anti-bot Protections Bypass**: Easily bypass protections with the `StealthyFetcher` and `PlayWrightFetcher` classes. ### Adaptive Scraping -- πŸ”„ **Smart Element Tracking**: Relocate elements after website changes, using an intelligent similarity system and integrated storage. -- 🎯 **Flexible Selection**: CSS selectors, XPath selectors, filters-based search, text search, regex search and more. +- πŸ”„ **Smart Element Tracking**: Relocate elements after website changes using an intelligent similarity system and integrated storage. +- 🎯 **Flexible Selection**: CSS selectors, XPath selectors, filters-based search, text search, regex search, and more. - πŸ” **Find Similar Elements**: Automatically locate elements similar to the element you found! -- 🧠 **Smart Content Scraping**: Extract data from multiple websites without specific selectors using Scrapling powerful features. +- 🧠 **Smart Content Scraping**: Extract data from multiple websites using Scrapling's powerful features without specific selectors. ### High Performance - πŸš€ **Lightning Fast**: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries. @@ -114,7 +101,7 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL ### Developer Friendly - πŸ› οΈ **Powerful Navigation API**: Easy DOM traversal in all directions. -- 🧬 **Rich Text Processing**: All strings have built-in regex, cleaning methods, and more. All elements' attributes are optimized dictionaries that takes less memory than standard dictionaries with added methods. +- 🧬 **Rich Text Processing**: All strings have built-in regex, cleaning methods, and more. All elements' attributes are optimized dictionaries with added methods that consume less memory than standard dictionaries. - πŸ“ **Auto Selectors Generation**: Generate robust short and full CSS/XPath selectors for any element. - πŸ”Œ **Familiar API**: Similar to Scrapy/BeautifulSoup and the same pseudo-elements used in Scrapy. - πŸ“˜ **Type hints**: Complete type/doc-strings coverage for future-proofing and best autocompletion support. @@ -124,12 +111,12 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL ```python from scrapling.fetchers import Fetcher -# Do http GET request to a web page and create an Adaptor instance +# Do HTTP GET request to a web page and create an Adaptor instance page = Fetcher.get('https://quotes.toscrape.com/', stealthy_headers=True) -# Get all text content from all HTML tags in the page except `script` and `style` tags +# Get all text content from all HTML tags in the page except the `script` and `style` tags page.get_all_text(ignore_tags=('script', 'style')) -# Get all quotes elements, any of these methods will return a list of strings directly (TextHandlers) +# Get all quotes elements; any of these methods will return a list of strings directly (TextHandlers) quotes = page.css('.quote .text::text') # CSS selector quotes = page.xpath('//span[@class="text"]/text()') # XPath quotes = page.css('.quote').css('.text::text') # Chained selectors @@ -147,13 +134,16 @@ quotes = page.find_all(['div'], class_='quote') quotes = page.find_all(class_='quote') # and so on... # Working with elements -quote.html_content # Get Inner HTML of this element +quote.html_content # Get the Inner HTML of this element quote.prettify() # Prettified version of Inner HTML above quote.attrib # Get that element's attributes quote.path # DOM path to element (List of all ancestors from tag till the element itself) ``` To keep it simple, all methods can be chained on top of each other! +> [!NOTE] +> Check out the full documentation from [here](https://scrapling.readthedocs.io/en/latest/) + ## Parsing Performance Scrapling isn't just powerful - it's also blazing fast. Scrapling implements many best practices, design patterns, and numerous optimizations to save fractions of seconds. All of that while focusing exclusively on parsing HTML documents. @@ -161,32 +151,45 @@ Here are benchmarks comparing Scrapling to popular Python libraries in two tests ### Text Extraction Speed Test (5000 nested elements). +This test consists of extracting the text content of 5000 nested div elements. + + | # | Library | Time (ms) | vs Scrapling | |---|:-----------------:|:---------:|:------------:| -| 1 | Scrapling | 5.55 | 1.0x | -| 2 | Parsel/Scrapy | 5.67 | 1.022x | -| 3 | Raw Lxml | 6.69 | 1.205x | -| 4 | PyQuery | 20.84 | 3.755x | -| 5 | Selectolax | 84.41 | 15.209x | -| 6 | BS4 with Lxml | 1313.45 | 236.658x | -| 7 | MechanicalSoup | 1313.66 | 236.695x | -| 8 | BS4 with html5lib | 3383.27 | 609.598x | +| 1 | Scrapling | 5.44 | 1.0x | +| 2 | Parsel/Scrapy | 5.53 | 1.017x | +| 3 | Raw Lxml | 6.76 | 1.243x | +| 4 | PyQuery | 21.96 | 4.037x | +| 5 | Selectolax | 67.12 | 12.338x | +| 6 | BS4 with Lxml | 1307.03 | 240.263x | +| 7 | MechanicalSoup | 1322.64 | 243.132x | +| 8 | BS4 with html5lib | 3373.75 | 620.175x | -As you see, Scrapling is on par with Scrapy and slightly faster than Lxml which both libraries are built on top of. These are the closest results to Scrapling. PyQuery is also built on top of Lxml but still, Scrapling is ~4 times faster. +As you see, Scrapling is on par with Scrapy and slightly faster than Lxml, which both libraries are built on top of. These are the closest results to Scrapling. PyQuery is also built on top of Lxml, but Scrapling is four times faster. ### Extraction By Text Speed Test -| Library | Time (ms) | vs Scrapling | -|:-----------:|:---------:|:------------:| -| Scrapling | 2.35 | 1.0x | -| AutoScraper | 11.44 | 4.868x | +Scrapling can find elements based on its text content and find elements similar to these elements. The only known library with these two features, too, is AutoScraper. -Scrapling can find elements with more methods and it returns full element `Adaptor` objects not only the text like AutoScraper. So, to make this test fair, both libraries will extract an element with text, find similar elements, and then extract the text content for all of them. As you see, Scrapling is still 4.8 times faster at the same task. +So, we compared this to see how fast Scrapling can be in these two tasks compared to AutoScraper. + +Here are the results: + +| Library | Time (ms) | vs Scrapling | +|-------------|:---------:|:------------:| +| Scrapling | 2.51 | 1.0x | +| AutoScraper | 11.41 | 4.546x | + +Scrapling can find elements with more methods and returns the entire element's `Adaptor` object, not only text like AutoScraper. So, to make this test fair, both libraries will extract an element with text, find similar elements, and then extract the text content for all of them. + +As you see, Scrapling is still 4.5 times faster at the same task. + +If we made Scrapling extract the elements only without stopping to extract each element's text, we would get speed twice as fast as this, but as I said, to make it fair comparison a bit :smile: > All benchmarks' results are an average of 100 runs. See our [benchmarks.py](https://github.com/D4Vinci/Scrapling/blob/main/benchmarks.py) for methodology and to run your comparisons. ## Installation -Scrapling is a breeze to get started with; Starting from version 0.2.9, we require at least Python 3.9 to work. +Scrapling is a breeze to get started with. Starting from version 0.2.9, we require at least Python 3.9 to work. ```bash pip3 install scrapling ``` @@ -196,600 +199,6 @@ scrapling install ``` If you have any installation issues, please open an issue. -## Fetching Websites -Fetchers are interfaces built on top of other libraries with added features that do requests or fetch pages for you in a single request fashion and then return an `Adaptor` object. This feature was introduced because the only option we had before was to fetch the page as you wanted it, then pass it manually to the `Adaptor` class to create an `Adaptor` instance and start playing around with the page. - -### Features -You might be slightly confused by now so let me clear things up. All fetcher-type classes are imported in the same way -```python ->>> from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, PlayWrightFetcher -``` -then use it right away without initializing like this and it will use the default parser settings: -```python ->>> page = StealthyFetcher.fetch('https://example.com') -``` -If you want to configure the parser (Adaptor class) that will be used on the response before returning it for you then do this first: -```python ->>> StealthyFetcher.configure(auto_match=True, keep_comments=False) -``` -or -```python ->>> StealthyFetcher.auto_match = True ->>> StealthyFetcher.keep_comments = False -``` -Then continue your code as normal. - -The available configuration arguments are: `auto_match`, `huge_tree`, `keep_comments`, `keep_cdata`, `storage`, and `storage_args`, which are the same ones you give to the `Adaptor` class. You can display the current configuration anytime by running `.display_config()` - -Also, the `Response` object returned from all fetchers is the same as the `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, `history`, and `request_headers`. All `cookies`, `headers`, and `request_headers` are always of type `dictionary`. -> [!NOTE] -> The `auto_match` argument is disabled by default, you need to enable it to use that feature. - -#### Set parser config per request -As you probably understood, the logic above for setting the parser config will work globally for all requests/fetches done through that class and it's intended. - -If your use case requires you to use different config for each request/fetch, then you can pass a dictionary to the request method (`fetch`/`get`/`post`/...) to an argument named `custom_config`. - -### Fetcher -This class is built on top of [httpx](https://www.python-httpx.org/) with additional configuration options, here you can do `GET`, `POST`, `PUT`, and `DELETE` requests. - -For all methods, you have `stealthy_headers` which makes `Fetcher` create and use real browser's headers then create a referer header as if this request came from Google's search of this URL's domain. It's enabled by default. You can also set the number of retries with the argument `retries` for all methods and this will make httpx retry requests if it failed for any reason. The default number of retries for all `Fetcher` methods is 3. - -> Hence: All headers generated by `stealthy_headers` argument can be overwritten by you through the `headers` argument - -You can route all traffic (HTTP and HTTPS) to a proxy for any of these methods in this format `http://username:password@localhost:8030` -```python ->> page = Fetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) ->> page = Fetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') ->> page = Fetcher.put('https://httpbin.org/put', data={'key': 'value'}) ->> page = Fetcher.delete('https://httpbin.org/delete') -``` -For Async requests, you will just replace the import like below: -```python ->> from scrapling.fetchers import AsyncFetcher ->> page = await AsyncFetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) ->> page = await AsyncFetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') ->> page = await AsyncFetcher.put('https://httpbin.org/put', data={'key': 'value'}) ->> page = await AsyncFetcher.delete('https://httpbin.org/delete') -``` -### StealthyFetcher -This class is built on top of [Camoufox](https://github.com/daijro/camoufox), bypassing most anti-bot protections by default. Scrapling adds extra layers of flavors and configurations to increase performance and undetectability even further. -```python ->> page = StealthyFetcher.fetch('https://www.browserscan.net/bot-detection') # Running headless by default ->> page.status == 200 -True ->> page = await StealthyFetcher.async_fetch('https://www.browserscan.net/bot-detection') # the async version of fetch ->> page.status == 200 -True -``` -> Note: all requests done by this fetcher are waiting by default for all JS to be fully loaded and executed so you don't have to :) - -#### The complete list of arguments - -| Argument | Description | Optional | -|:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| -| url | Target url | ❌ | -| headless | Pass `True` to run the browser in headless/hidden (**default**), `virtual` to run it in virtual screen mode, or `False` for headful/visible mode. The `virtual` mode requires having `xvfb` installed. | βœ”οΈ | -| block_images | Prevent the loading of images through Firefox preferences. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | βœ”οΈ | -| disable_resources | Drop requests of unnecessary resources for a speed boost. It depends but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | βœ”οΈ | -| google_search | Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. | βœ”οΈ | -| extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | βœ”οΈ | -| block_webrtc | Blocks WebRTC entirely. | βœ”οΈ | -| page_action | Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. | βœ”οΈ | -| addons | List of Firefox addons to use. **Must be paths to extracted addons.** | βœ”οΈ | -| humanize | Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. | βœ”οΈ | -| allow_webgl | Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled. | βœ”οΈ | -| geoip | Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address. It will also calculate and spoof the browser's language based on the distribution of language speakers in the target region. | βœ”οΈ | -| disable_ads | Disabled by default, this installs `uBlock Origin` addon on the browser if enabled. | βœ”οΈ | -| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | -| timeout | The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. | βœ”οΈ | -| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | -| proxy | The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | -| os_randomize | If enabled, Scrapling will randomize the OS fingerprints used. The default is Scrapling matching the fingerprints with the current OS. | βœ”οΈ | -| wait_selector_state | The state to wait for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | - - -This list isn't final so expect a lot more additions and flexibility to be added in the next versions! - -### PlayWrightFetcher -This class is built on top of [Playwright](https://playwright.dev/python/) which currently provides 4 main run options but they can be mixed as you want. -```python ->> page = PlayWrightFetcher.fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # Vanilla Playwright option ->> page.css_first("#search a::attr(href)") -'https://github.com/D4Vinci/Scrapling' ->> page = await PlayWrightFetcher.async_fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # the async version of fetch ->> page.css_first("#search a::attr(href)") -'https://github.com/D4Vinci/Scrapling' -``` -> Note: all requests done by this fetcher are waiting by default for all JS to be fully loaded and executed so you don't have to :) - -Using this Fetcher class, you can make requests with: - 1) Vanilla Playwright without any modifications other than the ones you chose. - 2) Stealthy Playwright with the stealth mode I wrote for it. It's still a WIP but it bypasses many online tests like [Sannysoft's](https://bot.sannysoft.com/).
Some of the things this fetcher's stealth mode does include: - * Patching the CDP runtime fingerprint. - * Mimics some of the real browsers' properties by injecting several JS files and using custom options. - * Using custom flags on launch to hide Playwright even more and make it faster. - * Generates real browser's headers of the same type and same user OS then append it to the request's headers. - 3) Real browsers by passing the `real_chrome` argument or the CDP URL of your browser to be controlled by the Fetcher and most of the options can be enabled on it. - 4) [NSTBrowser](https://app.nstbrowser.io/r/1vO5e5)'s [docker browserless](https://hub.docker.com/r/nstbrowser/browserless) option by passing the CDP URL and enabling `nstbrowser_mode` option. - -> Hence using the `real_chrome` argument requires that you have Chrome browser installed on your device - -Add that to a lot of controlling/hiding options as you will see in the arguments list below. - -#### The complete list of arguments - -| Argument | Description | Optional | -|:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| -| url | Target url | ❌ | -| headless | Pass `True` to run the browser in headless/hidden (**default**), or `False` for headful/visible mode. | βœ”οΈ | -| disable_resources | Drop requests of unnecessary resources for a speed boost. It depends but it made requests ~25% faster in my tests for some websites.
Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | βœ”οΈ | -| useragent | Pass a useragent string to be used. **Otherwise the fetcher will generate a real Useragent of the same browser and use it.** | βœ”οΈ | -| network_idle | Wait for the page until there are no network connections for at least 500 ms. | βœ”οΈ | -| timeout | The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. | βœ”οΈ | -| page_action | Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. | βœ”οΈ | -| wait_selector | Wait for a specific css selector to be in a specific state. | βœ”οΈ | -| wait_selector_state | The state to wait for the selector given with `wait_selector`. _Default state is `attached`._ | βœ”οΈ | -| google_search | Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. | βœ”οΈ | -| extra_headers | A dictionary of extra headers to add to the request. The referer set by the `google_search` argument takes priority over the referer set here if used together. | βœ”οΈ | -| proxy | The proxy to be used with requests, it can be a string or a dictionary with the keys 'server', 'username', and 'password' only. | βœ”οΈ | -| hide_canvas | Add random noise to canvas operations to prevent fingerprinting. | βœ”οΈ | -| disable_webgl | Disables WebGL and WebGL 2.0 support entirely. | βœ”οΈ | -| stealth | Enables stealth mode, always check the documentation to see what stealth mode does currently. | βœ”οΈ | -| real_chrome | If you have Chrome browser installed on your device, enable this and the Fetcher will launch an instance of your browser and use it. | βœ”οΈ | -| locale | Set the locale for the browser if wanted. The default value is `en-US`. | βœ”οΈ | -| cdp_url | Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. | βœ”οΈ | -| nstbrowser_mode | Enables NSTBrowser mode, **it have to be used with `cdp_url` argument or it will get completely ignored.** | βœ”οΈ | -| nstbrowser_config | The config you want to send with requests to the NSTBrowser. _If left empty, Scrapling defaults to an optimized NSTBrowser's docker browserless config._ | βœ”οΈ | - - -This list isn't final so expect a lot more additions and flexibility to be added in the next versions! - -## Advanced Parsing Features -### Smart Navigation -```python ->>> quote.tag -'div' - ->>> quote.parent -
...'> - ->>> quote.parent.tag -'div' - ->>> quote.children -[β€œThe...' parent='
, - Tags: