Saltar a contenido

Clase Response

La clase Response contiene las respuestas HTTP de todos los fetchers. Permite acceder al estado, las cabeceras, el contenido y las cookies, y usar un Selector para analizar la respuesta.

Puedes importar la clase Response así:

from scrapling.engines.toolbelt.custom import Response

scrapling.engines.toolbelt.custom.Response

Response(
    url,
    content,
    status,
    reason,
    cookies,
    headers,
    request_headers,
    encoding="utf-8",
    method="GET",
    history=None,
    meta=None,
    *,
    _log=True,
    **selector_config
)

Clases base: Selector


              flowchart TD
              scrapling.engines.toolbelt.custom.Response[Response]
              scrapling.parser.Selector[Selector]
              scrapling.core.mixins.SelectorsGeneration[SelectorsGeneration]

                              scrapling.parser.Selector --> scrapling.engines.toolbelt.custom.Response
                                scrapling.core.mixins.SelectorsGeneration --> scrapling.parser.Selector
                



              click scrapling.engines.toolbelt.custom.Response href "" "scrapling.engines.toolbelt.custom.Response"
              click scrapling.parser.Selector href "" "scrapling.parser.Selector"
              click scrapling.core.mixins.SelectorsGeneration href "" "scrapling.core.mixins.SelectorsGeneration"
            

Todos los motores devuelven esta clase para usar el mismo tipo de respuesta entre las distintas bibliotecas.

PARÁMETRO DESCRIPCIÓN
status

Código de estado HTTP.

TIPO: int

reason

Mensaje de estado HTTP.

TIPO: str

cookies

Cookies de la respuesta.

TIPO: Tuple[Dict[str, str], ...] | Dict[str, str]

headers

Cabeceras de la respuesta.

TIPO: Dict

request_headers

Cabeceras enviadas con la solicitud.

TIPO: Dict

history

Lista de respuestas de redirección, si las hay.

TIPO: List | None PREDETERMINADO: None

meta

Diccionario de metadatos, como el proxy usado.

TIPO: Dict[str, Any] | None PREDETERMINADO: None

request

Objeto Request del spider asociado, que establece el rastreador del sistema de spiders.

Clase principal que envuelve los datos HTML de entrada. Permite buscar elementos con CSS, XPath o texto. Consulta la documentación para más información.

Esta clase amplía las funciones de lxml.html.HtmlElement con una interfaz más sencilla. No hereda de lxml.html.HtmlElement porque no se puede serializar con pickle, lo que impide varias operaciones que usan referencias. Si lo pruebas, obtendrás AssertionError: invalid Element proxy at.... Es un problema antiguo de lxml; consulta this entry <https://bugs.launchpad.net/lxml/+bug/736708>.

PARÁMETRO DESCRIPCIÓN
content

Contenido HTML como cadena o bytes.

TIPO: str | bytes

url

Permite guardar una URL junto con los datos HTML para recuperarla después.

TIPO: str

encoding

Codificación que se usa para analizar el HTML. El valor predeterminado es UTF-8.

TIPO: str PREDETERMINADO: 'utf-8'

huge_tree

Está activado de forma predeterminada y debe mantenerse activo al analizar documentos HTML grandes. Controla la función de libxml2 que impide analizar ciertos documentos grandes para evitar que se agote la memoria.

root

Se usa internamente para pasar objetos etree en lugar de los argumentos de texto o cuerpo, y tiene la mayor prioridad. Úsalo solo si sabes cómo afecta al proceso.

keep_comments

Indica si se conservan los comentarios al analizar el HTML. Está desactivado de forma predeterminada.

adaptive

Desactiva la función adaptativa de forma global en todos los métodos. Tiene prioridad sobre todos los argumentos y métodos de la clase relacionados con esta función.

storage

Clase de almacenamiento que se usa para las funciones adaptativas. Consulta Docs para más información.

storage_args

Diccionario de pares argument->value que se pasa a la clase de almacenamiento. Si está vacío, se usan los valores predeterminados.

Código fuente en scrapling/engines/toolbelt/custom.py
def __init__(
    self,
    url: str,
    content: str | bytes,
    status: int,
    reason: str,
    cookies: Tuple[Dict[str, str], ...] | Dict[str, str],
    headers: Dict,
    request_headers: Dict,
    encoding: str = "utf-8",
    method: str = "GET",
    history: List | None = None,
    meta: Dict[str, Any] | None = None,
    *,
    _log: bool = True,
    **selector_config: Any,
):
    if isinstance(content, str):
        content = content.encode("utf-8")

    adaptive_domain: str = cast(str, selector_config.pop("adaptive_domain", ""))
    self.status = status
    self.method = method
    self.reason = reason
    self.cookies = cookies
    self.headers = headers
    self.request_headers = request_headers
    self.history = history or []
    super().__init__(
        content=content,
        url=adaptive_domain or url,
        encoding=encoding,
        **selector_config,
    )
    if _log:
        log.info(f"Fetched ({status}) <{method} {url}> (referer: {request_headers.get('referer')})")

    if meta and not isinstance(meta, dict):
        raise TypeError(f"Response meta should be dictionary but got {type(meta).__name__} instead!")

    self.meta: Dict[str, Any] = meta or {}
    self.request: Optional["Request"] = None  # Will be set by crawler

status instance-attribute

status = status

method instance-attribute

method = method

reason instance-attribute

reason = reason

cookies instance-attribute

cookies = cookies

headers instance-attribute

headers = headers

request_headers instance-attribute

request_headers = request_headers

history instance-attribute

history = history or []

meta instance-attribute

meta = meta or {}

request instance-attribute

request = None

body property

body

Devuelve el cuerpo original del Selector actual sin procesarlo. Es útil para solicitudes de contenido binario o que no sea HTML.

Devuelve el cuerpo original de la respuesta como bytes.

generate_css_selector property

generate_css_selector

Genera un selector CSS para el elemento actual.

DEVUELVE DESCRIPCIÓN
str

Cadena con el selector generado.

generate_full_css_selector property

generate_full_css_selector

Genera un selector CSS completo para el elemento actual.

DEVUELVE DESCRIPCIÓN
str

Cadena con el selector generado.

generate_xpath_selector property

