fix(parser)!: Optimize parser for repeated operations

This commit is contained in:
Karim shoair
2026-02-14 03:34:13 +02:00
parent d0a5a01299
commit e785cf26ce
+39 -61
View File
@@ -4,7 +4,7 @@ from urllib.parse import urljoin
from difflib import SequenceMatcher from difflib import SequenceMatcher
from re import Pattern as re_Pattern from re import Pattern as re_Pattern
from lxml.html import HtmlElement, HtmlMixin, HTMLParser from lxml.html import HtmlElement, HTMLParser
from cssselect import SelectorError, SelectorSyntaxError, parse as split_selectors from cssselect import SelectorError, SelectorSyntaxError, parse as split_selectors
from lxml.etree import ( from lxml.etree import (
XPath, XPath,
@@ -76,9 +76,6 @@ class Selector(SelectorsGeneration):
"_raw_body", "_raw_body",
) )
if TYPE_CHECKING:
_storage: StorageSystemMixin
def __init__( def __init__(
self, self,
content: Optional[str | bytes] = None, content: Optional[str | bytes] = None,
@@ -120,9 +117,17 @@ class Selector(SelectorsGeneration):
if root is None and content is None: if root is None and content is None:
raise ValueError("Selector class needs HTML content, or root arguments to work") raise ValueError("Selector class needs HTML content, or root arguments to work")
self.url = url
self._raw_body: str | bytes = ""
self.encoding = encoding
self.__keep_cdata = keep_cdata
self.__huge_tree_enabled = huge_tree
self.__keep_comments = keep_comments
# For selector stuff
self.__text: Optional[TextHandler] = None self.__text: Optional[TextHandler] = None
self.__tag: Optional[str] = None
self.__attributes: Optional[AttributesHandler] = None self.__attributes: Optional[AttributesHandler] = None
self.__tag: Optional[str] = None
self._storage: Optional[StorageSystemMixin] = None
if root is None: if root is None:
body: str | bytes body: str | bytes
if isinstance(content, str): if isinstance(content, str):
@@ -148,27 +153,11 @@ class Selector(SelectorsGeneration):
self._raw_body = content self._raw_body = content
else: else:
if self._is_text_node(root):
# Text node (from ::text, /text(), ::attr(), /@attr, etc.)
self._root = root
self._raw_body = ""
self.__adaptive_enabled = False
self.__keep_comments = keep_comments
self.__keep_cdata = keep_cdata
self.__huge_tree_enabled = huge_tree
self.encoding = encoding
self.url = url
self.__attributes = None
self.__tag = None
return
# All HTML types inherit from HtmlMixin so this to check for all at once
elif not issubclass(type(root), HtmlMixin):
raise TypeError(
f"Root have to be a valid element of `html` module types to work, not of type {type(root)}"
)
self._root = cast(HtmlElement, root) self._root = cast(HtmlElement, root)
self._raw_body = ""
if self._is_text_node(root):
self.__adaptive_enabled = False
return
self.__adaptive_enabled = bool(adaptive) self.__adaptive_enabled = bool(adaptive)
@@ -190,36 +179,6 @@ class Selector(SelectorsGeneration):
self._storage = storage(**storage_args) self._storage = storage(**storage_args)
self.__keep_comments = keep_comments
self.__keep_cdata = keep_cdata
self.__huge_tree_enabled = huge_tree
self.encoding = encoding
self.url = url
# For selector stuff
self.__attributes = None
self.__tag = None
@property
def __response_data(self):
# No need to check if all response attributes exist or not because if `status` exist, then the rest exist (Save some CPU cycles for speed)
if not hasattr(self, "_cached_response_data"):
self._cached_response_data = (
{
key: getattr(self, key)
for key in (
"status",
"reason",
"cookies",
"history",
"headers",
"request_headers",
)
}
if hasattr(self, "status")
else {}
)
return self._cached_response_data
def __getitem__(self, key: str) -> TextHandler: def __getitem__(self, key: str) -> TextHandler:
if self._is_text_node(self._root): if self._is_text_node(self._root):
raise TypeError("Text nodes do not have attributes") raise TypeError("Text nodes do not have attributes")
@@ -245,21 +204,40 @@ class Selector(SelectorsGeneration):
def __element_convertor(self, element: HtmlElement | _ElementUnicodeResult) -> "Selector": def __element_convertor(self, element: HtmlElement | _ElementUnicodeResult) -> "Selector":
"""Used internally to convert a single HtmlElement or text node to Selector directly without checks""" """Used internally to convert a single HtmlElement or text node to Selector directly without checks"""
db_instance = self._storage if (hasattr(self, "_storage") and self._storage) else None
return Selector( return Selector(
root=element, root=element,
url=self.url, url=self.url,
encoding=self.encoding, encoding=self.encoding,
adaptive=self.__adaptive_enabled, adaptive=self.__adaptive_enabled,
_storage=db_instance, # Reuse existing storage if it exists otherwise it won't be checked if `adaptive` is turned off _storage=self._storage,
keep_comments=self.__keep_comments, keep_comments=self.__keep_comments,
keep_cdata=self.__keep_cdata, keep_cdata=self.__keep_cdata,
huge_tree=self.__huge_tree_enabled, huge_tree=self.__huge_tree_enabled,
**self.__response_data,
) )
def __elements_convertor(self, elements: List[HtmlElement | _ElementUnicodeResult]) -> "Selectors": def __elements_convertor(self, elements: List[HtmlElement | _ElementUnicodeResult]) -> "Selectors":
return Selectors(map(self.__element_convertor, elements)) # Store them for non-repeated call-ups
url = self.url
encoding = self.encoding
adaptive = self.__adaptive_enabled
storage = self._storage
comments = self.__keep_comments
cdata = self.__keep_cdata
huge_tree = self.__huge_tree_enabled
return Selectors(
Selector(
root=el,
url=url,
encoding=encoding,
adaptive=adaptive,
_storage=storage,
keep_comments=comments,
keep_cdata=cdata,
huge_tree=huge_tree,
)
for el in elements
)
def __handle_elements(self, result: List[HtmlElement | _ElementUnicodeResult]) -> "Selectors": def __handle_elements(self, result: List[HtmlElement | _ElementUnicodeResult]) -> "Selectors":
"""Used internally in all functions to convert results to Selectors in bulk""" """Used internally in all functions to convert results to Selectors in bulk"""
@@ -889,7 +867,7 @@ class Selector(SelectorsGeneration):
:param identifier: This is the identifier that will be used to retrieve the element later from the storage. See :param identifier: This is the identifier that will be used to retrieve the element later from the storage. See
the docs for more info. the docs for more info.
""" """
if self.__adaptive_enabled: if self.__adaptive_enabled and self._storage:
target_element: Any = element target_element: Any = element
if isinstance(target_element, self.__class__): if isinstance(target_element, self.__class__):
target_element = target_element._root target_element = target_element._root
@@ -910,7 +888,7 @@ class Selector(SelectorsGeneration):
the docs for more info. the docs for more info.
:return: A dictionary of the unique properties :return: A dictionary of the unique properties
""" """
if self.__adaptive_enabled: if self.__adaptive_enabled and self._storage:
return self._storage.retrieve(identifier) return self._storage.retrieve(identifier)
raise RuntimeError( raise RuntimeError(