Merge pull request #51 from D4Vinci/dev

v0.2.98
This commit is contained in:
Karim shoair
2025-03-17 15:24:09 +02:00
committed by GitHub
15 changed files with 82 additions and 47 deletions
+6 -6
View File
@@ -118,7 +118,7 @@ Deep SerpApi is a dedicated search engine designed for large language models (LL
## Getting Started ## Getting Started
```python ```python
from scrapling import Fetcher from scrapling.fetchers import Fetcher
fetcher = Fetcher(auto_match=False) fetcher = Fetcher(auto_match=False)
@@ -200,7 +200,7 @@ Fetchers are interfaces built on top of other libraries with added features that
### Features ### Features
You might be slightly confused by now so let me clear things up. All fetcher-type classes are imported in the same way You might be slightly confused by now so let me clear things up. All fetcher-type classes are imported in the same way
```python ```python
from scrapling import Fetcher, StealthyFetcher, PlayWrightFetcher from scrapling.fetchers import Fetcher, 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. 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.
@@ -232,7 +232,7 @@ You can route all traffic (HTTP and HTTPS) to a proxy for any of these methods i
``` ```
For Async requests, you will just replace the import like below: For Async requests, you will just replace the import like below:
```python ```python
>> from scrapling import AsyncFetcher >> from scrapling.fetchers import AsyncFetcher
>> page = await AsyncFetcher().get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) >> 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().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().put('https://httpbin.org/put', data={'key': 'value'})
@@ -486,7 +486,7 @@ When website owners implement structural changes like
The selector will no longer function and your code needs maintenance. That's where Scrapling's auto-matching feature comes into play. The selector will no longer function and your code needs maintenance. That's where Scrapling's auto-matching feature comes into play.
```python ```python
from scrapling import Adaptor from scrapling.parser import Adaptor
# Before the change # Before the change
page = Adaptor(page_source, url='example.com') page = Adaptor(page_source, url='example.com')
element = page.css('#p1' auto_save=True) element = page.css('#p1' auto_save=True)
@@ -504,7 +504,7 @@ To solve this issue, I will use [The Web Archive](https://archive.org/)'s [Wayba
If I want to extract the Questions button from the old design I can use a selector like this `#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a` This selector is too specific because it was generated by Google Chrome. If I want to extract the Questions button from the old design I can use a selector like this `#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a` This selector is too specific because it was generated by Google Chrome.
Now let's test the same selector in both versions Now let's test the same selector in both versions
```python ```python
>> from scrapling import Fetcher >> from scrapling.fetchers import Fetcher
>> selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a' >> selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a'
>> old_url = "https://web.archive.org/web/20100102003420/http://stackoverflow.com/" >> old_url = "https://web.archive.org/web/20100102003420/http://stackoverflow.com/"
>> new_url = "https://stackoverflow.com/" >> new_url = "https://stackoverflow.com/"
@@ -565,7 +565,7 @@ Note: The filtering process always starts from the first filter it finds in the
Examples to clear any confusion :) Examples to clear any confusion :)
```python ```python
>> from scrapling import Fetcher >> 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`. # Find all elements with tag name `div`.
>> page.find_all('div') >> page.find_all('div')
+1 -1
View File
@@ -2,7 +2,7 @@
### All current types can be imported alone like below ### All current types can be imported alone like below
```python ```python
>>> from scrapling import TextHandler, AttributesHandler >>> from scrapling.core.custom_types import TextHandler, AttributesHandler
>>> somestring = TextHandler('{}') >>> somestring = TextHandler('{}')
>>> somestring.json() >>> somestring.json()
+35 -6
View File
@@ -1,12 +1,41 @@
# Declare top-level shortcuts
from scrapling.core.custom_types import AttributesHandler, TextHandler
from scrapling.fetchers import (AsyncFetcher, CustomFetcher, Fetcher,
PlayWrightFetcher, StealthyFetcher)
from scrapling.parser import Adaptor, Adaptors
__author__ = "Karim Shoair (karim.shoair@pm.me)" __author__ = "Karim Shoair (karim.shoair@pm.me)"
__version__ = "0.2.97" __version__ = "0.2.98"
__copyright__ = "Copyright (c) 2024 Karim Shoair" __copyright__ = "Copyright (c) 2024 Karim Shoair"
# 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
elif name == 'Adaptor':
from scrapling.parser import Adaptor as cls
return cls
elif name == 'Adaptors':
from scrapling.parser import Adaptors as cls
return cls
elif name == 'AttributesHandler':
from scrapling.core.custom_types import AttributesHandler as cls
return cls
elif name == 'TextHandler':
from scrapling.core.custom_types import TextHandler as cls
return cls
elif name == 'AsyncFetcher':
from scrapling.fetchers import AsyncFetcher as cls
return cls
elif name == 'StealthyFetcher':
from scrapling.fetchers import StealthyFetcher as cls
return cls
elif name == 'PlayWrightFetcher':
from scrapling.fetchers import PlayWrightFetcher as cls
return cls
elif name == 'CustomFetcher':
from scrapling.fetchers import CustomFetcher as cls
return cls
else:
raise AttributeError(f"module 'scrapling' has no attribute '{name}'")
__all__ = ['Adaptor', 'Fetcher', 'AsyncFetcher', 'StealthyFetcher', 'PlayWrightFetcher'] __all__ = ['Adaptor', 'Fetcher', 'AsyncFetcher', 'StealthyFetcher', 'PlayWrightFetcher']
+3 -3
View File
@@ -19,7 +19,7 @@ class StorageSystemMixin(ABC):
""" """
self.url = url self.url = url
@lru_cache(126, typed=True) @lru_cache(64, typed=True)
def _get_base_url(self, default_value: str = 'default') -> str: def _get_base_url(self, default_value: str = 'default') -> str:
if not self.url or type(self.url) is not str: if not self.url or type(self.url) is not str:
return default_value return default_value
@@ -51,7 +51,7 @@ class StorageSystemMixin(ABC):
raise NotImplementedError('Storage system must implement `save` method') raise NotImplementedError('Storage system must implement `save` method')
@staticmethod @staticmethod
@lru_cache(256, typed=True) @lru_cache(128, typed=True)
def _get_hash(identifier: str) -> str: def _get_hash(identifier: str) -> str:
"""If you want to hash identifier in your storage system, use this safer""" """If you want to hash identifier in your storage system, use this safer"""
identifier = identifier.lower().strip() identifier = identifier.lower().strip()
@@ -63,7 +63,7 @@ class StorageSystemMixin(ABC):
return f"{hash_value}_{len(identifier)}" # Length to reduce collision chance return f"{hash_value}_{len(identifier)}" # Length to reduce collision chance
@lru_cache(10, typed=True) @lru_cache(1, typed=True)
class SQLiteStorageSystem(StorageSystemMixin): class SQLiteStorageSystem(StorageSystemMixin):
"""The recommended system to use, it's race condition safe and thread safe. """The recommended system to use, it's race condition safe and thread safe.
Mainly built so the library can run in threaded frameworks like scrapy or threaded tools Mainly built so the library can run in threaded frameworks like scrapy or threaded tools
+3
View File
@@ -142,3 +142,6 @@ class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
@lru_cache(maxsize=256) @lru_cache(maxsize=256)
def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str: def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str:
return super().css_to_xpath(css, prefix) return super().css_to_xpath(css, prefix)
translator_instance = HTMLTranslator()
+1 -1
View File
@@ -115,7 +115,7 @@ class _StorageTools:
# return _impl # return _impl
@lru_cache(256, typed=True) @lru_cache(128, typed=True)
def clean_spaces(string): def clean_spaces(string):
string = string.replace('\t', ' ') string = string.replace('\t', ' ')
string = re.sub('[\n|\r]', '', string) string = re.sub('[\n|\r]', '', string)
+18 -9
View File
@@ -1,10 +1,19 @@
from .fetchers import AsyncFetcher as _AsyncFetcher
from .fetchers import Fetcher as _Fetcher
from .fetchers import PlayWrightFetcher as _PlayWrightFetcher
from .fetchers import StealthyFetcher as _StealthyFetcher
# If you are going to use Fetchers with the default settings, import them from this file instead for a cleaner looking code # If you are going to use Fetchers with the default settings, import them from this file instead for a cleaner looking code
Fetcher = _Fetcher()
AsyncFetcher = _AsyncFetcher() # A lightweight approach to create lazy loader for each import for backward compatibility
StealthyFetcher = _StealthyFetcher() # This will reduces initial memory footprint significantly (only loads what's used)
PlayWrightFetcher = _PlayWrightFetcher() def __getattr__(name):
if name == 'Fetcher':
from scrapling.fetchers import Fetcher as cls
return cls()
elif name == 'AsyncFetcher':
from scrapling.fetchers import AsyncFetcher as cls
return cls()
elif name == 'StealthyFetcher':
from scrapling.fetchers import StealthyFetcher as cls
return cls()
elif name == 'PlayWrightFetcher':
from scrapling.fetchers import PlayWrightFetcher as cls
return cls()
else:
raise AttributeError(f"module 'scrapling' has no attribute '{name}'")
+2 -2
View File
@@ -126,7 +126,7 @@ class PlaywrightEngine:
return cdp_url return cdp_url
@lru_cache(126, typed=True) @lru_cache(32, typed=True)
def __set_flags(self): def __set_flags(self):
"""Returns the flags that will be used while launching the browser if stealth mode is enabled""" """Returns the flags that will be used while launching the browser if stealth mode is enabled"""
flags = DEFAULT_STEALTH_FLAGS flags = DEFAULT_STEALTH_FLAGS
@@ -169,7 +169,7 @@ class PlaywrightEngine:
return context_kwargs return context_kwargs
@lru_cache(10) @lru_cache(1)
def __stealth_scripts(self): def __stealth_scripts(self):
# Basic bypasses nothing fancy as I'm still working on it # Basic bypasses nothing fancy as I'm still working on it
# But with adding these bypasses to the above config, it bypasses many online tests like # But with adding these bypasses to the above config, it bypasses many online tests like
+1 -1
View File
@@ -7,7 +7,7 @@ from scrapling.core.utils import log, lru_cache
from .toolbelt import Response, generate_convincing_referer, generate_headers from .toolbelt import Response, generate_convincing_referer, generate_headers
@lru_cache(5, typed=True) # Singleton easily @lru_cache(2, typed=True) # Singleton easily
class StaticEngine: class StaticEngine:
def __init__( def __init__(
self, url: str, proxy: Optional[str] = None, stealthy_headers: bool = True, follow_redirects: bool = True, self, url: str, proxy: Optional[str] = None, stealthy_headers: bool = True, follow_redirects: bool = True,
+2 -2
View File
@@ -16,7 +16,7 @@ class ResponseEncoding:
__ISO_8859_1_CONTENT_TYPES = {"text/plain", "text/html", "text/css", "text/javascript"} __ISO_8859_1_CONTENT_TYPES = {"text/plain", "text/html", "text/css", "text/javascript"}
@classmethod @classmethod
@lru_cache(maxsize=256) @lru_cache(maxsize=128)
def __parse_content_type(cls, header_value: str) -> Tuple[str, Dict[str, str]]: def __parse_content_type(cls, header_value: str) -> Tuple[str, Dict[str, str]]:
"""Parse content type and parameters from a content-type header value. """Parse content type and parameters from a content-type header value.
@@ -38,7 +38,7 @@ class ResponseEncoding:
return content_type, params return content_type, params
@classmethod @classmethod
@lru_cache(maxsize=256) @lru_cache(maxsize=128)
def get_value(cls, content_type: Optional[str], text: Optional[str] = 'test') -> str: def get_value(cls, content_type: Optional[str], text: Optional[str] = 'test') -> str:
"""Determine the appropriate character encoding from a content-type header. """Determine the appropriate character encoding from a content-type header.
+2 -2
View File
@@ -12,7 +12,7 @@ from scrapling.core._types import Dict, Union
from scrapling.core.utils import lru_cache from scrapling.core.utils import lru_cache
@lru_cache(128, typed=True) @lru_cache(10, typed=True)
def generate_convincing_referer(url: str) -> str: def generate_convincing_referer(url: str) -> str:
"""Takes the domain from the URL without the subdomain/suffix and make it look like you were searching google for this website """Takes the domain from the URL without the subdomain/suffix and make it look like you were searching google for this website
@@ -26,7 +26,7 @@ def generate_convincing_referer(url: str) -> str:
return f'https://www.google.com/search?q={website_name}' return f'https://www.google.com/search?q={website_name}'
@lru_cache(128, typed=True) @lru_cache(1, typed=True)
def get_os_name() -> Union[str, None]: def get_os_name() -> Union[str, None]:
"""Get the current OS name in the same format needed for browserforge """Get the current OS name in the same format needed for browserforge
+1 -1
View File
@@ -110,7 +110,7 @@ def construct_cdp_url(cdp_url: str, query_params: Optional[Dict] = None) -> str:
raise ValueError(f"Invalid CDP URL: {str(e)}") raise ValueError(f"Invalid CDP URL: {str(e)}")
@lru_cache(126, typed=True) @lru_cache(10, typed=True)
def js_bypass_path(filename: str) -> str: def js_bypass_path(filename: str) -> str:
"""Takes the base filename of JS file inside the `bypasses` folder then return the full path of it """Takes the base filename of JS file inside the `bypasses` folder then return the full path of it
+5 -11
View File
@@ -17,7 +17,7 @@ from scrapling.core.custom_types import (AttributesHandler, TextHandler,
from scrapling.core.mixins import SelectorsGeneration from scrapling.core.mixins import SelectorsGeneration
from scrapling.core.storage_adaptors import (SQLiteStorageSystem, from scrapling.core.storage_adaptors import (SQLiteStorageSystem,
StorageSystemMixin, _StorageTools) StorageSystemMixin, _StorageTools)
from scrapling.core.translator import HTMLTranslator from scrapling.core.translator import translator_instance
from scrapling.core.utils import (clean_spaces, flatten, html_forbidden, from scrapling.core.utils import (clean_spaces, flatten, html_forbidden,
is_jsonable, log) is_jsonable, log)
@@ -26,7 +26,7 @@ class Adaptor(SelectorsGeneration):
__slots__ = ( __slots__ = (
'url', 'encoding', '__auto_match_enabled', '_root', '_storage', 'url', 'encoding', '__auto_match_enabled', '_root', '_storage',
'__keep_comments', '__huge_tree_enabled', '__attributes', '__text', '__tag', '__keep_comments', '__huge_tree_enabled', '__attributes', '__text', '__tag',
'__keep_cdata', '__raw_body' '__keep_cdata'
) )
def __init__( def __init__(
@@ -72,20 +72,17 @@ class Adaptor(SelectorsGeneration):
raise ValueError("Adaptor class needs text, body, or root arguments to work") raise ValueError("Adaptor class needs text, body, or root arguments to work")
self.__text = '' self.__text = ''
self.__raw_body = ''
if root is None: if root is None:
if text is None: if text is None:
if not body or not isinstance(body, bytes): if not body or not isinstance(body, bytes):
raise TypeError(f"body argument must be valid and of type bytes, got {body.__class__}") raise TypeError(f"body argument must be valid and of type bytes, got {body.__class__}")
body = body.replace(b"\x00", b"").strip() body = body.replace(b"\x00", b"").strip()
self.__raw_body = body.replace(b"\x00", b"").strip().decode()
else: else:
if not isinstance(text, str): if not isinstance(text, str):
raise TypeError(f"text argument must be of type str, got {text.__class__}") raise TypeError(f"text argument must be of type str, got {text.__class__}")
body = text.strip().replace("\x00", "").encode(encoding) or b"<html/>" body = text.strip().replace("\x00", "").encode(encoding) or b"<html/>"
self.__raw_body = text.strip()
# https://lxml.de/api/lxml.etree.HTMLParser-class.html # https://lxml.de/api/lxml.etree.HTMLParser-class.html
parser = html.HTMLParser( parser = html.HTMLParser(
@@ -250,10 +247,7 @@ class Adaptor(SelectorsGeneration):
"""Return the inner html code of the element""" """Return the inner html code of the element"""
return TextHandler(etree.tostring(self._root, encoding='unicode', method='html', with_tail=False)) return TextHandler(etree.tostring(self._root, encoding='unicode', method='html', with_tail=False))
@property body = html_content
def body(self) -> TextHandler:
"""Return raw HTML code of the element/page without any processing when possible or return `Adaptor.html_content`"""
return TextHandler(self.__raw_body) or self.html_content
def prettify(self) -> TextHandler: def prettify(self) -> TextHandler:
"""Return a prettified version of the element's inner html-code""" """Return a prettified version of the element's inner html-code"""
@@ -476,7 +470,7 @@ class Adaptor(SelectorsGeneration):
try: try:
if not self.__auto_match_enabled or ',' not in selector: if not self.__auto_match_enabled or ',' not in selector:
# No need to split selectors in this case, let's save some CPU cycles :) # No need to split selectors in this case, let's save some CPU cycles :)
xpath_selector = HTMLTranslator().css_to_xpath(selector) xpath_selector = translator_instance.css_to_xpath(selector)
return self.xpath(xpath_selector, identifier or selector, auto_match, auto_save, percentage) return self.xpath(xpath_selector, identifier or selector, auto_match, auto_save, percentage)
results = [] results = []
@@ -484,7 +478,7 @@ class Adaptor(SelectorsGeneration):
for single_selector in split_selectors(selector): for single_selector in split_selectors(selector):
# I'm doing this only so the `save` function save data correctly for combined selectors # I'm doing this only so the `save` function save data correctly for combined selectors
# Like using the ',' to combine two different selectors that point to different elements. # Like using the ',' to combine two different selectors that point to different elements.
xpath_selector = HTMLTranslator().css_to_xpath(single_selector.canonical()) xpath_selector = translator_instance.css_to_xpath(single_selector.canonical())
results += self.xpath( results += self.xpath(
xpath_selector, identifier or single_selector.canonical(), auto_match, auto_save, percentage xpath_selector, identifier or single_selector.canonical(), auto_match, auto_save, percentage
) )
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata] [metadata]
name = scrapling name = scrapling
version = 0.2.97 version = 0.2.98
author = Karim Shoair author = Karim Shoair
author_email = karim.shoair@pm.me author_email = karim.shoair@pm.me
description = Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy again! description = Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy again!
+1 -1
View File
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setup( setup(
name="scrapling", name="scrapling",
version="0.2.97", version="0.2.98",
description="""Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy again! In an internet filled with complications, 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.""", it simplifies web scraping, even when websites' design changes, while providing impressive speed that surpasses almost all alternatives.""",
long_description=long_description, long_description=long_description,