generate_xpath_selector

Genera un selector XPath para el elemento actual.

DEVUELVE DESCRIPCIÓN
str

Cadena con el selector generado.

generate_full_xpath_selector property

generate_full_xpath_selector

Genera un selector XPath completo para el elemento actual.

DEVUELVE DESCRIPCIÓN
str

Cadena con el selector generado.

__slots__ class-attribute instance-attribute

__slots__ = (
    "url",
    "encoding",
    "__adaptive_enabled",
    "_root",
    "_storage",
    "__keep_comments",
    "__huge_tree_enabled",
    "__attributes",
    "__text",
    "__tag",
    "_raw_body",
)

url instance-attribute

url = url

encoding instance-attribute

encoding = encoding

tag property

tag

Obtiene el nombre de la etiqueta del elemento.

text property

text

Obtiene el texto del elemento.

attrib property

attrib

Obtiene los atributos del elemento.

html_content property

html_content

Devuelve el código HTML interno del elemento.

parent property

parent

Devuelve el padre directo del elemento, o None si no tiene.

below_elements property

below_elements

Devuelve todos los elementos que están bajo el elemento actual en el árbol DOM.

children property

children

Devuelve los elementos hijos del elemento actual, o una lista vacía si no tiene.

siblings property

siblings

Devuelve los otros hijos del padre del elemento actual, o una lista vacía si no hay ninguno.

path property

path

Devuelve una lista de tipo Selectors con el camino desde la raíz hasta el elemento actual.

next property

next

Devuelve el siguiente elemento entre los hijos del mismo padre, o None si no hay ninguno.

previous property

previous

Devuelve el elemento anterior entre los hijos del mismo padre, o None si no hay ninguno.

extract class-attribute instance-attribute

extract = getall

extract_first class-attribute instance-attribute

extract_first = get

markdown

markdown(css_selector=None, main_content_only=False)

Convierte el contenido de la respuesta en Markdown limpio.

Antes de convertirlo, siempre elimina scripts, estilos y contenido oculto o con inyección de instrucciones, igual que el servidor MCP. Requiere el paquete "markdownify" (pip install "scrapling[rag]").

PARÁMETRO DESCRIPCIÓN
css_selector

Selector CSS para convertir solo los elementos que coincidan. Se unen todas las coincidencias.

TIPO: Optional[str] PREDETERMINADO: None

main_content_only

Convierte solo el contenido de la etiqueta <body>.

TIPO: bool PREDETERMINADO: False

Código fuente en scrapling/engines/toolbelt/custom.py
def markdown(self, css_selector: Optional[str] = None, main_content_only: bool = False) -> str:
    """Convert the response content to clean Markdown.

    Scripts, styles, and hidden/prompt-injection content are always removed before conversion, which is
    the same cleaning the MCP server does. Requires the "markdownify" package (`pip install "scrapling[rag]"`).

    :param css_selector: CSS selector to convert only the matching elements. All matches are concatenated.
    :param main_content_only: Convert only the content inside the `<body>` tag.
    """
    from scrapling.core.shell import Convertor

    page = (cast(Selector, self.css("body").first) or self) if main_content_only else self
    page = Convertor._sanitize_for_ai(Convertor._strip_noise_tags(page))
    pages = [page] if not css_selector else page.css(css_selector)
    return "\n\n".join(Convertor._convert_to_markdown(element.html_content) for element in pages)

follow

follow(
    url,
    sid="",
    callback=None,
    priority=None,
    dont_filter=False,
    meta=None,
    referer_flow=True,
    **kwargs
)

Crea un Request para seguir una URL.

Método auxiliar para que los spiders sigan fácilmente los enlaces de las páginas.

IMPORTANTE: si dejas vacíos los siguientes argumentos, se usará el valor de la solicitud anterior. La única excepción es dont_filter.

También se heredan los argumentos de sesión de la solicitud anterior, salvo method, params, data, json, files y multipart. Por eso, el enlace se sigue con un GET simple, a menos que vuelvas a pasar esos argumentos.

PARÁMETRO DESCRIPCIÓN
url

URL que se va a seguir. Puede ser relativa; se combinará con la URL actual.

TIPO: str

sid

ID de sesión que se va a usar.

TIPO: str PREDETERMINADO: ''

callback

Método callback del spider que se va a usar.

TIPO: Callable[[Response], AsyncGenerator[Union[Dict[str, Any], Request, None], None]] | None PREDETERMINADO: None

priority

Número de prioridad. Cuanto mayor sea, antes se procesará la solicitud.

TIPO: int | None PREDETERMINADO: None

dont_filter

Desactiva el filtro para permitir esta solicitud aunque ya se haya realizado.

TIPO: bool PREDETERMINADO: False

meta

Metadatos adicionales que se incluyen en la solicitud.

TIPO: dict[str, Any] | None PREDETERMINADO: None

referer_flow

Está activado de forma predeterminada. Usa la URL de la respuesta actual como referer de la nueva solicitud.

TIPO: bool PREDETERMINADO: True

kwargs

Argumentos adicionales de Request.

TIPO: Any PREDETERMINADO: {}

DEVUELVE DESCRIPCIÓN
Any

Objeto Request listo para entregarse con yield.

