perf(parser): A lot of optimizations to speed things up

This commit is contained in:
Karim shoair
2025-08-01 05:42:32 +03:00
parent af5f3688c1
commit df48662c00
3 changed files with 39 additions and 37 deletions
-1
View File
@@ -249,7 +249,6 @@ class TextHandlers(List[TextHandler]):
) -> Union[TextHandler, "TextHandlers"]:
lst = super().__getitem__(pos)
if isinstance(pos, slice):
lst = [TextHandler(s) for s in lst]
return TextHandlers(cast(List[_TextHandlerType], lst))
return cast(_TextHandlerType, TextHandler(lst))
+2 -4
View File
@@ -10,9 +10,7 @@ from scrapling.core._types import Any, Dict, Iterable, List
# Using cache on top of a class is a brilliant way to achieve a Singleton design pattern without much code
from functools import lru_cache # isort:skip
html_forbidden = {
html.HtmlComment,
}
html_forbidden = (html.HtmlComment,)
__CLEANING_TABLE__ = str.maketrans({"\t": " ", "\n": None, "\r": None})
__CONSECUTIVE_SPACES_REGEX__ = re_compile(r" +")
@@ -108,7 +106,7 @@ class _StorageTools:
children = [
child.tag
for child in element.iterchildren()
if type(child) not in html_forbidden
if not isinstance(child, html_forbidden)
]
if children:
result.update({"children": tuple(children)})
+37 -32
View File
@@ -37,7 +37,7 @@ from scrapling.core.storage import (
_StorageTools,
)
from scrapling.core.translator import translator as _translator
from scrapling.core.utils import clean_spaces, flatten, html_forbidden, is_jsonable, log
from scrapling.core.utils import clean_spaces, flatten, html_forbidden, log
__DEFAULT_DB_FILE__ = str(Path(__file__).parent / "elements_storage.db")
@@ -55,6 +55,7 @@ class Selector(SelectorsGeneration):
"__text",
"__tag",
"__keep_cdata",
"_raw_body",
)
def __init__(
@@ -102,12 +103,12 @@ class Selector(SelectorsGeneration):
self.__text = ""
if root is None:
if isinstance(content, bytes):
body = content.replace(b"\x00", b"").strip()
elif isinstance(content, str):
if isinstance(content, str):
body = (
content.strip().replace("\x00", "").encode(encoding) or b"<html/>"
)
elif isinstance(content, bytes):
body = content.replace(b"\x00", b"").strip()
else:
raise TypeError(
f"content argument must be str or bytes, got {type(content)}"
@@ -126,9 +127,7 @@ class Selector(SelectorsGeneration):
)
self._root = fromstring(body, parser=parser, base_url=url)
jsonable_text = content if isinstance(content, str) else body.decode()
if is_jsonable(jsonable_text):
self.__text = TextHandler(jsonable_text)
self._raw_body = body.decode()
else:
# All HTML types inherit from HtmlMixin so this to check for all at once
@@ -138,6 +137,7 @@ class Selector(SelectorsGeneration):
)
self._root = root
self._raw_body = ""
self.__adaptive_enabled = adaptive
@@ -171,22 +171,27 @@ class Selector(SelectorsGeneration):
# 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)
self.__response_data = (
{
key: getattr(self, key)
for key in (
"status",
"reason",
"cookies",
"history",
"headers",
"request_headers",
)
}
if hasattr(self, "status")
else {}
)
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:
return self.attrib[key]
@@ -215,7 +220,7 @@ class Selector(SelectorsGeneration):
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))
return TextHandler(element)
def __element_convertor(self, element: HtmlElement) -> "Selector":
"""Used internally to convert a single HtmlElement to Selector directly without checks"""
@@ -250,15 +255,13 @@ class Selector(SelectorsGeneration):
self, result: List[HtmlElement | _ElementUnicodeResult]
) -> Union["Selectors", "TextHandlers"]:
"""Used internally in all functions to convert results to type (Selectors|TextHandlers) in bulk when possible"""
if not len(
result
): # Lxml will give a warning if I used something like `not result`
if not result:
return Selectors()
# From within the code, this method will always get a list of the same type,
# so we will continue without checks for a slight performance boost
if self._is_text_node(result[0]):
return TextHandlers(list(map(self.__content_convertor, result)))
return TextHandlers(map(TextHandler, result))
return Selectors(map(self.__element_convertor, result))
@@ -380,7 +383,7 @@ class Selector(SelectorsGeneration):
return Selectors(
self.__element_convertor(child)
for child in self._root.iterchildren()
if type(child) not in html_forbidden
if not isinstance(child, html_forbidden)
)
@property
@@ -418,7 +421,7 @@ class Selector(SelectorsGeneration):
"""Returns the next element of the current element in the children of the parent or ``None`` otherwise."""
next_element = self._root.getnext()
if next_element is not None:
while type(next_element) in html_forbidden:
while isinstance(next_element, html_forbidden):
# Ignore HTML comments and unwanted types
next_element = next_element.getnext()
@@ -429,7 +432,7 @@ class Selector(SelectorsGeneration):
"""Returns the previous element of the current element in the children of the parent or ``None`` otherwise."""
prev_element = self._root.getprevious()
if prev_element is not None:
while type(prev_element) in html_forbidden:
while isinstance(prev_element, html_forbidden):
# Ignore HTML comments and unwanted types
prev_element = prev_element.getprevious()
@@ -947,7 +950,7 @@ class Selector(SelectorsGeneration):
"Can't use Auto-match features while disabled globally, you have to start a new class instance."
)
def retrieve(self, identifier: str) -> Optional[Dict]:
def retrieve(self, identifier: str) -> Optional[Dict[str, Any]]:
"""Using the identifier, we search the storage and return the unique properties of the element
:param identifier: This is the identifier that will be used to retrieve the element from the storage. See
@@ -965,7 +968,9 @@ class Selector(SelectorsGeneration):
# Operations on text functions
def json(self) -> Dict:
"""Return JSON response if the response is jsonable otherwise throws error"""
if self.text:
if self._raw_body:
return TextHandler(self._raw_body).json()
elif self.text:
return self.text.json()
else:
return self.get_all_text(strip=True).json()