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
```python
from scrapling import Fetcher
from scrapling.fetchers import Fetcher
fetcher = Fetcher(auto_match=False)
@@ -200,7 +200,7 @@ 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 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.
@@ -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:
```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().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'})
@@ -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.
```python
from scrapling import Adaptor
from scrapling.parser import Adaptor
# Before the change
page = Adaptor(page_source, url='example.com')
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.
Now let's test the same selector in both versions
```python
>> from scrapling import Fetcher
>> from scrapling.fetchers import Fetcher
>> 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/"
@@ -565,7 +565,7 @@ Note: The filtering process always starts from the first filter it finds in the
Examples to clear any confusion :)
```python
>> from scrapling import Fetcher
>> from scrapling.fetchers import Fetcher
>> page = Fetcher().get('https://quotes.toscrape.com/')
# Find all elements with tag name `div`.
>> page.find_all('div')
+1 -1
View File
@@ -2,7 +2,7 @@
### All current types can be imported alone like below
```python
>>> from scrapling import TextHandler, AttributesHandler
>>> from scrapling.core.custom_types import TextHandler, AttributesHandler
>>> somestring = TextHandler('{}')
>>> 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)"
__version__ = "0.2.97"
__version__ = "0.2.98"
__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']
+3 -3
View File
@@ -19,7 +19,7 @@ class StorageSystemMixin(ABC):
"""
self.url = url
@lru_cache(126, typed=True)
@lru_cache(64, typed=True)
def _get_base_url(self, default_value: str = 'default') -> str:
if not self.url or type(self.url) is not str:
return default_value
@@ -51,7 +51,7 @@ class StorageSystemMixin(ABC):
raise NotImplementedError('Storage system must implement `save` method')
@staticmethod
@lru_cache(256, typed=True)
@lru_cache(128, typed=True)
def _get_hash(identifier: str) -> str:
"""If you want to hash identifier in your storage system, use this safer"""
identifier = identifier.lower().strip()
@@ -63,7 +63,7 @@ class StorageSystemMixin(ABC):
return f"{hash_value}_{len(identifier)}" # Length to reduce collision chance
@lru_cache(10, typed=True)
@lru_cache(1, typed=True)
class SQLiteStorageSystem(StorageSystemMixin):
"""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
+3
View File
@@ -142,3 +142,6 @@ class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
@lru_cache(maxsize=256)
def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str:
return super().css_to_xpath(css, prefix)
translator_instance = HTMLTranslator()
+1 -1
View File
@@ -115,7 +115,7 @@ class _StorageTools:
# return _impl
@lru_cache(256, typed=True)
@lru_cache(128, typed=True)
def clean_spaces(string):
string = string.replace('\t', ' ')
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
Fetcher = _Fetcher()
AsyncFetcher = _AsyncFetcher()
StealthyFetcher = _StealthyFetcher()
PlayWrightFetcher = _PlayWrightFetcher()
# 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 == '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
@lru_cache(126, typed=True)
@lru_cache(32, typed=True)
def __set_flags(self):
"""Returns the flags that will be used while launching the browser if stealth mode is enabled"""
flags = DEFAULT_STEALTH_FLAGS
@@ -169,7 +169,7 @@ class PlaywrightEngine:
return context_kwargs
@lru_cache(10)
@lru_cache(1)
def __stealth_scripts(self):
# 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
+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
@lru_cache(5, typed=True) # Singleton easily
@lru_cache(2, typed=True) # Singleton easily
class StaticEngine:
def __init__(
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"}
@classmethod
@lru_cache(maxsize=256)
@lru_cache(maxsize=128)
def __parse_content_type(cls, header_value: str) -> Tuple[str, Dict[str, str]]:
"""Parse content type and parameters from a content-type header value.
@@ -38,7 +38,7 @@ class ResponseEncoding:
return content_type, params
@classmethod
@lru_cache(maxsize=256)
@lru_cache(maxsize=128)
def get_value(cls, content_type: Optional[str], text: Optional[str] = 'test') -> str:
"""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
@lru_cache(128, typed=True)
@lru_cache(10, typed=True)
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
@@ -26,7 +26,7 @@ def generate_convincing_referer(url: str) -> str:
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]:
"""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)}")
@lru_cache(126, typed=True)
@lru_cache(10, typed=True)
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
+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.storage_adaptors import (SQLiteStorageSystem,
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,
is_jsonable, log)
@@ -26,7 +26,7 @@ class Adaptor(SelectorsGeneration):
__slots__ = (
'url', 'encoding', '__auto_match_enabled', '_root', '_storage',
'__keep_comments', '__huge_tree_enabled', '__attributes', '__text', '__tag',
'__keep_cdata', '__raw_body'
'__keep_cdata'
)
def __init__(
@@ -72,20 +72,17 @@ class Adaptor(SelectorsGeneration):
raise ValueError("Adaptor class needs text, body, or root arguments to work")
self.__text = ''
self.__raw_body = ''
if root is None:
if text is None:
if not body or not isinstance(body, bytes):
raise TypeError(f"body argument must be valid and of type bytes, got {body.__class__}")
body = body.replace(b"\x00", b"").strip()
self.__raw_body = body.replace(b"\x00", b"").strip().decode()
else:
if not isinstance(text, str):
raise TypeError(f"text argument must be of type str, got {text.__class__}")
body = text.strip().replace("\x00", "").encode(encoding) or b"<html/>"
self.__raw_body = text.strip()
# https://lxml.de/api/lxml.etree.HTMLParser-class.html
parser = html.HTMLParser(
@@ -250,10 +247,7 @@ class Adaptor(SelectorsGeneration):
"""Return the inner html code of the element"""
return TextHandler(etree.tostring(self._root, encoding='unicode', method='html', with_tail=False))
@property
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
body = html_content
def prettify(self) -> TextHandler:
"""Return a prettified version of the element's inner html-code"""
@@ -476,7 +470,7 @@ class Adaptor(SelectorsGeneration):
try:
if not self.__auto_match_enabled or ',' not in selector:
# 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)
results = []
@@ -484,7 +478,7 @@ class Adaptor(SelectorsGeneration):
for single_selector in split_selectors(selector):
# 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.
xpath_selector = HTMLTranslator().css_to_xpath(single_selector.canonical())
xpath_selector = translator_instance.css_to_xpath(single_selector.canonical())
results += self.xpath(
xpath_selector, identifier or single_selector.canonical(), auto_match, auto_save, percentage
)
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = scrapling
version = 0.2.97
version = 0.2.98
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!
+1 -1
View File
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setup(
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,
it simplifies web scraping, even when websites' design changes, while providing impressive speed that surpasses almost all alternatives.""",
long_description=long_description,