.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
google.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""This is the implementation of the Google WEB engine. Some of this
3implementations (manly the :py:obj:`get_google_info`) are shared by other
4engines:
5
6- :ref:`google images engine`
7- :ref:`google news engine`
8- :ref:`google videos engine`
9- :ref:`google scholar engine`
10- :ref:`google autocomplete`
11
12"""
13
14from typing import TYPE_CHECKING
15
16import re
17from urllib.parse import urlencode
18from lxml import html
19import babel
20import babel.core
21import babel.languages
22
23from searx.utils import extract_text, eval_xpath, eval_xpath_list, eval_xpath_getindex
24from searx.locales import language_tag, region_tag, get_official_locales
25from searx.network import get # see https://github.com/searxng/searxng/issues/762
26from searx.exceptions import SearxEngineCaptchaException
27from searx.enginelib.traits import EngineTraits
28from searx.result_types import EngineResults
29
30if TYPE_CHECKING:
31 import logging
32
33 logger: logging.Logger
34
35traits: EngineTraits
36
37
38# about
39about = {
40 "website": 'https://www.google.com',
41 "wikidata_id": 'Q9366',
42 "official_api_documentation": 'https://developers.google.com/custom-search/',
43 "use_official_api": False,
44 "require_api_key": False,
45 "results": 'HTML',
46}
47
48# engine dependent config
49categories = ['general', 'web']
50paging = True
51max_page = 50
52time_range_support = True
53safesearch = True
54
55time_range_dict = {'day': 'd', 'week': 'w', 'month': 'm', 'year': 'y'}
56
57# Filter results. 0: None, 1: Moderate, 2: Strict
58filter_mapping = {0: 'off', 1: 'medium', 2: 'high'}
59
60# specific xpath variables
61# ------------------------
62
63# Suggestions are links placed in a *card-section*, we extract only the text
64# from the links not the links itself.
65suggestion_xpath = '//div[contains(@class, "EIaa9b")]//a'
66
67# UI_ASYNC = 'use_ac:true,_fmt:html' # returns a HTTP 500 when user search for
68# # celebrities like '!google natasha allegri'
69# # or '!google chris evans'
70UI_ASYNC = 'use_ac:true,_fmt:prog'
71"""Format of the response from UI's async request."""
72
73
74def get_google_info(params, eng_traits):
75 """Composing various (language) properties for the google engines (:ref:`google
76 API`).
77
78 This function is called by the various google engines (:ref:`google web
79 engine`, :ref:`google images engine`, :ref:`google news engine` and
80 :ref:`google videos engine`).
81
82 :param dict param: Request parameters of the engine. At least
83 a ``searxng_locale`` key should be in the dictionary.
84
85 :param eng_traits: Engine's traits fetched from google preferences
86 (:py:obj:`searx.enginelib.traits.EngineTraits`)
87
88 :rtype: dict
89 :returns:
90 Py-Dictionary with the key/value pairs:
91
92 language:
93 The language code that is used by google (e.g. ``lang_en`` or
94 ``lang_zh-TW``)
95
96 country:
97 The country code that is used by google (e.g. ``US`` or ``TW``)
98
99 locale:
100 A instance of :py:obj:`babel.core.Locale` build from the
101 ``searxng_locale`` value.
102
103 subdomain:
104 Google subdomain :py:obj:`google_domains` that fits to the country
105 code.
106
107 params:
108 Py-Dictionary with additional request arguments (can be passed to
109 :py:func:`urllib.parse.urlencode`).
110
111 - ``hl`` parameter: specifies the interface language of user interface.
112 - ``lr`` parameter: restricts search results to documents written in
113 a particular language.
114 - ``cr`` parameter: restricts search results to documents
115 originating in a particular country.
116 - ``ie`` parameter: sets the character encoding scheme that should
117 be used to interpret the query string ('utf8').
118 - ``oe`` parameter: sets the character encoding scheme that should
119 be used to decode the XML result ('utf8').
120
121 headers:
122 Py-Dictionary with additional HTTP headers (can be passed to
123 request's headers)
124
125 - ``Accept: '*/*``
126
127 """
128
129 ret_val = {
130 'language': None,
131 'country': None,
132 'subdomain': None,
133 'params': {},
134 'headers': {},
135 'cookies': {},
136 'locale': None,
137 }
138
139 sxng_locale = params.get('searxng_locale', 'all')
140 try:
141 locale = babel.Locale.parse(sxng_locale, sep='-')
142 except babel.core.UnknownLocaleError:
143 locale = None
144
145 eng_lang = eng_traits.get_language(sxng_locale, 'lang_en')
146 lang_code = eng_lang.split('_')[-1] # lang_zh-TW --> zh-TW / lang_en --> en
147 country = eng_traits.get_region(sxng_locale, eng_traits.all_locale)
148
149 # Test zh_hans & zh_hant --> in the topmost links in the result list of list
150 # TW and HK you should a find wiktionary.org zh_hant link. In the result
151 # list of zh-CN should not be no hant link instead you should find
152 # zh.m.wikipedia.org/zh somewhere in the top.
153
154 # '!go 日 :zh-TW' --> https://zh.m.wiktionary.org/zh-hant/%E6%97%A5
155 # '!go 日 :zh-CN' --> https://zh.m.wikipedia.org/zh/%E6%97%A5
156
157 ret_val['language'] = eng_lang
158 ret_val['country'] = country
159 ret_val['locale'] = locale
160 ret_val['subdomain'] = eng_traits.custom['supported_domains'].get(country.upper(), 'www.google.com')
161
162 # hl parameter:
163 # The hl parameter specifies the interface language (host language) of
164 # your user interface. To improve the performance and the quality of your
165 # search results, you are strongly encouraged to set this parameter
166 # explicitly.
167 # https://developers.google.com/custom-search/docs/xml_results#hlsp
168 # The Interface Language:
169 # https://developers.google.com/custom-search/docs/xml_results_appendices#interfaceLanguages
170
171 # https://github.com/searxng/searxng/issues/2515#issuecomment-1607150817
172 ret_val['params']['hl'] = f'{lang_code}-{country}'
173
174 # lr parameter:
175 # The lr (language restrict) parameter restricts search results to
176 # documents written in a particular language.
177 # https://developers.google.com/custom-search/docs/xml_results#lrsp
178 # Language Collection Values:
179 # https://developers.google.com/custom-search/docs/xml_results_appendices#languageCollections
180 #
181 # To select 'all' languages an empty 'lr' value is used.
182 #
183 # Different to other google services, Google Scholar supports to select more
184 # than one language. The languages are separated by a pipe '|' (logical OR).
185 # By example: &lr=lang_zh-TW%7Clang_de selects articles written in
186 # traditional chinese OR german language.
187
188 ret_val['params']['lr'] = eng_lang
189 if sxng_locale == 'all':
190 ret_val['params']['lr'] = ''
191
192 # cr parameter:
193 # The cr parameter restricts search results to documents originating in a
194 # particular country.
195 # https://developers.google.com/custom-search/docs/xml_results#crsp
196
197 # specify a region (country) only if a region is given in the selected
198 # locale --> https://github.com/searxng/searxng/issues/2672
199 ret_val['params']['cr'] = ''
200 if len(sxng_locale.split('-')) > 1:
201 ret_val['params']['cr'] = 'country' + country
202
203 # gl parameter: (mandatory by Google News)
204 # The gl parameter value is a two-letter country code. For WebSearch
205 # results, the gl parameter boosts search results whose country of origin
206 # matches the parameter value. See the Country Codes section for a list of
207 # valid values.
208 # Specifying a gl parameter value in WebSearch requests should improve the
209 # relevance of results. This is particularly true for international
210 # customers and, even more specifically, for customers in English-speaking
211 # countries other than the United States.
212 # https://developers.google.com/custom-search/docs/xml_results#glsp
213
214 # https://github.com/searxng/searxng/issues/2515#issuecomment-1606294635
215 # ret_val['params']['gl'] = country
216
217 # ie parameter:
218 # The ie parameter sets the character encoding scheme that should be used
219 # to interpret the query string. The default ie value is latin1.
220 # https://developers.google.com/custom-search/docs/xml_results#iesp
221
222 ret_val['params']['ie'] = 'utf8'
223
224 # oe parameter:
225 # The oe parameter sets the character encoding scheme that should be used
226 # to decode the XML result. The default oe value is latin1.
227 # https://developers.google.com/custom-search/docs/xml_results#oesp
228
229 ret_val['params']['oe'] = 'utf8'
230
231 # num parameter:
232 # The num parameter identifies the number of search results to return.
233 # The default num value is 10, and the maximum value is 20. If you request
234 # more than 20 results, only 20 results will be returned.
235 # https://developers.google.com/custom-search/docs/xml_results#numsp
236
237 # HINT: seems to have no effect (tested in google WEB & Images)
238 # ret_val['params']['num'] = 20
239
240 # HTTP headers
241
242 ret_val['headers']['Accept'] = '*/*'
243
244 # Cookies
245
246 # - https://github.com/searxng/searxng/pull/1679#issuecomment-1235432746
247 # - https://github.com/searxng/searxng/issues/1555
248 ret_val['cookies']['CONSENT'] = "YES+"
249
250 return ret_val
251
252
254 if resp.url.host == 'sorry.google.com' or resp.url.path.startswith('/sorry'):
256
257
258def request(query, params):
259 """Google search request"""
260 # pylint: disable=line-too-long
261 offset = (params['pageno'] - 1) * 10
262 google_info = get_google_info(params, traits)
263
264 # https://www.google.de/search?q=corona&hl=de&lr=lang_de&start=0&tbs=qdr%3Ad&safe=medium
265 query_url = (
266 'https://'
267 + google_info['subdomain']
268 + '/search'
269 + "?"
270 + urlencode(
271 {
272 'q': query,
273 **google_info['params'],
274 'filter': '0',
275 'start': offset,
276 # 'vet': '12ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0QxK8CegQIARAC..i',
277 # 'ved': '2ahUKEwik3ZbIzfn7AhXMX_EDHbUDBh0Q_skCegQIARAG',
278 # 'cs' : 1,
279 # 'sa': 'N',
280 # 'yv': 3,
281 # 'prmd': 'vin',
282 # 'ei': 'GASaY6TxOcy_xc8PtYeY6AE',
283 # 'sa': 'N',
284 # 'sstk': 'AcOHfVkD7sWCSAheZi-0tx_09XDO55gTWY0JNq3_V26cNN-c8lfD45aZYPI8s_Bqp8s57AHz5pxchDtAGCA_cikAWSjy9kw3kgg'
285 # formally known as use_mobile_ui
286 'asearch': 'arc',
287 'async': UI_ASYNC,
288 }
289 )
290 )
291
292 if params['time_range'] in time_range_dict:
293 query_url += '&' + urlencode({'tbs': 'qdr:' + time_range_dict[params['time_range']]})
294 if params['safesearch']:
295 query_url += '&' + urlencode({'safe': filter_mapping[params['safesearch']]})
296 params['url'] = query_url
297
298 params['cookies'] = google_info['cookies']
299 params['headers'].update(google_info['headers'])
300 return params
301
302
303# =26;[3,"dimg_ZNMiZPCqE4apxc8P3a2tuAQ_137"]a87;data:image/jpeg;base64,/9j/4AAQSkZJRgABA
304# ...6T+9Nl4cnD+gr9OK8I56/tX3l86nWYw//2Q==26;
305RE_DATA_IMAGE = re.compile(r'"(dimg_[^"]*)"[^;]*;(data:image[^;]*;[^;]*);')
306
307
309 data_image_map = {}
310 for img_id, data_image in RE_DATA_IMAGE.findall(dom.text_content()):
311 end_pos = data_image.rfind('=')
312 if end_pos > 0:
313 data_image = data_image[: end_pos + 1]
314 data_image_map[img_id] = data_image
315 logger.debug('data:image objects --> %s', list(data_image_map.keys()))
316 return data_image_map
317
318
319def response(resp) -> EngineResults:
320 """Get response from google's search request"""
321 # pylint: disable=too-many-branches, too-many-statements
323
324 results = EngineResults()
325
326 # convert the text to dom
327 dom = html.fromstring(resp.text)
328 data_image_map = _parse_data_images(dom)
329
330 # results --> answer
331 answer_list = eval_xpath(dom, '//div[contains(@class, "LGOjhe")]')
332 for item in answer_list:
333 for bubble in eval_xpath(item, './/div[@class="nnFGuf"]'):
334 bubble.drop_tree()
335 results.add(
336 results.types.Answer(
337 answer=extract_text(item),
338 url=(eval_xpath(item, '../..//a/@href') + [None])[0],
339 )
340 )
341
342 # parse results
343
344 for result in eval_xpath_list(dom, './/div[contains(@jscontroller, "SC7lYd")]'):
345 # pylint: disable=too-many-nested-blocks
346
347 try:
348 title_tag = eval_xpath_getindex(result, './/a/h3[1]', 0, default=None)
349 if title_tag is None:
350 # this not one of the common google results *section*
351 logger.debug('ignoring item from the result_xpath list: missing title')
352 continue
353 title = extract_text(title_tag)
354
355 url = eval_xpath_getindex(result, './/a[h3]/@href', 0, None)
356 if url is None:
357 logger.debug('ignoring item from the result_xpath list: missing url of title "%s"', title)
358 continue
359
360 content_nodes = eval_xpath(result, './/div[contains(@data-sncf, "1")]')
361 for item in content_nodes:
362 for script in item.xpath(".//script"):
363 script.getparent().remove(script)
364
365 content = extract_text(content_nodes)
366
367 if not content:
368 logger.debug('ignoring item from the result_xpath list: missing content of title "%s"', title)
369 continue
370
371 thumbnail = content_nodes[0].xpath('.//img/@src')
372 if thumbnail:
373 thumbnail = thumbnail[0]
374 if thumbnail.startswith('data:image'):
375 img_id = content_nodes[0].xpath('.//img/@id')
376 if img_id:
377 thumbnail = data_image_map.get(img_id[0])
378 else:
379 thumbnail = None
380
381 results.append({'url': url, 'title': title, 'content': content, 'thumbnail': thumbnail})
382
383 except Exception as e: # pylint: disable=broad-except
384 logger.error(e, exc_info=True)
385 continue
386
387 # parse suggestion
388 for suggestion in eval_xpath_list(dom, suggestion_xpath):
389 # append suggestion
390 results.append({'suggestion': extract_text(suggestion)})
391
392 # return results
393 return results
394
395
396# get supported languages from their site
397
398
399skip_countries = [
400 # official language of google-country not in google-languages
401 'AL', # Albanien (sq)
402 'AZ', # Aserbaidschan (az)
403 'BD', # Bangladesch (bn)
404 'BN', # Brunei Darussalam (ms)
405 'BT', # Bhutan (dz)
406 'ET', # Äthiopien (am)
407 'GE', # Georgien (ka, os)
408 'GL', # Grönland (kl)
409 'KH', # Kambodscha (km)
410 'LA', # Laos (lo)
411 'LK', # Sri Lanka (si, ta)
412 'ME', # Montenegro (sr)
413 'MK', # Nordmazedonien (mk, sq)
414 'MM', # Myanmar (my)
415 'MN', # Mongolei (mn)
416 'MV', # Malediven (dv) // dv_MV is unknown by babel
417 'MY', # Malaysia (ms)
418 'NP', # Nepal (ne)
419 'TJ', # Tadschikistan (tg)
420 'TM', # Turkmenistan (tk)
421 'UZ', # Usbekistan (uz)
422]
423
424
425def fetch_traits(engine_traits: EngineTraits, add_domains: bool = True):
426 """Fetch languages from Google."""
427 # pylint: disable=import-outside-toplevel, too-many-branches
428
429 engine_traits.custom['supported_domains'] = {}
430
431 resp = get('https://www.google.com/preferences')
432 if not resp.ok: # type: ignore
433 raise RuntimeError("Response from Google's preferences is not OK.")
434
435 dom = html.fromstring(resp.text.replace('<?xml version="1.0" encoding="UTF-8"?>', ''))
436
437 # supported language codes
438
439 lang_map = {'no': 'nb'}
440 for x in eval_xpath_list(dom, "//select[@name='hl']/option"):
441 eng_lang = x.get("value")
442 try:
443 locale = babel.Locale.parse(lang_map.get(eng_lang, eng_lang), sep='-')
444 except babel.UnknownLocaleError:
445 print("INFO: google UI language %s (%s) is unknown by babel" % (eng_lang, x.text.split("(")[0].strip()))
446 continue
447 sxng_lang = language_tag(locale)
448
449 conflict = engine_traits.languages.get(sxng_lang)
450 if conflict:
451 if conflict != eng_lang:
452 print("CONFLICT: babel %s --> %s, %s" % (sxng_lang, conflict, eng_lang))
453 continue
454 engine_traits.languages[sxng_lang] = 'lang_' + eng_lang
455
456 # alias languages
457 engine_traits.languages['zh'] = 'lang_zh-CN'
458
459 # supported region codes
460
461 for x in eval_xpath_list(dom, "//select[@name='gl']/option"):
462 eng_country = x.get("value")
463
464 if eng_country in skip_countries:
465 continue
466 if eng_country == 'ZZ':
467 engine_traits.all_locale = 'ZZ'
468 continue
469
470 sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=True)
471
472 if not sxng_locales:
473 print("ERROR: can't map from google country %s (%s) to a babel region." % (x.get('data-name'), eng_country))
474 continue
475
476 for sxng_locale in sxng_locales:
477 engine_traits.regions[region_tag(sxng_locale)] = eng_country
478
479 # alias regions
480 engine_traits.regions['zh-CN'] = 'HK'
481
482 # supported domains
483
484 if add_domains:
485 resp = get('https://www.google.com/supported_domains')
486 if not resp.ok: # type: ignore
487 raise RuntimeError("Response from https://www.google.com/supported_domains is not OK.")
488
489 for domain in resp.text.split(): # type: ignore
490 domain = domain.strip()
491 if not domain or domain in [
492 '.google.com',
493 ]:
494 continue
495 region = domain.split('.')[-1].upper()
496 engine_traits.custom['supported_domains'][region] = 'www' + domain # type: ignore
497 if region == 'HK':
498 # There is no google.cn, we use .com.hk for zh-CN
499 engine_traits.custom['supported_domains']['CN'] = 'www' + domain # type: ignore
request(query, params)
Definition google.py:258
EngineResults response(resp)
Definition google.py:319
get_google_info(params, eng_traits)
Definition google.py:74
fetch_traits(EngineTraits engine_traits, bool add_domains=True)
Definition google.py:425
detect_google_sorry(resp)
Definition google.py:253