@@ -92,27 +92,27 @@ Scrapling is a high-performance, intelligent web scraping library for Python tha
|
|||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### Fetch websites as you prefer with async support
|
### Fetch websites as you prefer with async support
|
||||||
- **HTTP requests**: Stealthy and fast HTTP requests with `Fetcher`
|
- **HTTP Requests**: Fast and stealthy HTTP requests with the `Fetcher` class.
|
||||||
- **Stealthy fetcher**: Annoying anti-bot protection? No problem! Scrapling can bypass almost all of them with `StealthyFetcher` with default configuration!
|
- **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!
|
||||||
- **Your preferred browser**: Use your real browser with CDP, [NSTbrowser](https://app.nstbrowser.io/r/1vO5e5)'s browserless, PlayWright with stealth mode, or even vanilla PlayWright - All is possible with `PlayWrightFetcher`!
|
- **Anti-bot Protections Bypass**: Easily bypass protections with `StealthyFetcher` and `PlayWrightFetcher` classes.
|
||||||
|
|
||||||
### Adaptive Scraping
|
### Adaptive Scraping
|
||||||
- 🔄 **Smart Element Tracking**: Locate previously identified elements after website structure changes, using an intelligent similarity system and integrated storage.
|
- 🔄 **Smart Element Tracking**: Relocate elements after website changes, using an intelligent similarity system and integrated storage.
|
||||||
- 🎯 **Flexible Querying**: Use CSS selectors, XPath, Elements filters, text search, or regex - chain them however you want!
|
- 🎯 **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 want on the page (Ex: other products like the product you found on the page).
|
- 🔍 **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 without specific selectors using Scrapling powerful features.
|
||||||
|
|
||||||
### Performance
|
### High Performance
|
||||||
- 🚀 **Lightning Fast**: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries (outperforming BeautifulSoup in parsing by up to 620x in our tests).
|
- 🚀 **Lightning Fast**: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries.
|
||||||
- 🔋 **Memory Efficient**: Optimized data structures for minimal memory footprint.
|
- 🔋 **Memory Efficient**: Optimized data structures for minimal memory footprint.
|
||||||
- ⚡ **Fast JSON serialization**: 10x faster JSON serialization than the standard json library with more options.
|
- ⚡ **Fast JSON serialization**: 10x faster than standard library.
|
||||||
|
|
||||||
### Developing Experience
|
### Developer Friendly
|
||||||
- 🛠️ **Powerful Navigation API**: Traverse the DOM tree easily in all directions and get the info you want (parent, ancestors, sibling, children, next/previous element, and more).
|
- 🛠️ **Powerful Navigation API**: Easy DOM traversal in all directions.
|
||||||
- 🧬 **Rich Text Processing**: All strings have built-in methods for regex matching, cleaning, and more. All elements' attributes are read-only dictionaries that are faster 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 that takes less memory than standard dictionaries with added methods.
|
||||||
- 📝 **Automatic Selector Generation**: Create robust CSS/XPath selectors for any element.
|
- 📝 **Auto Selectors Generation**: Generate robust short and full CSS/XPath selectors for any element.
|
||||||
- 🔌 **API Similar to Scrapy/BeautifulSoup**: Familiar methods and similar pseudo-elements for Scrapy and BeautifulSoup users.
|
- 🔌 **Familiar API**: Similar to Scrapy/BeautifulSoup and the same pseudo-elements used in Scrapy.
|
||||||
- 📘 **Type hints and test coverage**: Complete type coverage and almost full test coverage for better IDE support and fewer bugs, respectively.
|
- 📘 **Type hints**: Complete type/doc-strings coverage for future-proofing and best autocompletion support.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -121,21 +121,22 @@ from scrapling import Fetcher
|
|||||||
|
|
||||||
fetcher = Fetcher(auto_match=False)
|
fetcher = Fetcher(auto_match=False)
|
||||||
|
|
||||||
# Fetch 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)
|
page = fetcher.get('https://quotes.toscrape.com/', stealthy_headers=True)
|
||||||
# Get all strings in the full page
|
# Get all text content from all HTML tags in the page except `script` and `style` tags
|
||||||
page.get_all_text(ignore_tags=('script', 'style'))
|
page.get_all_text(ignore_tags=('script', 'style'))
|
||||||
|
|
||||||
# Get all quotes, any of these methods will return a list of strings (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.css('.quote .text::text') # CSS selector
|
||||||
quotes = page.xpath('//span[@class="text"]/text()') # XPath
|
quotes = page.xpath('//span[@class="text"]/text()') # XPath
|
||||||
quotes = page.css('.quote').css('.text::text') # Chained selectors
|
quotes = page.css('.quote').css('.text::text') # Chained selectors
|
||||||
quotes = [element.text for element in page.css('.quote .text')] # Slower than bulk query above
|
quotes = [element.text for element in page.css('.quote .text')] # Slower than bulk query above
|
||||||
|
|
||||||
# Get the first quote element
|
# Get the first quote element
|
||||||
quote = page.css_first('.quote') # / page.css('.quote').first / page.css('.quote')[0]
|
quote = page.css_first('.quote') # same as page.css('.quote').first or page.css('.quote')[0]
|
||||||
|
|
||||||
# Tired of selectors? Use find_all/find
|
# Tired of selectors? Use find_all/find
|
||||||
|
# Get all 'div' HTML tags that one of its 'class' values is 'quote'
|
||||||
quotes = page.find_all('div', {'class': 'quote'})
|
quotes = page.find_all('div', {'class': 'quote'})
|
||||||
# Same as
|
# Same as
|
||||||
quotes = page.find_all('div', class_='quote')
|
quotes = page.find_all('div', class_='quote')
|
||||||
@@ -143,10 +144,10 @@ quotes = page.find_all(['div'], class_='quote')
|
|||||||
quotes = page.find_all(class_='quote') # and so on...
|
quotes = page.find_all(class_='quote') # and so on...
|
||||||
|
|
||||||
# Working with elements
|
# Working with elements
|
||||||
quote.html_content # Inner HTML
|
quote.html_content # Get Inner HTML of this element
|
||||||
quote.prettify() # Prettified version of Inner HTML
|
quote.prettify() # Prettified version of Inner HTML above
|
||||||
quote.attrib # Element attributes
|
quote.attrib # Get that element's attributes
|
||||||
quote.path # DOM path to element (List)
|
quote.path # DOM path to element (List of all ancestors from <html> tag till the element itself)
|
||||||
```
|
```
|
||||||
To keep it simple, all methods can be chained on top of each other!
|
To keep it simple, all methods can be chained on top of each other!
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@ True
|
|||||||
| 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. | ✔️ |
|
| 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. | ✔️ |
|
| 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. | ✔️ |
|
| 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 | Enabled by default, this installs `uBlock Origin` addon on the browser if enabled. | ✔️ |
|
| 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. | ✔️ |
|
| 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. | ✔️ |
|
| 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. | ✔️ |
|
| wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ |
|
||||||
@@ -544,7 +545,7 @@ Inspired by BeautifulSoup's `find_all` function you can find elements by using `
|
|||||||
* Any string passed is considered a tag name
|
* Any string passed is considered a tag name
|
||||||
* Any iterable passed like List/Tuple/Set is considered an iterable of tag names.
|
* Any iterable passed like List/Tuple/Set is considered an iterable of tag names.
|
||||||
* Any dictionary is considered a mapping of HTML element(s) attribute names and attribute values.
|
* Any dictionary is considered a mapping of HTML element(s) attribute names and attribute values.
|
||||||
* Any regex patterns passed are used as filters
|
* Any regex patterns passed are used as filters to elements by their text content
|
||||||
* Any functions passed are used as filters
|
* Any functions passed are used as filters
|
||||||
* Any keyword argument passed is considered as an HTML element attribute with its value.
|
* Any keyword argument passed is considered as an HTML element attribute with its value.
|
||||||
|
|
||||||
@@ -553,7 +554,7 @@ So the way it works is after collecting all passed arguments and keywords, each
|
|||||||
|
|
||||||
1. All elements with the passed tag name(s).
|
1. All elements with the passed tag name(s).
|
||||||
2. All elements that match all passed attribute(s).
|
2. All elements that match all passed attribute(s).
|
||||||
3. All elements that match all passed regex patterns.
|
3. All elements that its text content match all passed regex patterns.
|
||||||
4. All elements that fulfill all passed function(s).
|
4. All elements that fulfill all passed function(s).
|
||||||
|
|
||||||
Note: The filtering process always starts from the first filter it finds in the filtering order above so if no tag name(s) are passed but attributes are passed, the process starts from that layer and so on. **But the order in which you pass the arguments doesn't matter.**
|
Note: The filtering process always starts from the first filter it finds in the filtering order above so if no tag name(s) are passed but attributes are passed, the process starts from that layer and so on. **But the order in which you pass the arguments doesn't matter.**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from scrapling.fetchers import (AsyncFetcher, CustomFetcher, Fetcher,
|
|||||||
from scrapling.parser import Adaptor, Adaptors
|
from scrapling.parser import Adaptor, Adaptors
|
||||||
|
|
||||||
__author__ = "Karim Shoair (karim.shoair@pm.me)"
|
__author__ = "Karim Shoair (karim.shoair@pm.me)"
|
||||||
__version__ = "0.2.92"
|
__version__ = "0.2.93"
|
||||||
__copyright__ = "Copyright (c) 2024 Karim Shoair"
|
__copyright__ = "Copyright (c) 2024 Karim Shoair"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Type definitions for type checking purposes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Iterable,
|
from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Iterable,
|
||||||
List, Literal, Optional, Pattern, Tuple, Type, Union)
|
List, Literal, Optional, Pattern, Tuple, Type, TypeVar,
|
||||||
|
Union)
|
||||||
|
|
||||||
SelectorWaitStates = Literal["attached", "detached", "hidden", "visible"]
|
SelectorWaitStates = Literal["attached", "detached", "hidden", "visible"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import re
|
import re
|
||||||
|
import typing
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
from orjson import dumps, loads
|
from orjson import dumps, loads
|
||||||
from w3lib.html import replace_entities as _replace_entities
|
from w3lib.html import replace_entities as _replace_entities
|
||||||
|
|
||||||
from scrapling.core._types import Dict, List, Pattern, SupportsIndex, Union
|
from scrapling.core._types import (Dict, Iterable, List, Literal, Optional,
|
||||||
|
Pattern, SupportsIndex, TypeVar, Union)
|
||||||
from scrapling.core.utils import _is_iterable, flatten
|
from scrapling.core.utils import _is_iterable, flatten
|
||||||
|
|
||||||
|
# Define type variable for AttributeHandler value type
|
||||||
|
_TextHandlerType = TypeVar('_TextHandlerType', bound='TextHandler')
|
||||||
|
|
||||||
|
|
||||||
class TextHandler(str):
|
class TextHandler(str):
|
||||||
"""Extends standard Python string by adding more functionality"""
|
"""Extends standard Python string by adding more functionality"""
|
||||||
@@ -18,72 +23,89 @@ class TextHandler(str):
|
|||||||
return super().__new__(cls, string)
|
return super().__new__(cls, string)
|
||||||
return super().__new__(cls, '')
|
return super().__new__(cls, '')
|
||||||
|
|
||||||
# Make methods from original `str` class return `TextHandler` instead of returning `str` again
|
@typing.overload
|
||||||
# Of course, this stupid workaround is only so we can keep the auto-completion working without issues in your IDE
|
def __getitem__(self, key: SupportsIndex) -> 'TextHandler':
|
||||||
# and I made sonnet write it for me :)
|
pass
|
||||||
def strip(self, chars=None):
|
|
||||||
|
@typing.overload
|
||||||
|
def __getitem__(self, key: slice) -> "TextHandlers":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, key: Union[SupportsIndex, slice]) -> Union["TextHandler", "TextHandlers"]:
|
||||||
|
lst = super().__getitem__(key)
|
||||||
|
if isinstance(key, slice):
|
||||||
|
lst = [TextHandler(s) for s in lst]
|
||||||
|
return TextHandlers(typing.cast(List[_TextHandlerType], lst))
|
||||||
|
return typing.cast(_TextHandlerType, TextHandler(lst))
|
||||||
|
|
||||||
|
def split(self, sep: str = None, maxsplit: SupportsIndex = -1) -> 'TextHandlers':
|
||||||
|
return TextHandlers(
|
||||||
|
typing.cast(List[_TextHandlerType], [TextHandler(s) for s in super().split(sep, maxsplit)])
|
||||||
|
)
|
||||||
|
|
||||||
|
def strip(self, chars: str = None) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().strip(chars))
|
return TextHandler(super().strip(chars))
|
||||||
|
|
||||||
def lstrip(self, chars=None):
|
def lstrip(self, chars: str = None) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().lstrip(chars))
|
return TextHandler(super().lstrip(chars))
|
||||||
|
|
||||||
def rstrip(self, chars=None):
|
def rstrip(self, chars: str = None) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().rstrip(chars))
|
return TextHandler(super().rstrip(chars))
|
||||||
|
|
||||||
def capitalize(self):
|
def capitalize(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().capitalize())
|
return TextHandler(super().capitalize())
|
||||||
|
|
||||||
def casefold(self):
|
def casefold(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().casefold())
|
return TextHandler(super().casefold())
|
||||||
|
|
||||||
def center(self, width, fillchar=' '):
|
def center(self, width: SupportsIndex, fillchar: str = ' ') -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().center(width, fillchar))
|
return TextHandler(super().center(width, fillchar))
|
||||||
|
|
||||||
def expandtabs(self, tabsize=8):
|
def expandtabs(self, tabsize: SupportsIndex = 8) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().expandtabs(tabsize))
|
return TextHandler(super().expandtabs(tabsize))
|
||||||
|
|
||||||
def format(self, *args, **kwargs):
|
def format(self, *args: str, **kwargs: str) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().format(*args, **kwargs))
|
return TextHandler(super().format(*args, **kwargs))
|
||||||
|
|
||||||
def format_map(self, mapping):
|
def format_map(self, mapping) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().format_map(mapping))
|
return TextHandler(super().format_map(mapping))
|
||||||
|
|
||||||
def join(self, iterable):
|
def join(self, iterable: Iterable[str]) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().join(iterable))
|
return TextHandler(super().join(iterable))
|
||||||
|
|
||||||
def ljust(self, width, fillchar=' '):
|
def ljust(self, width: SupportsIndex, fillchar: str = ' ') -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().ljust(width, fillchar))
|
return TextHandler(super().ljust(width, fillchar))
|
||||||
|
|
||||||
def rjust(self, width, fillchar=' '):
|
def rjust(self, width: SupportsIndex, fillchar: str = ' ') -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().rjust(width, fillchar))
|
return TextHandler(super().rjust(width, fillchar))
|
||||||
|
|
||||||
def swapcase(self):
|
def swapcase(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().swapcase())
|
return TextHandler(super().swapcase())
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().title())
|
return TextHandler(super().title())
|
||||||
|
|
||||||
def translate(self, table):
|
def translate(self, table) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().translate(table))
|
return TextHandler(super().translate(table))
|
||||||
|
|
||||||
def zfill(self, width):
|
def zfill(self, width: SupportsIndex) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().zfill(width))
|
return TextHandler(super().zfill(width))
|
||||||
|
|
||||||
def replace(self, old, new, count=-1):
|
def replace(self, old: str, new: str, count: SupportsIndex = -1) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().replace(old, new, count))
|
return TextHandler(super().replace(old, new, count))
|
||||||
|
|
||||||
def upper(self):
|
def upper(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().upper())
|
return TextHandler(super().upper())
|
||||||
|
|
||||||
def lower(self):
|
def lower(self) -> Union[str, 'TextHandler']:
|
||||||
return TextHandler(super().lower())
|
return TextHandler(super().lower())
|
||||||
##############
|
##############
|
||||||
|
|
||||||
def sort(self, reverse: bool = False) -> str:
|
def sort(self, reverse: bool = False) -> Union[str, 'TextHandler']:
|
||||||
"""Return a sorted version of the string"""
|
"""Return a sorted version of the string"""
|
||||||
return self.__class__("".join(sorted(self, reverse=reverse)))
|
return self.__class__("".join(sorted(self, reverse=reverse)))
|
||||||
|
|
||||||
def clean(self) -> str:
|
def clean(self) -> Union[str, 'TextHandler']:
|
||||||
"""Return a new version of the string after removing all white spaces and consecutive spaces"""
|
"""Return a new version of the string after removing all white spaces and consecutive spaces"""
|
||||||
data = re.sub(r'[\t|\r|\n]', '', self)
|
data = re.sub(r'[\t|\r|\n]', '', self)
|
||||||
data = re.sub(' +', ' ', data)
|
data = re.sub(' +', ' ', data)
|
||||||
@@ -105,10 +127,32 @@ class TextHandler(str):
|
|||||||
# Check this out: https://github.com/ijl/orjson/issues/445
|
# Check this out: https://github.com/ijl/orjson/issues/445
|
||||||
return loads(str(self))
|
return loads(str(self))
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def re(
|
||||||
|
self,
|
||||||
|
regex: Union[str, Pattern[str]],
|
||||||
|
check_match: Literal[True],
|
||||||
|
replace_entities: bool = True,
|
||||||
|
clean_match: bool = False,
|
||||||
|
case_sensitive: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def re(
|
||||||
|
self,
|
||||||
|
regex: Union[str, Pattern[str]],
|
||||||
|
replace_entities: bool = True,
|
||||||
|
clean_match: bool = False,
|
||||||
|
case_sensitive: bool = False,
|
||||||
|
check_match: Literal[False] = False,
|
||||||
|
) -> "TextHandlers[TextHandler]":
|
||||||
|
...
|
||||||
|
|
||||||
def re(
|
def re(
|
||||||
self, regex: Union[str, Pattern[str]], replace_entities: bool = True, clean_match: bool = False,
|
self, regex: Union[str, Pattern[str]], replace_entities: bool = True, clean_match: bool = False,
|
||||||
case_sensitive: bool = False, check_match: bool = False
|
case_sensitive: bool = False, check_match: bool = False
|
||||||
) -> Union[List[str], bool]:
|
) -> Union["TextHandlers[TextHandler]", bool]:
|
||||||
"""Apply the given regex to the current text and return a list of strings with the matches.
|
"""Apply the given regex to the current text and return a list of strings with the matches.
|
||||||
|
|
||||||
:param regex: Can be either a compiled regular expression or a string.
|
:param regex: Can be either a compiled regular expression or a string.
|
||||||
@@ -133,12 +177,12 @@ class TextHandler(str):
|
|||||||
results = flatten(results)
|
results = flatten(results)
|
||||||
|
|
||||||
if not replace_entities:
|
if not replace_entities:
|
||||||
return [TextHandler(string) for string in results]
|
return TextHandlers(typing.cast(List[_TextHandlerType], [TextHandler(string) for string in results]))
|
||||||
|
|
||||||
return [TextHandler(_replace_entities(s)) for s in results]
|
return TextHandlers(typing.cast(List[_TextHandlerType], [TextHandler(_replace_entities(s)) for s in results]))
|
||||||
|
|
||||||
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> Union[str, None]:
|
clean_match: bool = False, case_sensitive: bool = False) -> "TextHandler":
|
||||||
"""Apply the given regex to text and return the first match if found, otherwise return the default value.
|
"""Apply the given regex to text and return the first match if found, otherwise return the default value.
|
||||||
|
|
||||||
:param regex: Can be either a compiled regular expression or a string.
|
:param regex: Can be either a compiled regular expression or a string.
|
||||||
@@ -158,15 +202,23 @@ class TextHandlers(List[TextHandler]):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __getitem__(self, pos: Union[SupportsIndex, slice]) -> Union[TextHandler, "TextHandlers[TextHandler]"]:
|
@typing.overload
|
||||||
|
def __getitem__(self, pos: SupportsIndex) -> TextHandler:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def __getitem__(self, pos: slice) -> "TextHandlers":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, pos: Union[SupportsIndex, slice]) -> Union[TextHandler, "TextHandlers"]:
|
||||||
lst = super().__getitem__(pos)
|
lst = super().__getitem__(pos)
|
||||||
if isinstance(pos, slice):
|
if isinstance(pos, slice):
|
||||||
return self.__class__(lst)
|
lst = [TextHandler(s) for s in lst]
|
||||||
else:
|
return TextHandlers(typing.cast(List[_TextHandlerType], lst))
|
||||||
return lst
|
return typing.cast(_TextHandlerType, TextHandler(lst))
|
||||||
|
|
||||||
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True, clean_match: bool = False,
|
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True, clean_match: bool = False,
|
||||||
case_sensitive: bool = False) -> 'List[str]':
|
case_sensitive: bool = False) -> 'TextHandlers[TextHandler]':
|
||||||
"""Call the ``.re()`` method for each element in this list and return
|
"""Call the ``.re()`` method for each element in this list and return
|
||||||
their results flattened as TextHandlers.
|
their results flattened as TextHandlers.
|
||||||
|
|
||||||
@@ -178,10 +230,10 @@ class TextHandlers(List[TextHandler]):
|
|||||||
results = [
|
results = [
|
||||||
n.re(regex, replace_entities, clean_match, case_sensitive) for n in self
|
n.re(regex, replace_entities, clean_match, case_sensitive) for n in self
|
||||||
]
|
]
|
||||||
return flatten(results)
|
return TextHandlers(flatten(results))
|
||||||
|
|
||||||
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> Union[str, None]:
|
clean_match: bool = False, case_sensitive: bool = False) -> TextHandler:
|
||||||
"""Call the ``.re_first()`` method for each element in this list and return
|
"""Call the ``.re_first()`` method for each element in this list and return
|
||||||
the first result or the default value otherwise.
|
the first result or the default value otherwise.
|
||||||
|
|
||||||
@@ -210,7 +262,7 @@ class TextHandlers(List[TextHandler]):
|
|||||||
get_all = extract
|
get_all = extract
|
||||||
|
|
||||||
|
|
||||||
class AttributesHandler(Mapping):
|
class AttributesHandler(Mapping[str, _TextHandlerType]):
|
||||||
"""A read-only mapping to use instead of the standard dictionary for the speed boost but at the same time I use it to add more functionalities.
|
"""A read-only mapping to use instead of the standard dictionary for the speed boost but at the same time I use it to add more functionalities.
|
||||||
If standard dictionary is needed, just convert this class to dictionary with `dict` function
|
If standard dictionary is needed, just convert this class to dictionary with `dict` function
|
||||||
"""
|
"""
|
||||||
@@ -231,7 +283,7 @@ class AttributesHandler(Mapping):
|
|||||||
# Fastest read-only mapping type
|
# Fastest read-only mapping type
|
||||||
self._data = MappingProxyType(mapping)
|
self._data = MappingProxyType(mapping)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key: str, default: Optional[str] = None) -> Union[_TextHandlerType, None]:
|
||||||
"""Acts like standard dictionary `.get()` method"""
|
"""Acts like standard dictionary `.get()` method"""
|
||||||
return self._data.get(key, default)
|
return self._data.get(key, default)
|
||||||
|
|
||||||
@@ -253,7 +305,7 @@ class AttributesHandler(Mapping):
|
|||||||
"""Convert current attributes to JSON string if the attributes are JSON serializable otherwise throws error"""
|
"""Convert current attributes to JSON string if the attributes are JSON serializable otherwise throws error"""
|
||||||
return dumps(dict(self._data))
|
return dumps(dict(self._data))
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key: str) -> _TextHandlerType:
|
||||||
return self._data[key]
|
return self._data[key]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
|||||||
@@ -139,6 +139,6 @@ class TranslatorMixin:
|
|||||||
|
|
||||||
|
|
||||||
class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
|
class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
|
||||||
@lru_cache(maxsize=256)
|
@lru_cache(maxsize=2048)
|
||||||
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)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from .fetchers import AsyncFetcher, Fetcher, PlayWrightFetcher, StealthyFetcher
|
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()
|
Fetcher = _Fetcher()
|
||||||
AsyncFetcher = AsyncFetcher()
|
AsyncFetcher = _AsyncFetcher()
|
||||||
StealthyFetcher = StealthyFetcher()
|
StealthyFetcher = _StealthyFetcher()
|
||||||
PlayWrightFetcher = PlayWrightFetcher()
|
PlayWrightFetcher = _PlayWrightFetcher()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class CamoufoxEngine:
|
|||||||
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[bool] = False, humanize: Optional[Union[bool, float]] = True,
|
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[bool] = False, humanize: Optional[Union[bool, float]] = True,
|
||||||
timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, addons: Optional[List[str]] = None,
|
timeout: Optional[float] = 30000, page_action: Callable = None, wait_selector: Optional[str] = None, addons: Optional[List[str]] = None,
|
||||||
wait_selector_state: Optional[SelectorWaitStates] = 'attached', google_search: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
wait_selector_state: Optional[SelectorWaitStates] = 'attached', google_search: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
||||||
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = True,
|
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = False,
|
||||||
geoip: Optional[bool] = False,
|
geoip: Optional[bool] = False,
|
||||||
adaptor_arguments: Dict = None,
|
adaptor_arguments: Dict = None,
|
||||||
):
|
):
|
||||||
@@ -36,7 +36,7 @@ class CamoufoxEngine:
|
|||||||
:param 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.
|
:param 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.
|
||||||
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
||||||
:param network_idle: Wait for the page until there are no network connections for at least 500 ms.
|
:param network_idle: Wait for the page until there are no network connections for at least 500 ms.
|
||||||
:param disable_ads: Enabled by default, this installs `uBlock Origin` addon on the browser if enabled.
|
: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 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 page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again.
|
:param page_action: Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again.
|
||||||
@@ -95,6 +95,8 @@ class CamoufoxEngine:
|
|||||||
with Camoufox(
|
with Camoufox(
|
||||||
geoip=self.geoip,
|
geoip=self.geoip,
|
||||||
proxy=self.proxy,
|
proxy=self.proxy,
|
||||||
|
disable_coop=True,
|
||||||
|
enable_cache=True,
|
||||||
addons=self.addons,
|
addons=self.addons,
|
||||||
exclude_addons=addons,
|
exclude_addons=addons,
|
||||||
headless=self.headless,
|
headless=self.headless,
|
||||||
@@ -174,6 +176,8 @@ class CamoufoxEngine:
|
|||||||
async with AsyncCamoufox(
|
async with AsyncCamoufox(
|
||||||
geoip=self.geoip,
|
geoip=self.geoip,
|
||||||
proxy=self.proxy,
|
proxy=self.proxy,
|
||||||
|
disable_coop=True,
|
||||||
|
enable_cache=True,
|
||||||
addons=self.addons,
|
addons=self.addons,
|
||||||
exclude_addons=addons,
|
exclude_addons=addons,
|
||||||
headless=self.headless,
|
headless=self.headless,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class PlaywrightEngine:
|
|||||||
"""
|
"""
|
||||||
cdp_url = self.cdp_url
|
cdp_url = self.cdp_url
|
||||||
if self.nstbrowser_mode:
|
if self.nstbrowser_mode:
|
||||||
if self.nstbrowser_config and type(self.nstbrowser_config) is Dict:
|
if self.nstbrowser_config and isinstance(self.nstbrowser_config, dict):
|
||||||
config = self.nstbrowser_config
|
config = self.nstbrowser_config
|
||||||
else:
|
else:
|
||||||
query = NSTBROWSER_DEFAULT_QUERY.copy()
|
query = NSTBROWSER_DEFAULT_QUERY.copy()
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class AsyncFetcher(Fetcher):
|
|||||||
:return: A `Response` object that is the same as `Adaptor` object except it has these added attributes: `status`, `reason`, `cookies`, `headers`, and `request_headers`
|
: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(self.adaptor_arguments.items())
|
||||||
response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_post(**kwargs)
|
response_object = await StaticEngine(url, proxy, stealthy_headers, follow_redirects, timeout, retries=retries, adaptor_arguments=adaptor_arguments).async_put(**kwargs)
|
||||||
return response_object
|
return response_object
|
||||||
|
|
||||||
async def delete(
|
async def delete(
|
||||||
@@ -177,7 +177,7 @@ class StealthyFetcher(BaseFetcher):
|
|||||||
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[bool] = False, addons: Optional[List[str]] = None,
|
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[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,
|
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: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
wait_selector_state: SelectorWaitStates = 'attached', google_search: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
||||||
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = True, geoip: Optional[bool] = False,
|
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = False, geoip: Optional[bool] = False,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""
|
"""
|
||||||
Opens up a browser and do your request based on your chosen options below.
|
Opens up a browser and do your request based on your chosen options below.
|
||||||
@@ -191,7 +191,7 @@ class StealthyFetcher(BaseFetcher):
|
|||||||
This can help save your proxy usage but be careful with this option as it makes some websites never finish loading.
|
This can help save your proxy usage but be careful with this option as it makes some websites never finish loading.
|
||||||
:param block_webrtc: Blocks WebRTC entirely.
|
:param block_webrtc: Blocks WebRTC entirely.
|
||||||
:param addons: List of Firefox addons to use. Must be paths to extracted addons.
|
:param addons: List of Firefox addons to use. Must be paths to extracted addons.
|
||||||
:param disable_ads: Enabled by default, this installs `uBlock Origin` addon on the browser if enabled.
|
:param disable_ads: Disabled by default, this installs `uBlock Origin` addon on the browser if enabled.
|
||||||
:param 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.
|
:param 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.
|
||||||
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
||||||
:param geoip: Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address.
|
:param geoip: Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address.
|
||||||
@@ -235,7 +235,7 @@ class StealthyFetcher(BaseFetcher):
|
|||||||
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[bool] = False, addons: Optional[List[str]] = None,
|
block_webrtc: Optional[bool] = False, allow_webgl: Optional[bool] = True, network_idle: Optional[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,
|
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: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
wait_selector_state: SelectorWaitStates = 'attached', google_search: Optional[bool] = True, extra_headers: Optional[Dict[str, str]] = None,
|
||||||
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = True, geoip: Optional[bool] = False,
|
proxy: Optional[Union[str, Dict[str, str]]] = None, os_randomize: Optional[bool] = None, disable_ads: Optional[bool] = False, geoip: Optional[bool] = False,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""
|
"""
|
||||||
Opens up a browser and do your request based on your chosen options below.
|
Opens up a browser and do your request based on your chosen options below.
|
||||||
@@ -249,7 +249,7 @@ class StealthyFetcher(BaseFetcher):
|
|||||||
This can help save your proxy usage but be careful with this option as it makes some websites never finish loading.
|
This can help save your proxy usage but be careful with this option as it makes some websites never finish loading.
|
||||||
:param block_webrtc: Blocks WebRTC entirely.
|
:param block_webrtc: Blocks WebRTC entirely.
|
||||||
:param addons: List of Firefox addons to use. Must be paths to extracted addons.
|
:param addons: List of Firefox addons to use. Must be paths to extracted addons.
|
||||||
:param disable_ads: Enabled by default, this installs `uBlock Origin` addon on the browser if enabled.
|
:param disable_ads: Disabled by default, this installs `uBlock Origin` addon on the browser if enabled.
|
||||||
:param 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.
|
:param 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.
|
||||||
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
:param allow_webgl: Enabled by default. Disabling it WebGL not recommended as many WAFs now checks if WebGL is enabled.
|
||||||
:param geoip: Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address.
|
:param geoip: Recommended to use with proxies; Automatically use IP's longitude, latitude, timezone, country, locale, & spoof the WebRTC IP address.
|
||||||
|
|||||||
+124
-160
@@ -1,6 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import typing
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
@@ -145,16 +146,16 @@ class Adaptor(SelectorsGeneration):
|
|||||||
# Faster than checking `element.is_attribute or element.is_text or element.is_tail`
|
# Faster than checking `element.is_attribute or element.is_text or element.is_tail`
|
||||||
return issubclass(type(element), etree._ElementUnicodeResult)
|
return issubclass(type(element), etree._ElementUnicodeResult)
|
||||||
|
|
||||||
def __get_correct_result(
|
@staticmethod
|
||||||
self, element: Union[html.HtmlElement, etree._ElementUnicodeResult]
|
def __content_convertor(element: Union[html.HtmlElement, etree._ElementUnicodeResult]) -> TextHandler:
|
||||||
) -> Union[TextHandler, html.HtmlElement, 'Adaptor', str]:
|
"""Used internally to convert a single element's text content to TextHandler directly without checks
|
||||||
"""Used internally in all functions to convert results to type (Adaptor|Adaptors) when possible"""
|
|
||||||
if self._is_text_node(element):
|
|
||||||
# etree._ElementUnicodeResult basically inherit from `str` so it's fine
|
|
||||||
return TextHandler(str(element))
|
|
||||||
else:
|
|
||||||
if issubclass(type(element), html.HtmlMixin):
|
|
||||||
|
|
||||||
|
This single line has been isolated like this so when it's used with map we get that slight performance boost vs list comprehension
|
||||||
|
"""
|
||||||
|
return TextHandler(str(element))
|
||||||
|
|
||||||
|
def __element_convertor(self, element: html.HtmlElement) -> 'Adaptor':
|
||||||
|
"""Used internally to convert a single HtmlElement to Adaptor directly without checks"""
|
||||||
return Adaptor(
|
return Adaptor(
|
||||||
root=element,
|
root=element,
|
||||||
text='', body=b'', # Since root argument is provided, both `text` and `body` will be ignored so this is just a filler
|
text='', body=b'', # Since root argument is provided, both `text` and `body` will be ignored so this is just a filler
|
||||||
@@ -163,29 +164,28 @@ class Adaptor(SelectorsGeneration):
|
|||||||
huge_tree=self.__huge_tree_enabled,
|
huge_tree=self.__huge_tree_enabled,
|
||||||
**self.__response_data
|
**self.__response_data
|
||||||
)
|
)
|
||||||
return element
|
|
||||||
|
|
||||||
def __convert_results(
|
def __handle_element(self, element: Union[html.HtmlElement, etree._ElementUnicodeResult]) -> Union[TextHandler, 'Adaptor', None]:
|
||||||
self, result: Union[List[html.HtmlElement], html.HtmlElement]
|
"""Used internally in all functions to convert a single element to type (Adaptor|TextHandler) when possible"""
|
||||||
) -> Union['Adaptors[Adaptor]', 'Adaptor', List, None]:
|
if element is None:
|
||||||
"""Used internally in all functions to convert results to type (Adaptor|Adaptors) in bulk when possible"""
|
|
||||||
if result is None:
|
|
||||||
return None
|
return None
|
||||||
elif result == []: # Lxml will give a warning if I used something like `not result`
|
elif self._is_text_node(element):
|
||||||
return []
|
# etree._ElementUnicodeResult basically inherit from `str` so it's fine
|
||||||
|
return self.__content_convertor(element)
|
||||||
|
else:
|
||||||
|
return self.__element_convertor(element)
|
||||||
|
|
||||||
if isinstance(result, Adaptors):
|
def __handle_elements(self, result: List[Union[html.HtmlElement, etree._ElementUnicodeResult]]) -> Union['Adaptors', 'TextHandlers', List]:
|
||||||
return result
|
"""Used internally in all functions to convert results to type (Adaptors|TextHandlers) in bulk when possible"""
|
||||||
|
if not len(result): # Lxml will give a warning if I used something like `not result`
|
||||||
|
return Adaptors([])
|
||||||
|
|
||||||
if type(result) is list:
|
# From within the code, this method will always get a list of the same type
|
||||||
results = [self.__get_correct_result(n) for n in result]
|
# so we will continue without checks for slight performance boost
|
||||||
if all(isinstance(res, self.__class__) for res in results):
|
if self._is_text_node(result[0]):
|
||||||
return Adaptors(results)
|
return TextHandlers(list(map(self.__content_convertor, result)))
|
||||||
elif all(isinstance(res, TextHandler) for res in results):
|
|
||||||
return TextHandlers(results)
|
|
||||||
return results
|
|
||||||
|
|
||||||
return self.__get_correct_result(result)
|
return Adaptors(list(map(self.__element_convertor, result)))
|
||||||
|
|
||||||
def __getstate__(self) -> Any:
|
def __getstate__(self) -> Any:
|
||||||
# lxml don't like it :)
|
# lxml don't like it :)
|
||||||
@@ -223,29 +223,16 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:return: A TextHandler
|
:return: A TextHandler
|
||||||
"""
|
"""
|
||||||
_all_strings = []
|
_all_strings = []
|
||||||
|
for node in self._root.xpath('.//*'):
|
||||||
def _traverse(node: html.HtmlElement) -> None:
|
|
||||||
"""Traverse element children and get text content of each
|
|
||||||
|
|
||||||
:param node: Current node in the tree structure
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if node.tag not in ignore_tags:
|
if node.tag not in ignore_tags:
|
||||||
text = node.text
|
text = node.text
|
||||||
if text and type(text) is str:
|
if text and type(text) is str:
|
||||||
if valid_values:
|
if valid_values and text.strip():
|
||||||
if text.strip():
|
|
||||||
_all_strings.append(text if not strip else text.strip())
|
_all_strings.append(text if not strip else text.strip())
|
||||||
else:
|
else:
|
||||||
_all_strings.append(text if not strip else text.strip())
|
_all_strings.append(text if not strip else text.strip())
|
||||||
|
|
||||||
for branch in node.iterchildren():
|
return TextHandler(separator.join(_all_strings))
|
||||||
_traverse(branch)
|
|
||||||
|
|
||||||
# We will start using Lxml directly for the speed boost
|
|
||||||
_traverse(self._root)
|
|
||||||
|
|
||||||
return TextHandler(separator.join([s for s in _all_strings]))
|
|
||||||
|
|
||||||
def urljoin(self, relative_url: str) -> str:
|
def urljoin(self, relative_url: str) -> str:
|
||||||
"""Join this Adaptor's url with a relative url to form an absolute full URL."""
|
"""Join this Adaptor's url with a relative url to form an absolute full URL."""
|
||||||
@@ -259,18 +246,18 @@ class Adaptor(SelectorsGeneration):
|
|||||||
return self.__attributes
|
return self.__attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def html_content(self) -> str:
|
def html_content(self) -> TextHandler:
|
||||||
"""Return the inner html code of the element"""
|
"""Return the inner html code of the element"""
|
||||||
return 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
|
@property
|
||||||
def body(self) -> str:
|
def body(self) -> TextHandler:
|
||||||
"""Return raw HTML code of the element/page without any processing when possible or return `Adaptor.html_content`"""
|
"""Return raw HTML code of the element/page without any processing when possible or return `Adaptor.html_content`"""
|
||||||
return self.__raw_body or self.html_content
|
return TextHandler(self.__raw_body) or self.html_content
|
||||||
|
|
||||||
def prettify(self) -> str:
|
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"""
|
||||||
return etree.tostring(self._root, encoding='unicode', pretty_print=True, method='html', with_tail=False)
|
return TextHandler(etree.tostring(self._root, encoding='unicode', pretty_print=True, method='html', with_tail=False))
|
||||||
|
|
||||||
def has_class(self, class_name: str) -> bool:
|
def has_class(self, class_name: str) -> bool:
|
||||||
"""Check if element has a specific class
|
"""Check if element has a specific class
|
||||||
@@ -282,26 +269,32 @@ class Adaptor(SelectorsGeneration):
|
|||||||
@property
|
@property
|
||||||
def parent(self) -> Union['Adaptor', None]:
|
def parent(self) -> Union['Adaptor', None]:
|
||||||
"""Return the direct parent of the element or ``None`` otherwise"""
|
"""Return the direct parent of the element or ``None`` otherwise"""
|
||||||
return self.__convert_results(self._root.getparent())
|
return self.__handle_element(self._root.getparent())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self) -> Union['Adaptors[Adaptor]', List]:
|
def below_elements(self) -> 'Adaptors[Adaptor]':
|
||||||
|
"""Return all elements under the current element in the DOM tree"""
|
||||||
|
below = self._root.xpath('.//*')
|
||||||
|
return self.__handle_elements(below)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self) -> 'Adaptors[Adaptor]':
|
||||||
"""Return the children elements of the current element or empty list otherwise"""
|
"""Return the children elements of the current element or empty list otherwise"""
|
||||||
return self.__convert_results(list(
|
return Adaptors([
|
||||||
child for child in self._root.iterchildren() if type(child) not in html_forbidden
|
self.__element_convertor(child) for child in self._root.iterchildren() if type(child) not in html_forbidden
|
||||||
))
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def siblings(self) -> Union['Adaptors[Adaptor]', List]:
|
def siblings(self) -> 'Adaptors[Adaptor]':
|
||||||
"""Return other children of the current element's parent or empty list otherwise"""
|
"""Return other children of the current element's parent or empty list otherwise"""
|
||||||
if self.parent:
|
if self.parent:
|
||||||
return Adaptors([child for child in self.parent.children if child._root != self._root])
|
return Adaptors([child for child in self.parent.children if child._root != self._root])
|
||||||
return []
|
return Adaptors([])
|
||||||
|
|
||||||
def iterancestors(self) -> Generator['Adaptor', None, None]:
|
def iterancestors(self) -> Generator['Adaptor', None, None]:
|
||||||
"""Return a generator that loops over all ancestors of the element, starting with element's parent."""
|
"""Return a generator that loops over all ancestors of the element, starting with element's parent."""
|
||||||
for ancestor in self._root.iterancestors():
|
for ancestor in self._root.iterancestors():
|
||||||
yield self.__convert_results(ancestor)
|
yield self.__element_convertor(ancestor)
|
||||||
|
|
||||||
def find_ancestor(self, func: Callable[['Adaptor'], bool]) -> Union['Adaptor', None]:
|
def find_ancestor(self, func: Callable[['Adaptor'], bool]) -> Union['Adaptor', None]:
|
||||||
"""Loop over all ancestors of the element till one match the passed function
|
"""Loop over all ancestors of the element till one match the passed function
|
||||||
@@ -328,7 +321,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
# Ignore html comments and unwanted types
|
# Ignore html comments and unwanted types
|
||||||
next_element = next_element.getnext()
|
next_element = next_element.getnext()
|
||||||
|
|
||||||
return self.__convert_results(next_element)
|
return self.__handle_element(next_element)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def previous(self) -> Union['Adaptor', None]:
|
def previous(self) -> Union['Adaptor', None]:
|
||||||
@@ -339,7 +332,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
# Ignore html comments and unwanted types
|
# Ignore html comments and unwanted types
|
||||||
prev_element = prev_element.getprevious()
|
prev_element = prev_element.getprevious()
|
||||||
|
|
||||||
return self.__convert_results(prev_element)
|
return self.__handle_element(prev_element)
|
||||||
|
|
||||||
# For easy copy-paste from Scrapy/parsel code when needed :)
|
# For easy copy-paste from Scrapy/parsel code when needed :)
|
||||||
def get(self, default=None):
|
def get(self, default=None):
|
||||||
@@ -392,34 +385,26 @@ class Adaptor(SelectorsGeneration):
|
|||||||
if issubclass(type(element), html.HtmlElement):
|
if issubclass(type(element), html.HtmlElement):
|
||||||
element = _StorageTools.element_to_dict(element)
|
element = _StorageTools.element_to_dict(element)
|
||||||
|
|
||||||
# TODO: Optimize the traverse logic a bit, maybe later
|
for node in self._root.xpath('.//*'):
|
||||||
def _traverse(node: html.HtmlElement, ele: Dict) -> None:
|
# Collect all elements in the page then for each element get the matching score of it against the node.
|
||||||
"""Get the matching score of the given element against the node then traverse the children
|
|
||||||
|
|
||||||
:param node: Current node in the tree structure
|
|
||||||
:param ele: The element we are searching for as dictionary
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# Hence: the code doesn't stop even if the score was 100%
|
# Hence: the code doesn't stop even if the score was 100%
|
||||||
# because there might be another element(s) left in page with the same score
|
# because there might be another element(s) left in page with the same score
|
||||||
score = self.__calculate_similarity_score(ele, node)
|
score = self.__calculate_similarity_score(element, node)
|
||||||
score_table.setdefault(score, []).append(node)
|
score_table.setdefault(score, []).append(node)
|
||||||
for branch in node.iterchildren():
|
|
||||||
_traverse(branch, ele)
|
|
||||||
|
|
||||||
# This will block until we traverse all children/branches
|
|
||||||
_traverse(self._root, element)
|
|
||||||
|
|
||||||
if score_table:
|
if score_table:
|
||||||
highest_probability = max(score_table.keys())
|
highest_probability = max(score_table.keys())
|
||||||
if score_table[highest_probability] and highest_probability >= percentage:
|
if score_table[highest_probability] and highest_probability >= percentage:
|
||||||
|
if log.getEffectiveLevel() < 20:
|
||||||
|
# No need to execute this part if logging level is not debugging
|
||||||
log.debug(f'Highest probability was {highest_probability}%')
|
log.debug(f'Highest probability was {highest_probability}%')
|
||||||
log.debug('Top 5 best matching elements are: ')
|
log.debug('Top 5 best matching elements are: ')
|
||||||
for percent in tuple(sorted(score_table.keys(), reverse=True))[:5]:
|
for percent in tuple(sorted(score_table.keys(), reverse=True))[:5]:
|
||||||
log.debug(f'{percent} -> {self.__convert_results(score_table[percent])}')
|
log.debug(f'{percent} -> {self.__handle_elements(score_table[percent])}')
|
||||||
|
|
||||||
if not adaptor_type:
|
if not adaptor_type:
|
||||||
return score_table[highest_probability]
|
return score_table[highest_probability]
|
||||||
return self.__convert_results(score_table[highest_probability])
|
return self.__handle_elements(score_table[highest_probability])
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def css_first(self, selector: str, identifier: str = '',
|
def css_first(self, selector: str, identifier: str = '',
|
||||||
@@ -439,8 +424,6 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:param percentage: The minimum percentage to accept while auto-matching and not going lower than that.
|
:param percentage: The minimum percentage to accept while auto-matching and not going lower than that.
|
||||||
Be aware that the percentage calculation depends solely on the page structure so don't play with this
|
Be aware that the percentage calculation depends solely on the page structure so don't play with this
|
||||||
number unless you must know what you are doing!
|
number unless you must know what you are doing!
|
||||||
|
|
||||||
:return: List as :class:`Adaptors`
|
|
||||||
"""
|
"""
|
||||||
for element in self.css(selector, identifier, auto_match, auto_save, percentage):
|
for element in self.css(selector, identifier, auto_match, auto_save, percentage):
|
||||||
return element
|
return element
|
||||||
@@ -465,8 +448,6 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:param percentage: The minimum percentage to accept while auto-matching and not going lower than that.
|
:param percentage: The minimum percentage to accept while auto-matching and not going lower than that.
|
||||||
Be aware that the percentage calculation depends solely on the page structure so don't play with this
|
Be aware that the percentage calculation depends solely on the page structure so don't play with this
|
||||||
number unless you must know what you are doing!
|
number unless you must know what you are doing!
|
||||||
|
|
||||||
:return: List as :class:`Adaptors`
|
|
||||||
"""
|
"""
|
||||||
for element in self.xpath(selector, identifier, auto_match, auto_save, percentage, **kwargs):
|
for element in self.xpath(selector, identifier, auto_match, auto_save, percentage, **kwargs):
|
||||||
return element
|
return element
|
||||||
@@ -493,7 +474,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:return: List as :class:`Adaptors`
|
:return: List as :class:`Adaptors`
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not self.__auto_match_enabled:
|
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 = HTMLTranslator().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)
|
||||||
@@ -507,11 +488,8 @@ class Adaptor(SelectorsGeneration):
|
|||||||
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
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
xpath_selector = HTMLTranslator().css_to_xpath(selector)
|
|
||||||
return self.xpath(xpath_selector, identifier or selector, auto_match, auto_save, percentage)
|
|
||||||
|
|
||||||
return self.__convert_results(results)
|
return results
|
||||||
except (SelectorError, SelectorSyntaxError,):
|
except (SelectorError, SelectorSyntaxError,):
|
||||||
raise SelectorSyntaxError(f"Invalid CSS selector: {selector}")
|
raise SelectorSyntaxError(f"Invalid CSS selector: {selector}")
|
||||||
|
|
||||||
@@ -538,37 +516,37 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:return: List as :class:`Adaptors`
|
:return: List as :class:`Adaptors`
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
selected_elements = self._root.xpath(selector, **kwargs)
|
elements = self._root.xpath(selector, **kwargs)
|
||||||
|
|
||||||
if selected_elements:
|
if elements:
|
||||||
if not self.__auto_match_enabled and auto_save:
|
if auto_save:
|
||||||
|
if not self.__auto_match_enabled:
|
||||||
log.warning("Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
|
log.warning("Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
|
||||||
|
|
||||||
elif self.__auto_match_enabled and auto_save:
|
|
||||||
self.save(selected_elements[0], identifier or selector)
|
|
||||||
|
|
||||||
return self.__convert_results(selected_elements)
|
|
||||||
else:
|
else:
|
||||||
if self.__auto_match_enabled and auto_match:
|
self.save(elements[0], identifier or selector)
|
||||||
|
|
||||||
|
return self.__handle_elements(elements)
|
||||||
|
elif self.__auto_match_enabled:
|
||||||
|
if auto_match:
|
||||||
element_data = self.retrieve(identifier or selector)
|
element_data = self.retrieve(identifier or selector)
|
||||||
if element_data:
|
if element_data:
|
||||||
relocated = self.relocate(element_data, percentage)
|
elements = self.relocate(element_data, percentage)
|
||||||
if relocated is not None and auto_save:
|
if elements is not None and auto_save:
|
||||||
self.save(relocated[0], identifier or selector)
|
self.save(elements[0], identifier or selector)
|
||||||
|
|
||||||
return self.__convert_results(relocated)
|
return self.__handle_elements(elements)
|
||||||
else:
|
else:
|
||||||
return self.__convert_results(selected_elements)
|
if auto_match:
|
||||||
|
|
||||||
elif not self.__auto_match_enabled and auto_match:
|
|
||||||
log.warning("Argument `auto_match` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
|
log.warning("Argument `auto_match` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
|
||||||
|
elif auto_save:
|
||||||
|
log.warning("Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.")
|
||||||
|
|
||||||
return self.__convert_results(selected_elements)
|
return self.__handle_elements(elements)
|
||||||
|
|
||||||
except (SelectorError, SelectorSyntaxError, etree.XPathError, etree.XPathEvalError):
|
except (SelectorError, SelectorSyntaxError, etree.XPathError, etree.XPathEvalError):
|
||||||
raise SelectorSyntaxError(f"Invalid XPath selector: {selector}")
|
raise SelectorSyntaxError(f"Invalid XPath selector: {selector}")
|
||||||
|
|
||||||
def find_all(self, *args: Union[str, Iterable[str], Pattern, Callable, Dict[str, str]], **kwargs: str) -> Union['Adaptors[Adaptor]', List]:
|
def find_all(self, *args: Union[str, Iterable[str], Pattern, Callable, Dict[str, str]], **kwargs: str) -> 'Adaptors':
|
||||||
"""Find elements by filters of your creations for ease..
|
"""Find elements by filters of your creations for ease..
|
||||||
|
|
||||||
:param args: Tag name(s), an iterable of tag names, regex patterns, function, or a dictionary of elements' attributes. Leave empty for selecting all.
|
:param args: Tag name(s), an iterable of tag names, regex patterns, function, or a dictionary of elements' attributes. Leave empty for selecting all.
|
||||||
@@ -588,15 +566,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
|
|
||||||
attributes = dict()
|
attributes = dict()
|
||||||
tags, patterns = set(), set()
|
tags, patterns = set(), set()
|
||||||
results, functions, selectors = [], [], []
|
results, functions, selectors = Adaptors([]), [], []
|
||||||
|
|
||||||
def _search_tree(element: Adaptor, filter_function: Callable) -> None:
|
|
||||||
"""Collect element if it fulfills passed function otherwise, traverse the children tree and iterate"""
|
|
||||||
if filter_function(element):
|
|
||||||
results.append(element)
|
|
||||||
|
|
||||||
for branch in element.children:
|
|
||||||
_search_tree(branch, filter_function)
|
|
||||||
|
|
||||||
# Brace yourself for a wonderful journey!
|
# Brace yourself for a wonderful journey!
|
||||||
for arg in args:
|
for arg in args:
|
||||||
@@ -608,12 +578,12 @@ class Adaptor(SelectorsGeneration):
|
|||||||
raise TypeError('Nested Iterables are not accepted, only iterables of tag names are accepted')
|
raise TypeError('Nested Iterables are not accepted, only iterables of tag names are accepted')
|
||||||
tags.update(set(arg))
|
tags.update(set(arg))
|
||||||
|
|
||||||
elif type(arg) is dict:
|
elif isinstance(arg, dict):
|
||||||
if not all([(type(k) is str and type(v) is str) for k, v in arg.items()]):
|
if not all([(type(k) is str and type(v) is str) for k, v in arg.items()]):
|
||||||
raise TypeError('Nested dictionaries are not accepted, only string keys and string values are accepted')
|
raise TypeError('Nested dictionaries are not accepted, only string keys and string values are accepted')
|
||||||
attributes.update(arg)
|
attributes.update(arg)
|
||||||
|
|
||||||
elif type(arg) is re.Pattern:
|
elif isinstance(arg, re.Pattern):
|
||||||
patterns.add(arg)
|
patterns.add(arg)
|
||||||
|
|
||||||
elif callable(arg):
|
elif callable(arg):
|
||||||
@@ -634,14 +604,14 @@ class Adaptor(SelectorsGeneration):
|
|||||||
attributes[attribute_name] = value
|
attributes[attribute_name] = value
|
||||||
|
|
||||||
# It's easier and faster to build a selector than traversing the tree
|
# It's easier and faster to build a selector than traversing the tree
|
||||||
tags = tags or ['']
|
tags = tags or ['*']
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
selector = tag
|
selector = tag
|
||||||
for key, value in attributes.items():
|
for key, value in attributes.items():
|
||||||
value = value.replace('"', r'\"') # Escape double quotes in user input
|
value = value.replace('"', r'\"') # Escape double quotes in user input
|
||||||
# Not escaping anything with the key so the user can pass patterns like {'href*': '/p/'} or get errors :)
|
# Not escaping anything with the key so the user can pass patterns like {'href*': '/p/'} or get errors :)
|
||||||
selector += '[{}="{}"]'.format(key, value)
|
selector += '[{}="{}"]'.format(key, value)
|
||||||
if selector:
|
if selector != '*':
|
||||||
selectors.append(selector)
|
selectors.append(selector)
|
||||||
|
|
||||||
if selectors:
|
if selectors:
|
||||||
@@ -655,14 +625,15 @@ class Adaptor(SelectorsGeneration):
|
|||||||
for function in functions:
|
for function in functions:
|
||||||
results = results.filter(function)
|
results = results.filter(function)
|
||||||
else:
|
else:
|
||||||
|
results = results or self.below_elements
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
results.extend(self.find_by_regex(pattern, first_match=False))
|
results = results.filter(lambda e: e.text.re(pattern, check_match=True))
|
||||||
|
|
||||||
for result in (results or [self]):
|
# Collect element if it fulfills passed function otherwise
|
||||||
for function in functions:
|
for function in functions:
|
||||||
_search_tree(result, function)
|
results = results.filter(function)
|
||||||
|
|
||||||
return self.__convert_results(results)
|
return results
|
||||||
|
|
||||||
def find(self, *args: Union[str, Iterable[str], Pattern, Callable, Dict[str, str]], **kwargs: str) -> Union['Adaptor', None]:
|
def find(self, *args: Union[str, Iterable[str], Pattern, Callable, Dict[str, str]], **kwargs: str) -> Union['Adaptor', None]:
|
||||||
"""Find elements by filters of your creations for ease then return the first result. Otherwise return `None`.
|
"""Find elements by filters of your creations for ease then return the first result. Otherwise return `None`.
|
||||||
@@ -792,7 +763,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
return self.get_all_text(strip=True).json()
|
return self.get_all_text(strip=True).json()
|
||||||
|
|
||||||
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True,
|
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> 'List[str]':
|
clean_match: bool = False, case_sensitive: bool = False) -> TextHandlers:
|
||||||
"""Apply the given regex to the current text and return a list of strings with the matches.
|
"""Apply the given regex to the current text and return a list of strings with the matches.
|
||||||
|
|
||||||
:param regex: Can be either a compiled regular expression or a string.
|
:param regex: Can be either a compiled regular expression or a string.
|
||||||
@@ -803,7 +774,7 @@ class Adaptor(SelectorsGeneration):
|
|||||||
return self.text.re(regex, replace_entities, clean_match, case_sensitive)
|
return self.text.re(regex, replace_entities, clean_match, case_sensitive)
|
||||||
|
|
||||||
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> Union[str, None]:
|
clean_match: bool = False, case_sensitive: bool = False) -> TextHandler:
|
||||||
"""Apply the given regex to text and return the first match if found, otherwise return the default value.
|
"""Apply the given regex to text and return the first match if found, otherwise return the default value.
|
||||||
|
|
||||||
:param regex: Can be either a compiled regular expression or a string.
|
:param regex: Can be either a compiled regular expression or a string.
|
||||||
@@ -894,12 +865,12 @@ class Adaptor(SelectorsGeneration):
|
|||||||
if potential_match != root and are_alike(root, target_attrs, potential_match):
|
if potential_match != root and are_alike(root, target_attrs, potential_match):
|
||||||
similar_elements.append(potential_match)
|
similar_elements.append(potential_match)
|
||||||
|
|
||||||
return self.__convert_results(similar_elements)
|
return self.__handle_elements(similar_elements)
|
||||||
|
|
||||||
def find_by_text(
|
def find_by_text(
|
||||||
self, text: str, first_match: bool = True, partial: bool = False,
|
self, text: str, first_match: bool = True, partial: bool = False,
|
||||||
case_sensitive: bool = False, clean_match: bool = True
|
case_sensitive: bool = False, clean_match: bool = True
|
||||||
) -> Union['Adaptors[Adaptor]', 'Adaptor', List]:
|
) -> Union['Adaptors[Adaptor]', 'Adaptor']:
|
||||||
"""Find elements that its text content fully/partially matches input.
|
"""Find elements that its text content fully/partially matches input.
|
||||||
:param text: Text query to match
|
:param text: Text query to match
|
||||||
:param first_match: Return first element that matches conditions, enabled by default
|
:param first_match: Return first element that matches conditions, enabled by default
|
||||||
@@ -908,15 +879,14 @@ class Adaptor(SelectorsGeneration):
|
|||||||
:param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
|
:param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = []
|
results = Adaptors([])
|
||||||
if not case_sensitive:
|
if not case_sensitive:
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
|
|
||||||
def _traverse(node: Adaptor) -> None:
|
# This selector gets all elements with text content
|
||||||
|
for node in self.__handle_elements(self._root.xpath('.//*[normalize-space(text())]')):
|
||||||
"""Check if element matches given text otherwise, traverse the children tree and iterate"""
|
"""Check if element matches given text otherwise, traverse the children tree and iterate"""
|
||||||
node_text = node.text
|
node_text = node.text
|
||||||
# if there's already no text in this node, dodge it to save CPU cycles and time
|
|
||||||
if node_text:
|
|
||||||
if clean_match:
|
if clean_match:
|
||||||
node_text = node_text.clean()
|
node_text = node_text.clean()
|
||||||
|
|
||||||
@@ -929,53 +899,40 @@ class Adaptor(SelectorsGeneration):
|
|||||||
elif text == node_text:
|
elif text == node_text:
|
||||||
results.append(node)
|
results.append(node)
|
||||||
|
|
||||||
if results and first_match:
|
if first_match and results:
|
||||||
# we got an element so we should stop
|
# we got an element so we should stop
|
||||||
return
|
break
|
||||||
|
|
||||||
for branch in node.children:
|
|
||||||
_traverse(branch)
|
|
||||||
|
|
||||||
# This will block until we traverse all children/branches
|
|
||||||
_traverse(self)
|
|
||||||
|
|
||||||
if first_match:
|
if first_match:
|
||||||
if results:
|
if results:
|
||||||
return results[0]
|
return results[0]
|
||||||
return self.__convert_results(results)
|
return results
|
||||||
|
|
||||||
def find_by_regex(
|
def find_by_regex(
|
||||||
self, query: Union[str, Pattern[str]], first_match: bool = True, case_sensitive: bool = False, clean_match: bool = True
|
self, query: Union[str, Pattern[str]], first_match: bool = True, case_sensitive: bool = False, clean_match: bool = True
|
||||||
) -> Union['Adaptors[Adaptor]', 'Adaptor', List]:
|
) -> Union['Adaptors[Adaptor]', 'Adaptor']:
|
||||||
"""Find elements that its text content matches the input regex pattern.
|
"""Find elements that its text content matches the input regex pattern.
|
||||||
:param query: Regex query/pattern to match
|
:param query: Regex query/pattern to match
|
||||||
:param first_match: Return first element that matches conditions, enabled by default
|
:param first_match: Return first element that matches conditions, enabled by default
|
||||||
:param case_sensitive: if enabled, letters case will be taken into consideration in the regex
|
:param case_sensitive: if enabled, letters case will be taken into consideration in the regex
|
||||||
:param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
|
:param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
|
||||||
"""
|
"""
|
||||||
results = []
|
results = Adaptors([])
|
||||||
|
|
||||||
def _traverse(node: Adaptor) -> None:
|
# This selector gets all elements with text content
|
||||||
|
for node in self.__handle_elements(self._root.xpath('.//*[normalize-space(text())]')):
|
||||||
"""Check if element matches given regex otherwise, traverse the children tree and iterate"""
|
"""Check if element matches given regex otherwise, traverse the children tree and iterate"""
|
||||||
node_text = node.text
|
node_text = node.text
|
||||||
# if there's already no text in this node, dodge it to save CPU cycles and time
|
|
||||||
if node_text:
|
|
||||||
if node_text.re(query, check_match=True, clean_match=clean_match, case_sensitive=case_sensitive):
|
if node_text.re(query, check_match=True, clean_match=clean_match, case_sensitive=case_sensitive):
|
||||||
results.append(node)
|
results.append(node)
|
||||||
|
|
||||||
if results and first_match:
|
if first_match and results:
|
||||||
# we got an element so we should stop
|
# we got an element so we should stop
|
||||||
return
|
break
|
||||||
|
|
||||||
for branch in node.children:
|
|
||||||
_traverse(branch)
|
|
||||||
|
|
||||||
# This will block until we traverse all children/branches
|
|
||||||
_traverse(self)
|
|
||||||
|
|
||||||
if results and first_match:
|
if results and first_match:
|
||||||
return results[0]
|
return results[0]
|
||||||
return self.__convert_results(results)
|
return results
|
||||||
|
|
||||||
|
|
||||||
class Adaptors(List[Adaptor]):
|
class Adaptors(List[Adaptor]):
|
||||||
@@ -984,7 +941,15 @@ class Adaptors(List[Adaptor]):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __getitem__(self, pos: Union[SupportsIndex, slice]) -> Union[Adaptor, "Adaptors[Adaptor]"]:
|
@typing.overload
|
||||||
|
def __getitem__(self, pos: SupportsIndex) -> Adaptor:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def __getitem__(self, pos: slice) -> "Adaptors":
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, pos: Union[SupportsIndex, slice]) -> Union[Adaptor, "Adaptors"]:
|
||||||
lst = super().__getitem__(pos)
|
lst = super().__getitem__(pos)
|
||||||
if isinstance(pos, slice):
|
if isinstance(pos, slice):
|
||||||
return self.__class__(lst)
|
return self.__class__(lst)
|
||||||
@@ -993,7 +958,7 @@ class Adaptors(List[Adaptor]):
|
|||||||
|
|
||||||
def xpath(
|
def xpath(
|
||||||
self, selector: str, identifier: str = '', auto_save: bool = False, percentage: int = 0, **kwargs: Any
|
self, selector: str, identifier: str = '', auto_save: bool = False, percentage: int = 0, **kwargs: Any
|
||||||
) -> Union["Adaptors[Adaptor]", List]:
|
) -> "Adaptors[Adaptor]":
|
||||||
"""
|
"""
|
||||||
Call the ``.xpath()`` method for each element in this list and return
|
Call the ``.xpath()`` method for each element in this list and return
|
||||||
their results as another :class:`Adaptors`.
|
their results as another :class:`Adaptors`.
|
||||||
@@ -1019,7 +984,7 @@ class Adaptors(List[Adaptor]):
|
|||||||
]
|
]
|
||||||
return self.__class__(flatten(results))
|
return self.__class__(flatten(results))
|
||||||
|
|
||||||
def css(self, selector: str, identifier: str = '', auto_save: bool = False, percentage: int = 0) -> Union["Adaptors[Adaptor]", List]:
|
def css(self, selector: str, identifier: str = '', auto_save: bool = False, percentage: int = 0) -> "Adaptors[Adaptor]":
|
||||||
"""
|
"""
|
||||||
Call the ``.css()`` method for each element in this list and return
|
Call the ``.css()`` method for each element in this list and return
|
||||||
their results flattened as another :class:`Adaptors`.
|
their results flattened as another :class:`Adaptors`.
|
||||||
@@ -1044,7 +1009,7 @@ class Adaptors(List[Adaptor]):
|
|||||||
return self.__class__(flatten(results))
|
return self.__class__(flatten(results))
|
||||||
|
|
||||||
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True,
|
def re(self, regex: Union[str, Pattern[str]], replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> 'List[str]':
|
clean_match: bool = False, case_sensitive: bool = False) -> TextHandlers[TextHandler]:
|
||||||
"""Call the ``.re()`` method for each element in this list and return
|
"""Call the ``.re()`` method for each element in this list and return
|
||||||
their results flattened as List of TextHandler.
|
their results flattened as List of TextHandler.
|
||||||
|
|
||||||
@@ -1056,10 +1021,10 @@ class Adaptors(List[Adaptor]):
|
|||||||
results = [
|
results = [
|
||||||
n.text.re(regex, replace_entities, clean_match, case_sensitive) for n in self
|
n.text.re(regex, replace_entities, clean_match, case_sensitive) for n in self
|
||||||
]
|
]
|
||||||
return flatten(results)
|
return TextHandlers(flatten(results))
|
||||||
|
|
||||||
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
def re_first(self, regex: Union[str, Pattern[str]], default=None, replace_entities: bool = True,
|
||||||
clean_match: bool = False, case_sensitive: bool = False) -> Union[str, None]:
|
clean_match: bool = False, case_sensitive: bool = False) -> TextHandler:
|
||||||
"""Call the ``.re_first()`` method for each element in this list and return
|
"""Call the ``.re_first()`` method for each element in this list and return
|
||||||
the first result or the default value otherwise.
|
the first result or the default value otherwise.
|
||||||
|
|
||||||
@@ -1084,15 +1049,14 @@ class Adaptors(List[Adaptor]):
|
|||||||
return element
|
return element
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def filter(self, func: Callable[['Adaptor'], bool]) -> Union['Adaptors', List]:
|
def filter(self, func: Callable[['Adaptor'], bool]) -> 'Adaptors[Adaptor]':
|
||||||
"""Filter current elements based on the passed function
|
"""Filter current elements based on the passed function
|
||||||
:param func: A function that takes each element as an argument and returns True/False
|
:param func: A function that takes each element as an argument and returns True/False
|
||||||
:return: The new `Adaptors` object or empty list otherwise.
|
:return: The new `Adaptors` object or empty list otherwise.
|
||||||
"""
|
"""
|
||||||
results = [
|
return self.__class__([
|
||||||
element for element in self if func(element)
|
element for element in self if func(element)
|
||||||
]
|
])
|
||||||
return self.__class__(results) if results else results
|
|
||||||
|
|
||||||
# For easy copy-paste from Scrapy/parsel code when needed :)
|
# For easy copy-paste from Scrapy/parsel code when needed :)
|
||||||
def get(self, default=None):
|
def get(self, default=None):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = scrapling
|
name = scrapling
|
||||||
version = 0.2.92
|
version = 0.2.93
|
||||||
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, adaptive, and high-performance web scraping library for Python.
|
description = Scrapling is an undetectable, powerful, flexible, adaptive, and high-performance web scraping library for Python.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="scrapling",
|
name="scrapling",
|
||||||
version="0.2.92",
|
version="0.2.93",
|
||||||
description="""Scrapling is a powerful, flexible, and high-performance web scraping library for Python. It
|
description="""Scrapling is a powerful, flexible, and high-performance web scraping library for Python. It
|
||||||
simplifies the process of extracting data from websites, even when they undergo structural changes, and offers
|
simplifies the process of extracting data from websites, even when they undergo structural changes, and offers
|
||||||
impressive speed improvements over many popular scraping tools.""",
|
impressive speed improvements over many popular scraping tools.""",
|
||||||
@@ -52,8 +52,7 @@ setup(
|
|||||||
],
|
],
|
||||||
# Instead of using requirements file to dodge possible errors from tox?
|
# Instead of using requirements file to dodge possible errors from tox?
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"requests>=2.3",
|
"lxml>=5.0",
|
||||||
"lxml>=4.5",
|
|
||||||
"cssselect>=1.2",
|
"cssselect>=1.2",
|
||||||
'click',
|
'click',
|
||||||
"w3lib",
|
"w3lib",
|
||||||
@@ -62,7 +61,7 @@ setup(
|
|||||||
'httpx[brotli,zstd, socks]',
|
'httpx[brotli,zstd, socks]',
|
||||||
'playwright>=1.49.1',
|
'playwright>=1.49.1',
|
||||||
'rebrowser-playwright>=1.49.1',
|
'rebrowser-playwright>=1.49.1',
|
||||||
'camoufox[geoip]>=0.4.9'
|
'camoufox[geoip]>=0.4.10'
|
||||||
],
|
],
|
||||||
python_requires=">=3.9",
|
python_requires=">=3.9",
|
||||||
url="https://github.com/D4Vinci/Scrapling",
|
url="https://github.com/D4Vinci/Scrapling",
|
||||||
|
|||||||
Reference in New Issue
Block a user