Source code for searx.engines.yahoo

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Yahoo Search (Web)

As a bot detection measure, Yahoo has put search behind a cookie
named `YBV`, which is cached for 24h before expiring.

"""

import typing as t
from urllib.parse import unquote, urlencode, urljoin

from searx.enginelib import EngineCache
from searx.network import get  # see https://github.com/searxng/searxng/issues/762
from searx.result_types import EngineResults
from searx.utils import eval_xpath_getindex, eval_xpath_list, extract_text, html_to_text

if t.TYPE_CHECKING:
    from searx.extended_types import SXNG_Response
    from searx.search.processors import OnlineParams

about = {
    "website": "https://search.yahoo.com/",
    "wikidata_id": None,
    "official_api_documentation": "https://developer.yahoo.com/api/",
    "use_official_api": False,
    "require_api_key": False,
    "results": "HTML",
}

categories = ["general", "web"]
paging = True
time_range_support = True
safesearch = True

time_range_dict = {"day": "d", "week": "w", "month": "m"}
safesearch_dict = {0: "p", 1: "i", 2: "r"}

region2domain = {
    "CO": "co.search.yahoo.com",  # Colombia
    "TH": "th.search.yahoo.com",  # Thailand
    "VE": "ve.search.yahoo.com",  # Venezuela
    "CL": "cl.search.yahoo.com",  # Chile
    "PE": "pe.search.yahoo.com",  # Peru
    "CA": "ca.search.yahoo.com",  # Canada
    "DE": "de.search.yahoo.com",  # Germany
    "FR": "fr.search.yahoo.com",  # France
    "GB": "uk.search.yahoo.com",  # United Kingdom
    "UK": "uk.search.yahoo.com",
    "BR": "br.search.yahoo.com",  # Brazil
    "IN": "in.search.yahoo.com",  # India
    "ES": "espanol.search.yahoo.com",  # Espanol
    "PH": "ph.search.yahoo.com",  # Philippines
    "AR": "ar.search.yahoo.com",  # Argentina
    "MX": "mx.search.yahoo.com",  # Mexico
    "SG": "sg.search.yahoo.com",  # Singapore
    "AU": "au.search.yahoo.com",  # Australia
    "NZ": "nz.search.yahoo.com",  # New Zealand
    "IE": "ie.search.yahoo.com",  # Ireland
    "ZA": "za.search.yahoo.com",  # South Africa
    "US": "search.yahoo.com",
    "IT": "it.search.yahoo.com",  # Italy
    "NL": "nl.search.yahoo.com",  # Netherlands
    "BE": "be.search.yahoo.com",  # Belgium
    "CH": "ch.search.yahoo.com",  # Switzerland
    "AT": "at.search.yahoo.com",  # Austria
    "SE": "se.search.yahoo.com",  # Sweden
    "NO": "no.search.yahoo.com",  # Norway
    "DK": "dk.search.yahoo.com",  # Denmark
    "FI": "fi.search.yahoo.com",  # Finland
    "GR": "gr.search.yahoo.com",  # Greece
    "TR": "tr.search.yahoo.com",  # Turkey
    "MY": "malaysia.search.yahoo.com",  # Malaysia
    "ID": "id.search.yahoo.com",  # Indonesia
    "VN": "vn.search.yahoo.com",  # Vietnam
    "PL": "pl.search.yahoo.com",  # Poland
    "RO": "ro.search.yahoo.com",  # Romania
}

yahoo_languages = {
    "all": "any",
    "ar": "ar",  # Arabic
    "bg": "bg",  # Bulgarian
    "cs": "cs",  # Czech
    "da": "da",  # Danish
    "de": "de",  # German
    "el": "el",  # Greek
    "en": "en",  # English
    "es": "es",  # Spanish
    "et": "et",  # Estonian
    "fi": "fi",  # Finnish
    "fr": "fr",  # French
    "he": "he",  # Hebrew
    "hr": "hr",  # Croatian
    "hu": "hu",  # Hungarian
    "it": "it",  # Italian
    "ja": "ja",  # Japanese
    "ko": "ko",  # Korean
    "lt": "lt",  # Lithuanian
    "lv": "lv",  # Latvian
    "nl": "nl",  # Dutch
    "no": "no",  # Norwegian
    "pl": "pl",  # Polish
    "pt": "pt",  # Portuguese
    "ro": "ro",  # Romanian
    "ru": "ru",  # Russian
    "sk": "sk",  # Slovak
    "sl": "sl",  # Slovenian
    "sv": "sv",  # Swedish
    "th": "th",  # Thai
    "tr": "tr",  # Turkish
    "zh": "zh_chs",
}


CACHE: EngineCache
"""YBV cookie"""

_YBV_HOPS = 4
"""Tracking pixel -> gif -> (optional) geo redirect i.e. au.search.yahoo.com -> 200"""


def setup(engine_settings: dict[str, t.Any]):
    global CACHE  # pylint: disable=global-statement
    CACHE = EngineCache(engine_settings["name"])


def request(query: str, params: "OnlineParams") -> None:
    parts = params["searxng_locale"].split("-")
    lang = yahoo_languages.get(parts[0], "any")

    url_params: dict[str, str | int] = {"p": query}
    if params["time_range"] in time_range_dict:
        url_params["btf"] = time_range_dict[params["time_range"]]
    if params["pageno"] == 1:
        url_params["iscqry"] = ""
    else:
        url_params["b"] = params["pageno"] * 7 + 1
        url_params["pz"] = 7
        url_params["bct"] = 0
        url_params["xargs"] = 0

    params["cookies"]["sB"] = urlencode(
        {
            "v": 1,
            "vm": safesearch_dict[params["safesearch"]],
            "fl": 1,
            "vl": f"lang_{lang}",
            "pn": 10,
            "rw": "new",
            "userset": 1,
        }
    )

    domain = "search.yahoo.com"
    if len(parts) > 1 and parts[-1] in region2domain:
        domain = region2domain[parts[-1]]
    logger.debug("domain selected: %s", domain)
    params["url"] = f"https://{domain}/search?{urlencode(url_params)}"
    if ybv := CACHE.get("YBV"):
        params["cookies"]["YBV"] = ybv


[docs] def parse_url(url_string: str) -> str: """remove yahoo-specific tracking-url""" endings = ["/RS", "/RK"] endpositions = [] start = url_string.find("http", url_string.find("/RU=") + 1) for ending in endings: endpos = url_string.rfind(ending) if endpos > -1: endpositions.append(endpos) if start == 0 or len(endpositions) == 0: return url_string end = min(endpositions) return unquote(url_string[start:end])
def _yahoo_html(resp: "SXNG_Response") -> "SXNG_Response": cookies = dict(resp.search_params["cookies"]) params = resp.search_params for _ in range(_YBV_HOPS): if ybv := resp.cookies.get("YBV"): cookies["YBV"] = ybv if ybv.startswith("v0.2"): CACHE.set("YBV", ybv, expire=86400) if resp.status_code == 200: return resp loc = resp.headers.get("location") if resp.status_code not in (302, 307) or not loc: return resp # request ourselves instead of following it resp = get( urljoin(resp.url, loc), cookies=cookies, headers=params["headers"], allow_redirects=False, ) resp.search_params = params return resp def response(resp: "SXNG_Response") -> EngineResults: resp = _yahoo_html(resp) results = EngineResults() if resp.status_code != 200: resp.raise_for_status() dom = resp.html() for result in eval_xpath_list(dom, '//div[contains(@class,"algo-sr")]'): url = eval_xpath_getindex(result, './/div[contains(@class,"compTitle")]//a/@href', 0, default=None) if url is None: continue url = parse_url(url) title = extract_text(eval_xpath_getindex(result, ".//h3//a/@aria-label", 0, default=None), allow_none=True) if not title: title = extract_text(eval_xpath_getindex(result, ".//a/h3", 0, default=None), allow_none=True) if not title: continue content = extract_text(eval_xpath_getindex(result, './/div[contains(@class, "compText")]', 0)) results.add( results.types.MainResult( url=url, # title sometimes contains HTML tags / see # https://github.com/searxng/searxng/issues/3790 title=html_to_text(title), content=html_to_text(content), ) ) for suggestion in eval_xpath_list(dom, '//div[contains(@class, "AlsoTry")]//table//a'): results.add(results.types.LegacyResult(suggestion=extract_text(suggestion))) return results