perf(parser): A lot of optimizations to speed things up
This commit is contained in:
@@ -249,7 +249,6 @@ class TextHandlers(List[TextHandler]):
|
|||||||
) -> Union[TextHandler, "TextHandlers"]:
|
) -> Union[TextHandler, "TextHandlers"]:
|
||||||
lst = super().__getitem__(pos)
|
lst = super().__getitem__(pos)
|
||||||
if isinstance(pos, slice):
|
if isinstance(pos, slice):
|
||||||
lst = [TextHandler(s) for s in lst]
|
|
||||||
return TextHandlers(cast(List[_TextHandlerType], lst))
|
return TextHandlers(cast(List[_TextHandlerType], lst))
|
||||||
return cast(_TextHandlerType, TextHandler(lst))
|
return cast(_TextHandlerType, TextHandler(lst))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# 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
|
from functools import lru_cache # isort:skip
|
||||||
|
|
||||||
html_forbidden = {
|
html_forbidden = (html.HtmlComment,)
|
||||||
html.HtmlComment,
|
|
||||||
}
|
|
||||||
|
|
||||||
__CLEANING_TABLE__ = str.maketrans({"\t": " ", "\n": None, "\r": None})
|
__CLEANING_TABLE__ = str.maketrans({"\t": " ", "\n": None, "\r": None})
|
||||||
__CONSECUTIVE_SPACES_REGEX__ = re_compile(r" +")
|
__CONSECUTIVE_SPACES_REGEX__ = re_compile(r" +")
|
||||||
@@ -108,7 +106,7 @@ class _StorageTools:
|
|||||||
children = [
|
children = [
|
||||||
child.tag
|
child.tag
|
||||||
for child in element.iterchildren()
|
for child in element.iterchildren()
|
||||||
if type(child) not in html_forbidden
|
if not isinstance(child, html_forbidden)
|
||||||
]
|
]
|
||||||
if children:
|
if children:
|
||||||
result.update({"children": tuple(children)})
|
result.update({"children": tuple(children)})
|
||||||
|
|||||||
+23
-18
@@ -37,7 +37,7 @@ from scrapling.core.storage import (
|
|||||||
_StorageTools,
|
_StorageTools,
|
||||||
)
|
)
|
||||||
from scrapling.core.translator import translator as _translator
|
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")
|
__DEFAULT_DB_FILE__ = str(Path(__file__).parent / "elements_storage.db")
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ class Selector(SelectorsGeneration):
|
|||||||
"__text",
|
"__text",
|
||||||
"__tag",
|
"__tag",
|
||||||
"__keep_cdata",
|
"__keep_cdata",
|
||||||
|
"_raw_body",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -102,12 +103,12 @@ class Selector(SelectorsGeneration):
|
|||||||
|
|
||||||
self.__text = ""
|
self.__text = ""
|
||||||
if root is None:
|
if root is None:
|
||||||
if isinstance(content, bytes):
|
if isinstance(content, str):
|
||||||
body = content.replace(b"\x00", b"").strip()
|
|
||||||
elif isinstance(content, str):
|
|
||||||
body = (
|
body = (
|
||||||
content.strip().replace("\x00", "").encode(encoding) or b"<html/>"
|
content.strip().replace("\x00", "").encode(encoding) or b"<html/>"
|
||||||
)
|
)
|
||||||
|
elif isinstance(content, bytes):
|
||||||
|
body = content.replace(b"\x00", b"").strip()
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"content argument must be str or bytes, got {type(content)}"
|
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)
|
self._root = fromstring(body, parser=parser, base_url=url)
|
||||||
|
|
||||||
jsonable_text = content if isinstance(content, str) else body.decode()
|
self._raw_body = body.decode()
|
||||||
if is_jsonable(jsonable_text):
|
|
||||||
self.__text = TextHandler(jsonable_text)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# All HTML types inherit from HtmlMixin so this to check for all at once
|
# 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._root = root
|
||||||
|
self._raw_body = ""
|
||||||
|
|
||||||
self.__adaptive_enabled = adaptive
|
self.__adaptive_enabled = adaptive
|
||||||
|
|
||||||
@@ -171,8 +171,12 @@ class Selector(SelectorsGeneration):
|
|||||||
# For selector stuff
|
# For selector stuff
|
||||||
self.__attributes = None
|
self.__attributes = None
|
||||||
self.__tag = 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)
|
# 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 = (
|
if not hasattr(self, "_cached_response_data"):
|
||||||
|
self._cached_response_data = (
|
||||||
{
|
{
|
||||||
key: getattr(self, key)
|
key: getattr(self, key)
|
||||||
for key in (
|
for key in (
|
||||||
@@ -187,6 +191,7 @@ class Selector(SelectorsGeneration):
|
|||||||
if hasattr(self, "status")
|
if hasattr(self, "status")
|
||||||
else {}
|
else {}
|
||||||
)
|
)
|
||||||
|
return self._cached_response_data
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> TextHandler:
|
def __getitem__(self, key: str) -> TextHandler:
|
||||||
return self.attrib[key]
|
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
|
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":
|
def __element_convertor(self, element: HtmlElement) -> "Selector":
|
||||||
"""Used internally to convert a single HtmlElement to Selector directly without checks"""
|
"""Used internally to convert a single HtmlElement to Selector directly without checks"""
|
||||||
@@ -250,15 +255,13 @@ class Selector(SelectorsGeneration):
|
|||||||
self, result: List[HtmlElement | _ElementUnicodeResult]
|
self, result: List[HtmlElement | _ElementUnicodeResult]
|
||||||
) -> Union["Selectors", "TextHandlers"]:
|
) -> Union["Selectors", "TextHandlers"]:
|
||||||
"""Used internally in all functions to convert results to type (Selectors|TextHandlers) in bulk when possible"""
|
"""Used internally in all functions to convert results to type (Selectors|TextHandlers) in bulk when possible"""
|
||||||
if not len(
|
if not result:
|
||||||
result
|
|
||||||
): # Lxml will give a warning if I used something like `not result`
|
|
||||||
return Selectors()
|
return Selectors()
|
||||||
|
|
||||||
# From within the code, this method will always get a list of the same type,
|
# 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
|
# so we will continue without checks for a slight performance boost
|
||||||
if self._is_text_node(result[0]):
|
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))
|
return Selectors(map(self.__element_convertor, result))
|
||||||
|
|
||||||
@@ -380,7 +383,7 @@ class Selector(SelectorsGeneration):
|
|||||||
return Selectors(
|
return Selectors(
|
||||||
self.__element_convertor(child)
|
self.__element_convertor(child)
|
||||||
for child in self._root.iterchildren()
|
for child in self._root.iterchildren()
|
||||||
if type(child) not in html_forbidden
|
if not isinstance(child, html_forbidden)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@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."""
|
"""Returns the next element of the current element in the children of the parent or ``None`` otherwise."""
|
||||||
next_element = self._root.getnext()
|
next_element = self._root.getnext()
|
||||||
if next_element is not None:
|
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
|
# Ignore HTML comments and unwanted types
|
||||||
next_element = next_element.getnext()
|
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."""
|
"""Returns the previous element of the current element in the children of the parent or ``None`` otherwise."""
|
||||||
prev_element = self._root.getprevious()
|
prev_element = self._root.getprevious()
|
||||||
if prev_element is not None:
|
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
|
# Ignore HTML comments and unwanted types
|
||||||
prev_element = prev_element.getprevious()
|
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."
|
"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
|
"""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
|
: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
|
# Operations on text functions
|
||||||
def json(self) -> Dict:
|
def json(self) -> Dict:
|
||||||
"""Return JSON response if the response is jsonable otherwise throws error"""
|
"""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()
|
return self.text.json()
|
||||||
else:
|
else:
|
||||||
return self.get_all_text(strip=True).json()
|
return self.get_all_text(strip=True).json()
|
||||||
|
|||||||
Reference in New Issue
Block a user