Código fuente en scrapling/engines/toolbelt/custom.py
def follow(
    self,
    url: str,
    sid: str = "",
    callback: Callable[["Response"], AsyncGenerator[Union[Dict[str, Any], "Request", None], None]] | None = None,
    priority: int | None = None,
    dont_filter: bool = False,
    meta: dict[str, Any] | None = None,
    referer_flow: bool = True,
    **kwargs: Any,
) -> Any:
    """Create a Request to follow a URL.

    This is a helper method for spiders to easily follow links found in pages.

    **IMPORTANT**: The below arguments if left empty, the corresponding value from the previous request will be used. The only exception is `dont_filter`.

    The session kwargs of the previous request are inherited as well, except `method`, `params`, `data`, `json`, `files`, and `multipart`, so a followed link is a plain GET unless you pass them again.

    :param url: The URL to follow (can be relative, will be joined with current URL)
    :param sid: The session id to use
    :param callback: Spider callback method to use
    :param priority: The priority number to use, the higher the number, the higher priority to be processed first.
    :param dont_filter: If this request has been done before, disable the filter to allow it again.
    :param meta: Additional meta data to included in the request
    :param referer_flow: Enabled by default, set the current response url as referer for the new request url.
    :param kwargs: Additional Request arguments
    :return: Request object ready to be yielded
    """
    from scrapling.spiders import Request

    if not self.request or not isinstance(self.request, Request):
        raise TypeError("This response has no request set yet.")

    # Merge original session kwargs with new kwargs (new takes precedence)
    inherited = {k: v for k, v in self.request._session_kwargs.items() if k not in _NON_INHERITABLE_KWARGS}
    for key in ("headers", "extra_headers"):
        if inherited.get(key):
            inherited[key] = {k: v for k, v in inherited[key].items() if k.lower() != "content-length"}
    session_kwargs = {**inherited, **kwargs}

    if referer_flow:
        # For requests
        headers = session_kwargs.get("headers", {})
        headers["referer"] = self.url
        session_kwargs["headers"] = headers

        # For browsers
        extra_headers = session_kwargs.get("extra_headers", {})
        extra_headers["referer"] = self.url
        session_kwargs["extra_headers"] = extra_headers

        session_kwargs["google_search"] = False

    return Request(
        url=self.urljoin(url),
        sid=sid or self.request.sid,
        callback=callback or self.request.callback,
        priority=priority if priority is not None else self.request.priority,
        dont_filter=dont_filter,
        meta={**(self.meta or {}), **(meta or {})},
        **session_kwargs,
    )

__str__

__str__()
Código fuente en scrapling/engines/toolbelt/custom.py
def __str__(self) -> str:
    return f"<{self.status} {self.url}>"

__getitem__

__getitem__(key)
Código fuente en scrapling/parser.py
def __getitem__(self, key: str) -> TextHandler:
    if self._is_text_node(self._root):
        raise TypeError("Text nodes do not have attributes")
    return self.attrib[key]

__contains__

__contains__(key)
Código fuente en scrapling/parser.py
def __contains__(self, key: str) -> bool:
    if self._is_text_node(self._root):
        return False
    return key in self.attrib

__getstate__

__getstate__()
Código fuente en scrapling/parser.py
def __getstate__(self) -> Any:
    # lxml don't like it :)
    raise TypeError("Can't pickle Selector objects")

get_all_text

get_all_text(
    separator="\n",
    strip=False,
    ignore_tags=("script", "style"),
    valid_values=True,
)

Obtiene todas las cadenas de los descendientes de este elemento y las une con el separador indicado.

PARÁMETRO DESCRIPCIÓN
separator

Separador que se usa para unir las cadenas.

TIPO: str PREDETERMINADO: '\n'

strip

Si es True, elimina los espacios de los extremos de cada cadena antes de unirlas.

TIPO: bool PREDETERMINADO: False

ignore_tags

Tupla con los nombres de las etiquetas que quieres ignorar.

TIPO: Tuple PREDETERMINADO: ('script', 'style')

valid_values

Si está activado, ignora los elementos cuyo texto está vacío o solo contiene espacios en blanco.

TIPO: bool PREDETERMINADO: True

DEVUELVE DESCRIPCIÓN
TextHandler

Un TextHandler.

Código fuente en scrapling/parser.py
def get_all_text(
    self,
    separator: str = "\n",
    strip: bool = False,
    ignore_tags: Tuple = (
        "script",
        "style",
    ),
    valid_values: bool = True,
) -> TextHandler:
    """Get all child strings of this element, concatenated using the given separator.

    :param separator: Strings will be concatenated using this separator.
    :param strip: If True, strings will be stripped before being concatenated.
    :param ignore_tags: A tuple of all tag names you want to ignore
    :param valid_values: If enabled, elements with text-content that is empty or only whitespaces will be ignored

    :return: A TextHandler
    """
    if self._is_text_node(self._root):
        return TextHandler(str(self._root))

    ignored_elements: set[Any] = set()
    if ignore_tags:
        for element in self._root.iter(*ignore_tags):
            if element not in ignored_elements:
                ignored_elements.update(element.iter())

    _all_strings = []

    def append_text(text: str) -> None:
        processed_text = text.strip() if strip else text
        if not valid_values or processed_text.strip():
            _all_strings.append(processed_text)

    def is_visible_text_node(text_node: _ElementUnicodeResult) -> bool:
        parent = text_node.getparent()
        if parent is None:
            return False

        owner = parent.getparent() if text_node.is_tail else parent
        return owner not in ignored_elements

    for text_node in cast(list[_ElementUnicodeResult], _find_all_text_nodes(self._root)):
        text = str(text_node)
        if text and is_visible_text_node(text_node):
            append_text(text)

    return cast(TextHandler, TextHandler(separator).join(_all_strings))

urljoin

urljoin(relative_url)

Combina la URL de este Selector con una URL relativa para formar una URL absoluta.

Código fuente en scrapling/parser.py
def urljoin(self, relative_url: str) -> str:
    """Join this Selector's url with a relative url to form an absolute full URL."""
    return urljoin(self.url, relative_url)

prettify

prettify()

Devuelve el HTML interno del elemento con un formato más legible.

Código fuente en scrapling/parser.py
def prettify(self) -> TextHandler:
    """Return a prettified version of the element's inner html-code"""
    if self._is_text_node(self._root):
        return TextHandler(str(self._root))
    content = tostring(
        self._root,
        encoding=self.encoding,
        pretty_print=True,
        method="html",
        with_tail=False,
    )
    if isinstance(content, bytes):
        content = content.strip().decode(self.encoding)
    return TextHandler(content)

has_class

has_class(class_name)

Comprueba si el elemento tiene una clase concreta.

PARÁMETRO DESCRIPCIÓN
class_name

Nombre de la clase que se quiere comprobar.

TIPO: str

DEVUELVE DESCRIPCIÓN
bool

True si el elemento tiene una clase con ese nombre; en caso contrario, False.

Código fuente en scrapling/parser.py
def has_class(self, class_name: str) -> bool:
    """Check if the element has a specific class
    :param class_name: The class name to check for
    :return: True if element has class with that name otherwise False
    """
    if self._is_text_node(self._root):
        return False
    return class_name in self._root.classes

