Adding new find_all and find functions to parser
This commit is contained in:
+68
-2
@@ -5,8 +5,8 @@ from scrapling.core.translator import HTMLTranslator
|
|||||||
from scrapling.core.mixins import SelectorsGeneration
|
from scrapling.core.mixins import SelectorsGeneration
|
||||||
from scrapling.core.custom_types import TextHandler, TextHandlers, AttributesHandler
|
from scrapling.core.custom_types import TextHandler, TextHandlers, AttributesHandler
|
||||||
from scrapling.core.storage_adaptors import SQLiteStorageSystem, StorageSystemMixin, _StorageTools
|
from scrapling.core.storage_adaptors import SQLiteStorageSystem, StorageSystemMixin, _StorageTools
|
||||||
from scrapling.core.utils import setup_basic_logging, logging, clean_spaces, flatten, html_forbidden
|
from scrapling.core.utils import setup_basic_logging, logging, clean_spaces, flatten, _is_iterable, html_forbidden
|
||||||
from scrapling.core._types import Any, Dict, List, Tuple, Optional, Pattern, Union, Callable, Generator, SupportsIndex
|
from scrapling.core._types import Any, Dict, List, Tuple, Optional, Pattern, Union, Callable, Generator, SupportsIndex, Iterable
|
||||||
|
|
||||||
from lxml import etree, html
|
from lxml import etree, html
|
||||||
from cssselect import SelectorError, SelectorSyntaxError, parse as split_selectors
|
from cssselect import SelectorError, SelectorSyntaxError, parse as split_selectors
|
||||||
@@ -542,6 +542,72 @@ class Adaptor(SelectorsGeneration):
|
|||||||
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, **kwargs) -> Union['Adaptors[Adaptor]', List]:
|
||||||
|
"""Find elements by their tag name and filter them based on attributes for ease..
|
||||||
|
|
||||||
|
:param args: Tag name(s), an iterable of tag names, or a dictionary of elements' attributes. Leave empty for selecting all.
|
||||||
|
:param kwargs: The attributes you want to filter elements based on it.
|
||||||
|
:return: The `Adaptors` object of the elements or empty list
|
||||||
|
"""
|
||||||
|
# Attributes that are Python reserved words and can't be used directly
|
||||||
|
# Ex: find_all('a', class="blah") -> find_all('a', class_="blah")
|
||||||
|
whitelisted = {
|
||||||
|
'id_': 'id',
|
||||||
|
'class_': 'class',
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args and not kwargs:
|
||||||
|
raise TypeError('You have to pass something to search with, like tag name(s), tag attributes, or both.')
|
||||||
|
|
||||||
|
tags = set()
|
||||||
|
selectors = []
|
||||||
|
attributes = dict()
|
||||||
|
# Brace yourself for a wonderful journey!
|
||||||
|
for arg in args:
|
||||||
|
if type(arg) is str:
|
||||||
|
tags.add(arg)
|
||||||
|
|
||||||
|
elif type(arg) in [list, tuple, set]:
|
||||||
|
if not all(map(lambda x: type(x) is str, arg)):
|
||||||
|
raise TypeError('Nested Iterables are not accepted, only iterables of tag names are accepted')
|
||||||
|
tags.update(set(arg))
|
||||||
|
|
||||||
|
elif type(arg) is dict:
|
||||||
|
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')
|
||||||
|
attributes.update(arg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError(f'Argument with type "{type(arg)}" is not accepted, please read the docs.')
|
||||||
|
|
||||||
|
if not all([(type(k) is str and type(v) is str) for k, v in kwargs.items()]):
|
||||||
|
raise TypeError('Only string values are accepted for arguments')
|
||||||
|
attributes.update(kwargs)
|
||||||
|
|
||||||
|
# It's easier and faster to build a selector than traversing the tree
|
||||||
|
tags = tags or ['']
|
||||||
|
for tag in tags:
|
||||||
|
selector = tag
|
||||||
|
for key, value in attributes.items():
|
||||||
|
key = whitelisted.get(key, key)
|
||||||
|
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 :)
|
||||||
|
selector += '[{}="{}"]'.format(key, value)
|
||||||
|
selectors.append(selector)
|
||||||
|
|
||||||
|
return self.css(', '.join(selectors))
|
||||||
|
|
||||||
|
def find(self, *args, **kwargs) -> Union['Adaptor', None]:
|
||||||
|
"""Find elements by their tag name and filter them based on attributes for ease then return the first result. Otherwise return `None`.
|
||||||
|
|
||||||
|
:param args: Tag name(s), an iterable of tag names, or a dictionary of elements' attributes. Leave empty for selecting all.
|
||||||
|
:param kwargs: The attributes you want to filter elements based on it.
|
||||||
|
:return: The `Adaptor` object of the element or `None` if the result didn't match
|
||||||
|
"""
|
||||||
|
for element in self.find_all(*args, **kwargs):
|
||||||
|
return element
|
||||||
|
return None
|
||||||
|
|
||||||
def __calculate_similarity_score(self, original: Dict, candidate: html.HtmlElement) -> float:
|
def __calculate_similarity_score(self, original: Dict, candidate: html.HtmlElement) -> float:
|
||||||
"""Used internally to calculate a score that shows how candidate element similar to the original one
|
"""Used internally to calculate a score that shows how candidate element similar to the original one
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user