Files
searxng/searx/engines/bing.py
T

224 lines
7.4 KiB
Python
Raw Normal View History

2021-01-13 11:31:25 +01:00
# SPDX-License-Identifier: AGPL-3.0-or-later
2026-03-18 14:55:25 +01:00
"""This is the implementation of the Bing-Web engine. Some of this
implementations are shared by other engines:
- :ref:`bing images engine`
- :ref:`bing news engine`
- :ref:`bing videos engine`
2026-03-18 14:55:25 +01:00
.. note::
2026-03-18 14:55:25 +01:00
Some functionality (paging and time-range results) are not supported
since they depend on JavaScript.
"""
import base64
2019-08-05 16:15:40 +02:00
import re
2026-03-18 14:55:25 +01:00
import typing as t
from urllib.parse import parse_qs, urlencode, urlparse
2026-03-01 11:33:06 +01:00
import babel
import babel.languages
2026-03-01 11:33:06 +01:00
from lxml import html
2013-10-24 23:52:57 +02:00
from searx.enginelib.traits import EngineTraits
2026-03-18 14:55:25 +01:00
from searx.locales import region_tag
2026-03-01 11:33:06 +01:00
from searx.utils import eval_xpath, eval_xpath_getindex, eval_xpath_list, extract_text
2026-03-18 14:55:25 +01:00
if t.TYPE_CHECKING:
from searx.extended_types import SXNG_Response
from searx.search.processors import OnlineParams
about: dict[str, t.Any] = {
2026-03-01 11:33:06 +01:00
"website": "https://www.bing.com",
"wikidata_id": "Q182496",
2026-03-18 14:55:25 +01:00
"official_api_documentation": "https://github.com/MicrosoftDocs/bing-docs",
2021-01-13 11:31:25 +01:00
"use_official_api": False,
"require_api_key": False,
2026-03-01 11:33:06 +01:00
"results": "HTML",
2021-01-13 11:31:25 +01:00
}
2014-09-01 14:38:59 +02:00
# engine dependent config
2026-03-01 11:33:06 +01:00
categories = ["general", "web"]
2023-09-27 18:24:33 +02:00
safesearch = True
2026-03-18 14:55:25 +01:00
_safesearch_map: dict[int, str] = {
0: "off",
1: "moderate",
2: "strict",
}
"""Filter results. 0: None, 1: Moderate, 2: Strict"""
2026-03-01 11:33:06 +01:00
base_url = "https://www.bing.com/search"
2026-03-18 14:55:25 +01:00
"""Bing-Web search URL"""
def get_locale_params(engine_region: str | None) -> dict[str, str] | None:
"""API documentation states the ``mkt`` parameter is *the
recommended primary signal* for locale:
If known, you are encouraged to always specify the market.
Specifying the market helps Bing route the request and return an
appropriate and optimal response.
The ``mkt`` parameter takes a full ``<language>-<country>`` code.
2026-03-18 14:55:25 +01:00
This function is shared with :py:mod:`searx.engines.bing_images`,
:py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
"""
2026-03-18 14:55:25 +01:00
if not engine_region or engine_region == "clear":
return None
2026-03-18 14:55:25 +01:00
return {"mkt": engine_region}
2014-09-01 14:38:59 +02:00
2026-03-18 14:55:25 +01:00
def override_accept_language(params: "OnlineParams", engine_region: str | None) -> None:
"""Override the ``Accept-Language`` header.
2026-03-18 14:55:25 +01:00
The default header built by :py:class:`~searx.search.processors.online.OnlineProcessor`
appends ``en;q=0.3`` as a fallback language::
Accept-Language: de,de-DE;q=0.7,en;q=0.3
Bing seems to better select the results locale based on the
``Accept-Language`` value header.
This function is shared with :py:mod:`searx.engines.bing_images`,
:py:mod:`searx.engines.bing_news`, and :py:mod:`searx.engines.bing_videos`.
"""
if not engine_region or engine_region == "clear":
return
lang = engine_region.split("-")[0]
params["headers"]["Accept-Language"] = f"{engine_region},{lang};q=0.9"
def request(query: str, params: "OnlineParams") -> "OnlineParams":
"""Assemble a Bing-Web request."""
2026-03-18 14:55:25 +01:00
engine_region = traits.get_region(params["searxng_locale"], traits.all_locale)
override_accept_language(params, engine_region)
2026-03-18 14:55:25 +01:00
query_params: dict[str, str | int] = {
2026-03-01 11:33:06 +01:00
"q": query,
2026-03-18 14:55:25 +01:00
"adlt": _safesearch_map.get(params.get("safesearch", 0), "off"),
2023-09-27 18:24:33 +02:00
}
2026-03-18 14:55:25 +01:00
locale_params = get_locale_params(engine_region)
if locale_params:
query_params.update(locale_params)
2023-09-27 18:24:33 +02:00
2026-03-01 11:33:06 +01:00
params["url"] = f"{base_url}?{urlencode(query_params)}"
2025-10-17 15:43:49 +02:00
# in some regions where geoblocking is employed (e.g. China),
# www.bing.com redirects to the regional version of Bing
2026-03-01 11:33:06 +01:00
params["allow_redirects"] = True
2025-10-17 15:43:49 +02:00
return params
2013-10-24 23:52:57 +02:00
2026-03-18 14:55:25 +01:00
def response(resp: "SXNG_Response") -> list[dict[str, t.Any]]:
"""Get response from Bing-Web"""
2026-03-18 14:55:25 +01:00
results: list[dict[str, t.Any]] = []
2014-09-01 14:38:59 +02:00
dom = html.fromstring(resp.text)
2021-12-18 11:40:12 +01:00
2026-03-18 14:55:25 +01:00
for item in eval_xpath_list(dom, '//ol[@id="b_results"]/li[contains(@class, "b_algo")]'):
link = eval_xpath_getindex(item, ".//h2/a", 0, None)
if link is None:
continue
2026-03-18 14:55:25 +01:00
href = link.attrib.get("href", "")
2015-01-25 20:14:37 +01:00
title = extract_text(link)
2026-03-18 14:55:25 +01:00
if not href or not title:
continue
# what about cn.bing.com, ..?
if href.startswith("https://www.bing.com/ck/a?"):
qs = parse_qs(urlparse(href).query)
u_values = qs.get("u")
if u_values:
u_val = u_values[0]
if u_val.startswith("a1"):
encoded = u_val[2:]
# base64url without padding
encoded += "=" * (-len(encoded) % 4)
href = base64.urlsafe_b64decode(encoded).decode("utf-8", errors="replace")
# remove decorative icons that Bing injects into <p> elements
# (`<span class="algoSlug_icon">`)
content_els = eval_xpath(item, ".//p")
for p in content_els:
for icon in p.xpath('.//span[@class="algoSlug_icon"]'):
icon.getparent().remove(icon)
content = extract_text(content_els)
results.append({"url": href, "title": title, "content": content})
if results:
2020-01-02 22:28:47 +01:00
result_len_container = "".join(eval_xpath(dom, '//span[@class="sb_count"]//text()'))
2026-03-18 14:55:25 +01:00
result_len_container = re.sub(r"[^0-9]", "", result_len_container)
if result_len_container:
results.append({"number_of_results": int(result_len_container)})
2013-10-24 23:52:57 +02:00
return results
2026-03-18 14:55:25 +01:00
def fetch_traits(engine_traits: EngineTraits) -> None:
"""Fetch regions from Bing-Web."""
2023-09-27 18:24:33 +02:00
# pylint: disable=import-outside-toplevel
from searx.network import get # see https://github.com/searxng/searxng/issues/762
from searx.utils import gen_useragent
headers = {
"User-Agent": gen_useragent(),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US;q=0.5,en;q=0.3",
"DNT": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-GPC": "1",
"Cache-Control": "max-age=0",
}
2026-03-01 11:33:06 +01:00
resp = get("https://www.bing.com/account/general", headers=headers, timeout=5)
if not resp.ok:
raise RuntimeError("Response from Bing is not OK.")
2026-03-01 11:33:06 +01:00
dom = html.fromstring(resp.text)
2026-03-18 14:55:25 +01:00
map_market_codes: dict[str, str] = {
"zh-hk": "en-hk", # not sure why, but at Microslop this is the market code for Hongkong
2023-09-27 18:24:33 +02:00
}
for href in eval_xpath(dom, '//div[@id="region-section-content"]//div[@class="regionItem"]/a/@href'):
2026-03-01 11:33:06 +01:00
cc_tag = parse_qs(urlparse(href).query)["cc"][0]
if cc_tag == "clear":
2023-09-27 18:24:33 +02:00
engine_traits.all_locale = cc_tag
continue
2023-09-27 18:24:33 +02:00
# add market codes from official languages of the country ..
for lang_tag in babel.languages.get_official_languages(cc_tag, de_facto=True):
2026-03-01 11:33:06 +01:00
lang_tag = lang_tag.split("_")[0] # zh_Hant --> zh
2023-09-27 18:24:33 +02:00
market_code = f"{lang_tag}-{cc_tag}" # zh-tw
market_code = map_market_codes.get(market_code, market_code)
2026-03-18 14:55:25 +01:00
try:
sxng_tag = region_tag(babel.Locale.parse("%s_%s" % (lang_tag, cc_tag.upper())))
except babel.UnknownLocaleError:
# silently ignore unknown languages
continue
2023-09-27 18:24:33 +02:00
conflict = engine_traits.regions.get(sxng_tag)
if conflict:
if conflict != market_code:
print("CONFLICT: babel %s --> %s, %s" % (sxng_tag, conflict, market_code))
2026-03-18 14:55:25 +01:00
continue
2023-09-27 18:24:33 +02:00
engine_traits.regions[sxng_tag] = market_code