iterancestors

iterancestors()

Devuelve un generador que recorre todos los antecesores del elemento, empezando por su padre.

Código fuente en scrapling/parser.py
def iterancestors(self) -> Generator["Selector", None, None]:
    """Return a generator that loops over all ancestors of the element, starting with the element's parent."""
    if self._is_text_node(self._root):
        return
    for ancestor in self._root.iterancestors():
        yield self.__element_convertor(ancestor)

find_ancestor

find_ancestor(func)

Recorre los antecesores del elemento hasta encontrar uno que cumpla la función indicada.

PARÁMETRO DESCRIPCIÓN
func

Función que recibe cada antecesor y devuelve True o False.

TIPO: Callable[[Selector], bool]

DEVUELVE DESCRIPCIÓN
Optional[Selector]

Primer antecesor que cumple la función, o None si no hay ninguno.

Código fuente en scrapling/parser.py
def find_ancestor(self, func: Callable[["Selector"], bool]) -> Optional["Selector"]:
    """Loop over all ancestors of the element till one match the passed function
    :param func: A function that takes each ancestor as an argument and returns True/False
    :return: The first ancestor that match the function or ``None`` otherwise.
    """
    for ancestor in self.iterancestors():
        if func(ancestor):
            return ancestor
    return None

get

get()

Serializa este elemento como una cadena. Para nodos de texto devuelve su texto. Para elementos HTML devuelve el HTML externo, incluida la propia etiqueta.

Código fuente en scrapling/parser.py
def get(self) -> TextHandler:
    """
    Serialize this element to a string.
    For text nodes, returns the text value. For HTML elements, returns the outer HTML.
    """
    if self._is_text_node(self._root):
        return TextHandler(str(self._root))
    return self.html_content

getall

getall()

Devuelve una lista de un solo elemento con la cadena serializada de este elemento.

Código fuente en scrapling/parser.py
def getall(self) -> TextHandlers:
    """Return a single-element list containing this element's serialized string."""
    return TextHandlers([self.get()])

__repr__

__repr__()
Código fuente en scrapling/parser.py
def __repr__(self) -> str:
    length_limit = 40

    if self._is_text_node(self._root):
        text = str(self._root)
        if len(text) > length_limit:
            text = text[:length_limit].strip() + "..."
        return f"<text='{text}'>"

    content = clean_spaces(self.html_content)
    if len(content) > length_limit:
        content = content[:length_limit].strip() + "..."
    data = f"<data='{content}'"

    if self.parent:
        parent_content = clean_spaces(self.parent.html_content)
        if len(parent_content) > length_limit:
            parent_content = parent_content[:length_limit].strip() + "..."

        data += f" parent='{parent_content}'"

    return data + ">"

relocate

relocate(element, percentage=40, selector_type=False)

Busca de nuevo el elemento en el árbol de la página. Se usa de forma automática cuando cambia la estructura de la página.

PARÁMETRO DESCRIPCIÓN
element

Elemento que se quiere volver a localizar en el árbol.

TIPO: Union[Dict, HtmlElement, Selector]

percentage

Porcentaje mínimo de coincidencia que se acepta. Su cálculo depende solo de la estructura de la página. Cambia este valor únicamente si sabes cómo afecta a los resultados.

TIPO: int PREDETERMINADO: 40

selector_type

Si es True, convierte el resultado en un objeto Selectors.

TIPO: bool PREDETERMINADO: False

DEVUELVE DESCRIPCIÓN
Union[List[HtmlElement], Selectors]

Lista de elementos HTML con la mayor puntuación de coincidencia, o un objeto 'Selectors'.

Código fuente en scrapling/parser.py
def relocate(
    self,
    element: Union[Dict, HtmlElement, "Selector"],
    percentage: int = 40,
    selector_type: bool = False,
) -> Union[List[HtmlElement], "Selectors"]:
    """This function will search again for the element in the page tree, used automatically on page structure change

    :param element: The element we want to relocate in the tree
    :param percentage: The minimum percentage to accept and not going lower than that. 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!
    :param selector_type: If True, the return result will be converted to `Selectors` object
    :return: List of pure HTML elements that got the highest matching score or 'Selectors' object
    """
    score_table: Dict[float, List[Any]] = {}
    # Note: `element` will most likely always be a dictionary at this point.
    if isinstance(element, self.__class__):
        element = element._root

    if issubclass(type(element), HtmlElement):
        element = _StorageTools.element_to_dict(element)

    for node in cast(List, _find_all_elements(self._root)):
        # Collect all elements in the page, then for each element get the matching score of it against the node.
        # 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
        score = self.__calculate_similarity_score(cast(Dict, element), node)
        score_table.setdefault(score, []).append(node)

    if score_table:
        highest_probability = max(score_table.keys())
        if score_table[highest_probability] and highest_probability >= percentage:
            if log.getEffectiveLevel() < 20:
                # No need to execute this part if the logging level is not debugging
                log.debug(f"Highest probability was {highest_probability}%")
                log.debug("Top 5 best matching elements are: ")
                for percent in tuple(sorted(score_table.keys(), reverse=True))[:5]:
                    log.debug(f"{percent} -> {self.__elements_convertor(score_table[percent])}")

            if not selector_type:
                return score_table[highest_probability]
            return self.__elements_convertor(score_table[highest_probability])
        log.warning(
            f"Adaptive relocation found no element above the {percentage}% threshold "
            f"(top score: {highest_probability}%). Lower `percentage` if this is the right element."
        )
    return []

css

css(
    selector,
    identifier="",
    adaptive=False,
    auto_save=False,
    percentage=40,
)

Busca en el árbol actual con selectores CSS3.

Importante: usa el argumento identifier si piensas cambiar el selector más adelante y quieres volver a localizar los mismos elementos.

PARÁMETRO DESCRIPCIÓN
selector

Selector CSS3 que se va a usar.

TIPO: str

adaptive

Si está activado, intenta volver a localizar el elemento si ya se había guardado.

TIPO: bool PREDETERMINADO: False

identifier

Cadena que se usa para guardar y recuperar los datos del elemento en el modo adaptativo. Si no se indica, se usa el selector.

TIPO: str PREDETERMINADO: ''

auto_save

Guarda los elementos nuevos de forma automática para usarlos después con adaptive.

TIPO: bool PREDETERMINADO: False

percentage

Porcentaje mínimo de coincidencia que acepta adaptive. Su cálculo depende solo de la estructura de la página. Cambia este valor únicamente si sabes cómo afecta a los resultados.

TIPO: int PREDETERMINADO: 40

DEVUELVE DESCRIPCIÓN
Selectors

Clase Selectors.

Código fuente en scrapling/parser.py
def css(
    self,
    selector: str,
    identifier: str = "",
    adaptive: bool = False,
    auto_save: bool = False,
    percentage: int = 40,
) -> "Selectors":
    """Search the current tree with CSS3 selectors

    **Important:
    It's recommended to use the identifier argument if you plan to use a different selector later
    and want to relocate the same element(s)**

    :param selector: The CSS3 selector to be used.
    :param adaptive: Enabled will make the function try to relocate the element if it was 'saved' before
    :param identifier: A string that will be used to save/retrieve element's data in adaptive,
     otherwise the selector will be used.
    :param auto_save: Automatically save new elements for `adaptive` later
    :param percentage: The minimum percentage to accept while `adaptive` is working and not going lower than that.
     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!

    :return: `Selectors` class.
    """
    if self._is_text_node(self._root):
        return Selectors()

    try:
        if not self.__adaptive_enabled or "," not in selector:
            # No need to split selectors in this case, let's save some CPU cycles :)
            xpath_selector = _css_to_xpath(selector)
            return self.xpath(
                xpath_selector,
                identifier or selector,
                adaptive,
                auto_save,
                percentage,
            )

        results = Selectors()
        for single_selector in split_selectors(selector):
            # I'm doing this only so the `save` function saves data correctly for combined selectors
            # Like using the ',' to combine two different selectors that point to different elements.
            xpath_selector = _css_to_xpath(single_selector.canonical())
            results += self.xpath(
                xpath_selector,
                identifier or single_selector.canonical(),
                adaptive,
                auto_save,
                percentage,
            )

        return Selectors(results)
    except (
        SelectorError,
        SelectorSyntaxError,
    ) as e:
        raise SelectorSyntaxError(f"Invalid CSS selector '{selector}': {str(e)}") from e

xpath

xpath(
    selector,
    identifier="",
    adaptive=False,
    auto_save=False,
    percentage=40,
    **kwargs
)

Busca en el árbol actual con selectores XPath.

Importante: usa el argumento identifier si piensas cambiar el selector más adelante y quieres volver a localizar los mismos elementos.

Nota: los argumentos adicionales se pasan como variables a la expresión XPath.

PARÁMETRO DESCRIPCIÓN
selector

Selector XPath que se va a usar.

TIPO: str

adaptive

Si está activado, intenta volver a localizar el elemento si ya se había guardado.

TIPO: bool PREDETERMINADO: False

identifier

Cadena que se usa para guardar y recuperar los datos del elemento en el modo adaptativo. Si no se indica, se usa el selector.

TIPO: str PREDETERMINADO: ''

auto_save

Guarda los elementos nuevos de forma automática para usarlos después con adaptive.

TIPO: bool PREDETERMINADO: False

percentage

Porcentaje mínimo de coincidencia que acepta adaptive. Su cálculo depende solo de la estructura de la página. Cambia este valor únicamente si sabes cómo afecta a los resultados.

TIPO: int PREDETERMINADO: 40

DEVUELVE DESCRIPCIÓN
Selectors

Clase Selectors.

Código fuente en scrapling/parser.py
def xpath(
    self,
    selector: str,
    identifier: str = "",
    adaptive: bool = False,
    auto_save: bool = False,
    percentage: int = 40,
    **kwargs: Any,
) -> "Selectors":
    """Search the current tree with XPath selectors

    **Important:
    It's recommended to use the identifier argument if you plan to use a different selector later
    and want to relocate the same element(s)**

     Note: **Additional keyword arguments will be passed as XPath variables in the XPath expression!**

    :param selector: The XPath selector to be used.
    :param adaptive: Enabled will make the function try to relocate the element if it was 'saved' before
    :param identifier: A string that will be used to save/retrieve element's data in adaptive,
     otherwise the selector will be used.
    :param auto_save: Automatically save new elements for `adaptive` later
    :param percentage: The minimum percentage to accept while `adaptive` is working and not going lower than that.
     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!

    :return: `Selectors` class.
    """
    if self._is_text_node(self._root):
        return Selectors()

    try:
        elements = self._root.xpath(selector, **kwargs)
        # string(), count(), boolean() and the like return one value instead of a list of nodes
        if not isinstance(elements, list):
            value = ("1" if elements else "0") if isinstance(elements, bool) else str(elements)
            return self.__elements_convertor([_ElementUnicodeResult(value)])

        if elements:
            if not self.__adaptive_enabled and auto_save:
                log.warning(
                    "Argument `auto_save` will be ignored because `adaptive` wasn't enabled on initialization. Check docs for more info."
                )
            elif self.__adaptive_enabled and auto_save:
                self.save(elements[0], identifier or selector)

            return self.__handle_elements(elements)
        elif self.__adaptive_enabled:
            if adaptive:
                element_data = self.retrieve(identifier or selector)
                if element_data:
                    elements = self.relocate(element_data, percentage)
                    if elements and auto_save:
                        self.save(elements[0], identifier or selector)

            return self.__handle_elements(elements)
        else:
            if adaptive:
                log.warning(
                    "Argument `adaptive` will be ignored because `adaptive` wasn't enabled on initialization. Check docs for more info."
                )
            elif auto_save:
                log.warning(
                    "Argument `auto_save` will be ignored because `adaptive` wasn't enabled on initialization. Check docs for more info."
                )

            return self.__handle_elements(elements)

    except (
        SelectorError,
        SelectorSyntaxError,
        XPathError,
        XPathEvalError,
    ) as e:
        raise SelectorSyntaxError(f"Invalid XPath selector: {selector}") from e

find_all

find_all(*args, **kwargs)

Busca elementos con filtros que puedes definir tú mismo.

PARÁMETRO DESCRIPCIÓN
args

Uno o varios nombres de etiquetas, un iterable de nombres, patrones regex, una función o un diccionario de atributos. Déjalo vacío para seleccionar todos los elementos.

TIPO: str | Iterable[str] | Pattern | Callable | Dict[str, str] PREDETERMINADO: ()

kwargs

Atributos que se usan para filtrar los elementos.

TIPO: str PREDETERMINADO: {}

DEVUELVE DESCRIPCIÓN
Selectors

Objeto Selectors con los elementos, o una lista vacía.

Código fuente en scrapling/parser.py
def find_all(
    self,
    *args: str | Iterable[str] | Pattern | Callable | Dict[str, str],
    **kwargs: str,
) -> "Selectors":
    """Find elements by filters of your creations for ease.

    :param args: Tag name(s), iterable of tag names, regex patterns, function, 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 `Selectors` object of the elements or empty list
    """
    if self._is_text_node(self._root):
        return Selectors()

    if not args and not kwargs:
        raise TypeError("You have to pass something to search with, like tag name(s), tag attributes, or both.")

    attributes: Dict[str, Any] = dict()
    tags: Set[str] = set()
    patterns: Set[Pattern] = set()
    results, functions, selectors = Selectors(), [], []

    # Brace yourself for a wonderful journey!
    for arg in args:
        if isinstance(arg, str):
            tags.add(arg)

        elif type(arg) in (list, tuple, set):
            arg = cast(Iterable, arg)  # Type narrowing for type checkers like pyright
            if not all(map(lambda x: isinstance(x, str), arg)):
                raise TypeError("Nested Iterables are not accepted, only iterables of tag names are accepted")
            tags.update(set(arg))

        elif isinstance(arg, dict):
            if not all([(isinstance(k, str) and isinstance(v, 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)

        elif isinstance(arg, re_Pattern):
            patterns.add(arg)

        elif callable(arg):
            if len(signature(arg).parameters) > 0:
                functions.append(arg)
            else:
                raise TypeError(
                    "Callable filter function must have at least one argument to take `Selector` objects."
                )

        else:
            raise TypeError(f'Argument with type "{type(arg)}" is not accepted, please read the docs.')

    if not all([(isinstance(k, str) and isinstance(v, str)) for k, v in kwargs.items()]):
        raise TypeError("Only string values are accepted for arguments")

    for attribute_name, value in kwargs.items():
        # Only replace names for kwargs, replacing them in dictionaries doesn't make sense
        attribute_name = _whitelisted.get(attribute_name, attribute_name)
        attributes[attribute_name] = value

    # It's easier and faster to build a selector than traversing the tree
    tags = tags or set("*")
    for tag in tags:
        selector = tag
        for key, value in attributes.items():
            # Not escaping anything with the key so the user can pass patterns like {'href*': '/p/'} or get errors :)
            if key == "class" and (class_names := value.split()):
                # `class` is a space-separated list, so exact-match [class="x"] misses class="x y"; match each name with ~=
                # An empty/blank value has no names to match, so it falls through to the exact match below
                selector += "".join('[class~="{}"]'.format(_escape_css_string(name)) for name in class_names)
            else:
                selector += '[{}="{}"]'.format(key, _escape_css_string(value))
        if selector != "*":
            selectors.append(selector)

    if selectors:
        results = cast(Selectors, self.css(", ".join(selectors)))
        if results:
            # From the results, get the ones that fulfill passed regex patterns
            for pattern in patterns:
                results = results.filter(lambda e: e.text.re(pattern, check_match=True))

            # From the results, get the ones that fulfill passed functions
            for function in functions:
                results = results.filter(function)
    else:
        results = results or self.below_elements
        for pattern in patterns:
            results = results.filter(lambda e: e.text.re(pattern, check_match=True))

        # Collect an element if it fulfills the passed function otherwise
        for function in functions:
            results = results.filter(function)

    return results

find

find(*args, **kwargs)

Busca elementos con tus propios filtros y devuelve el primer resultado. Si no encuentra ninguno, devuelve None.

PARÁMETRO DESCRIPCIÓN
args

Uno o varios nombres de etiquetas, un iterable de nombres, patrones regex, una función o un diccionario de atributos. Déjalo vacío para seleccionar todos los elementos.

TIPO: str | Iterable[str] | Pattern | Callable | Dict[str, str] PREDETERMINADO: ()

kwargs

Atributos que se usan para filtrar los elementos.

TIPO: str PREDETERMINADO: {}

DEVUELVE DESCRIPCIÓN
Optional[Selector]

Objeto Selector del elemento, o None si no hubo coincidencia.

Código fuente en scrapling/parser.py
def find(
    self,
    *args: str | Iterable[str] | Pattern | Callable | Dict[str, str],
    **kwargs: str,
) -> Optional["Selector"]:
    """Find elements by filters of your creations for ease, then return the first result. Otherwise return `None`.

    :param args: Tag name(s), iterable of tag names, regex patterns, function, 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 `Selector` object of the element or `None` if the result didn't match
    """
    for element in self.find_all(*args, **kwargs):
        return element
    return None

save

save(element, identifier)

Guarda las propiedades únicas del elemento para poder recuperarlo y volver a localizarlo después.

PARÁMETRO DESCRIPCIÓN
element

Elemento que se quiere guardar. Puede ser un Selector o un HtmlElement directo.

TIPO: HtmlElement

identifier

Identificador que se usará para recuperar el elemento del almacenamiento. Consulta la documentación para más información.

TIPO: str

Código fuente en scrapling/parser.py
def save(self, element: HtmlElement, identifier: str) -> None:
    """Saves the element's unique properties to the storage for retrieval and relocation later

    :param element: The element itself that we want to save to storage, it can be a ` Selector ` or pure ` HtmlElement `
    :param identifier: This is the identifier that will be used to retrieve the element later from the storage. See
        the docs for more info.
    """
    if self.__adaptive_enabled and self._storage:
        target_element: Any = element
        if isinstance(target_element, self.__class__):
            target_element = target_element._root

        if self._is_text_node(target_element):
            target_element = target_element.getparent()

        self._storage.save(target_element, identifier)
    else:
        raise RuntimeError(
            "Can't use `adaptive` features while it's disabled globally, you have to start a new class instance."
        )

retrieve

retrieve(identifier)

Busca en el almacenamiento con el identificador y devuelve las propiedades únicas del elemento.

PARÁMETRO DESCRIPCIÓN
identifier

Identificador que se usa para recuperar el elemento del almacenamiento. Consulta la documentación para más información.

TIPO: str

DEVUELVE DESCRIPCIÓN
Optional[Dict[str, Any]]

Diccionario con las propiedades únicas.

Código fuente en scrapling/parser.py
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
        the docs for more info.
    :return: A dictionary of the unique properties
    """
    if self.__adaptive_enabled and self._storage:
        return self._storage.retrieve(identifier)

    raise RuntimeError(
        "Can't use `adaptive` features while it's disabled globally, you have to start a new class instance."
    )

json

json()

Devuelve la respuesta como JSON si se puede convertir; si no, lanza un error.

Código fuente en scrapling/parser.py
def json(self) -> Dict:
    """Return JSON response if the response is jsonable otherwise throws error"""
    if self._is_text_node(self._root):
        return TextHandler(str(self._root)).json()
    if self._raw_body and isinstance(self._raw_body, (str, bytes)):
        if isinstance(self._raw_body, str):
            return TextHandler(self._raw_body).json()
        else:
            if TYPE_CHECKING:
                assert isinstance(self._raw_body, bytes)
            return TextHandler(self._raw_body.decode()).json()
    elif self.text:
        return self.text.json()
    else:
        return self.get_all_text(strip=True).json()

re

re(
    regex,
    replace_entities=True,
    clean_match=False,
    case_sensitive=True,
)

Aplica la expresión regular al texto actual y devuelve una lista de cadenas con las coincidencias.

PARÁMETRO DESCRIPCIÓN
regex

Puede ser una expresión regular compilada o una cadena.

TIPO: str | Pattern[str]

replace_entities

Si está activado, sustituye las referencias a entidades por sus caracteres correspondientes.

TIPO: bool PREDETERMINADO: True

clean_match

Si está activado, ignora los espacios en blanco y los espacios consecutivos al buscar coincidencias.

TIPO: bool PREDETERMINADO: False

case_sensitive

Si está desactivado, compila la expresión regular para que no distinga mayúsculas y minúsculas.

TIPO: bool PREDETERMINADO: True

Código fuente en scrapling/parser.py
def re(
    self,
    regex: str | Pattern[str],
    replace_entities: bool = True,
    clean_match: bool = False,
    case_sensitive: bool = True,
) -> TextHandlers:
    """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 replace_entities: If enabled character entity references are replaced by their corresponding character
    :param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
    :param case_sensitive: if disabled, the function will set the regex to ignore the letters case while compiling it
    """
    return self.text.re(regex, replace_entities, clean_match, case_sensitive)

re_first

re_first(
    regex,
    default=None,
    replace_entities=True,
    clean_match=False,
    case_sensitive=True,
)

Aplica la expresión regular al texto y devuelve la primera coincidencia, o el valor predeterminado si no encuentra ninguna.

PARÁMETRO DESCRIPCIÓN
regex

Puede ser una expresión regular compilada o una cadena.

TIPO: str | Pattern[str]

default

Valor que se devuelve si no hay coincidencias.

PREDETERMINADO: None

replace_entities

Si está activado, sustituye las referencias a entidades por sus caracteres correspondientes.

TIPO: bool PREDETERMINADO: True

clean_match

Si está activado, ignora los espacios en blanco y los espacios consecutivos al buscar coincidencias.

TIPO: bool PREDETERMINADO: False

case_sensitive

Si está desactivado, compila la expresión regular para que no distinga mayúsculas y minúsculas.

TIPO: bool PREDETERMINADO: True

Código fuente en scrapling/parser.py
def re_first(
    self,
    regex: str | Pattern[str],
    default=None,
    replace_entities: bool = True,
    clean_match: bool = False,
    case_sensitive: bool = True,
) -> TextHandler:
    """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 default: The default value to be returned if there is no match
    :param replace_entities: if enabled character entity references are replaced by their corresponding character
    :param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
    :param case_sensitive: if disabled, the function will set the regex to ignore the letters case while compiling it
    """
    return self.text.re_first(regex, default, replace_entities, clean_match, case_sensitive)

find_similar

find_similar(
    similarity_threshold=0.2,
    ignore_attributes=("href", "src"),
    match_text=False,
)

Busca elementos a la misma profundidad del árbol y con los mismos nombres de etiqueta, de padre y otras propiedades. Devuelve los que coinciden con los atributos del elemento actual por encima del umbral indicado.

Esta función está inspirada en AutoScraper. Sirve, por ejemplo, cuando encuentras un div de producto en una lista y quieres usarlo como punto de partida para buscar los demás productos. Funciona con cualquier tipo de elemento.

PARÁMETRO DESCRIPCIÓN
similarity_threshold

Porcentaje que se usa al comparar los atributos. Antes de esa comparación, los elementos encontrados ya comparten profundidad, nombre de etiqueta y nombres de las etiquetas del padre y del abuelo. Por eso, es muy probable que sean correctos, alrededor del 99 %. La comparación de atributos añade otro filtro. Cambia este valor solo si no obtienes los resultados que necesitas. Si ni el elemento actual ni el similar tienen atributos, se considera una coincidencia del 100 %.

TIPO: float PREDETERMINADO: 0.2

ignore_attributes

Nombres de atributos que se ignoran en la comparación final. De forma predeterminada se ignoran href y src, porque las URL pueden variar mucho entre elementos y no son una referencia fiable.

TIPO: List | Tuple PREDETERMINADO: ('href', 'src')

match_text

Si es True, también compara el texto de los elementos. Normalmente no hace falta, pero puede ser útil en algunos casos.

TIPO: bool PREDETERMINADO: False

DEVUELVE DESCRIPCIÓN
Selectors

Contenedor Selectors de objetos Selector, o una lista vacía.

Código fuente en scrapling/parser.py
def find_similar(
    self,
    similarity_threshold: float = 0.2,
    ignore_attributes: List | Tuple = (
        "href",
        "src",
    ),
    match_text: bool = False,
) -> "Selectors":
    """Find elements that are in the same tree depth in the page with the same tag name and same parent tag etc...
    then return the ones that match the current element attributes with a percentage higher than the input threshold.

    This function is inspired by AutoScraper and made for cases where you, for example, found a product div inside
    a products-list container and want to find other products using that element as a starting point EXCEPT
    this function works in any case without depending on the element type.

    :param similarity_threshold: The percentage to use while comparing element attributes.
        Note: Elements found before attributes matching/comparison will be sharing the same depth, same tag name,
        same parent tag name, and same grand parent tag name. So they are 99% likely to be correct unless you are
        extremely unlucky, then attributes matching comes into play, so don't play with this number unless
        you are getting the results you don't want.
        Also, if the current element doesn't have attributes and the similar element as well, then it's a 100% match.
    :param ignore_attributes: Attribute names passed will be ignored while matching the attributes in the last step.
        The default value is to ignore `href` and `src` as URLs can change a lot between elements, so it's unreliable
    :param match_text: If True, element text content will be taken into calculation while matching.
        Not recommended to use in normal cases, but it depends.

    :return: A ``Selectors`` container of ``Selector`` objects or empty list
    """
    if self._is_text_node(self._root):
        return Selectors()

    # We will use the elements' root from now on to get the speed boost of using Lxml directly
    root = self._root
    similar_elements = list()

    current_depth = len(list(root.iterancestors()))
    target_attrs = self.__get_attributes(root, ignore_attributes) if ignore_attributes else root.attrib

    path_parts = [self.tag]
    if (parent := root.getparent()) is not None:
        path_parts.insert(0, parent.tag)
        if (grandparent := parent.getparent()) is not None:
            path_parts.insert(0, grandparent.tag)

    xpath_path = "//{}".format("/".join(path_parts))
    potential_matches = root.xpath(f"{xpath_path}[count(ancestor::*) = {current_depth}]")

    for potential_match in potential_matches:
        if potential_match != root and self.__are_alike(
            root,
            target_attrs,
            potential_match,
            ignore_attributes,
            similarity_threshold,
            match_text,
        ):
            similar_elements.append(potential_match)

    return Selectors(map(self.__element_convertor, similar_elements))

find_by_text

find_by_text(
    text,
    first_match=True,
    partial=False,
    case_sensitive=False,
    clean_match=True,
)

Busca elementos cuyo texto coincida de forma total o parcial con el texto indicado.

PARÁMETRO DESCRIPCIÓN
text

Texto que se quiere buscar.

TIPO: str

first_match

Devuelve el primer elemento que cumple las condiciones. Está activado de forma predeterminada.

TIPO: bool PREDETERMINADO: True

partial

Si está activado, devuelve elementos que contengan el texto indicado.

TIPO: bool PREDETERMINADO: False

case_sensitive

Si está activado, distingue mayúsculas y minúsculas.

TIPO: bool PREDETERMINADO: False

clean_match

Si está activado, ignora los espacios en blanco y los espacios consecutivos al buscar coincidencias.

TIPO: bool PREDETERMINADO: True

Código fuente en scrapling/parser.py
def find_by_text(
    self,
    text: str,
    first_match: bool = True,
    partial: bool = False,
    case_sensitive: bool = False,
    clean_match: bool = True,
) -> Union["Selectors", "Selector"]:
    """Find elements that its text content fully/partially matches input.
    :param text: Text query to match
    :param first_match: Returns the first element that matches conditions, enabled by default
    :param partial: If enabled, the function returns elements that contain the input text
    :param case_sensitive: if enabled, the letters case will be taken into consideration
    :param clean_match: if enabled, this will ignore all whitespaces and consecutive spaces while matching
    """
    if self._is_text_node(self._root):
        return Selectors()

    results = Selectors()
    if not case_sensitive:
        text = text.lower()

    possible_targets = cast(List, _find_all_elements_with_spaces(self._root))
    if possible_targets:
        for node in map(self.__element_convertor, possible_targets):
            """Check if element matches given text otherwise, traverse the children tree and iterate"""
            node_text: TextHandler = node.text
            if clean_match:
                node_text = TextHandler(node_text.clean())

            if not case_sensitive:
                node_text = TextHandler(node_text.lower())

            if partial:
                if text in node_text:
                    results.append(node)
            elif text == node_text:
                results.append(node)

            if first_match and results:
                # we got an element so we should stop
                break

        if first_match:
            if results:
                return results[0]
    return results

find_by_regex

find_by_regex(
    query,
    first_match=True,
    case_sensitive=False,
    clean_match=True,
)

Busca elementos cuyo texto coincida con el patrón de expresión regular indicado.

PARÁMETRO DESCRIPCIÓN
query

Expresión regular o patrón que se quiere buscar.

TIPO: str | Pattern[str]

first_match

Devuelve el primer elemento que cumple las condiciones. Está activado de forma predeterminada.

TIPO: bool PREDETERMINADO: True

case_sensitive

Si está activado, la expresión regular distingue mayúsculas y minúsculas.

TIPO: bool PREDETERMINADO: False

clean_match

Si está activado, ignora los espacios en blanco y los espacios consecutivos al buscar coincidencias.

TIPO: bool PREDETERMINADO: True

Código fuente en scrapling/parser.py
def find_by_regex(
    self,
    query: str | Pattern[str],
    first_match: bool = True,
    case_sensitive: bool = False,
    clean_match: bool = True,
) -> Union["Selectors", "Selector"]:
    """Find elements that its text content matches the input regex pattern.
    :param query: Regex query/pattern to match
    :param first_match: Return the first element that matches conditions; enabled by default.
    :param case_sensitive: If enabled, the 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.
    """
    if self._is_text_node(self._root):
        return Selectors()

    results = Selectors()

    possible_targets = cast(List, _find_all_elements_with_spaces(self._root))
    if possible_targets:
        for node in map(self.__element_convertor, possible_targets):
            """Check if element matches given regex otherwise, traverse the children tree and iterate"""
            node_text = node.text
            if node_text.re(
                query,
                check_match=True,
                clean_match=clean_match,
                case_sensitive=case_sensitive,
            ):
                results.append(node)

            if first_match and results:
                # we got an element so we should stop
                break

        if results and first_match:
            return results[0]
    return results