diff options
Diffstat (limited to 'searx/engines')
| -rw-r--r-- | searx/engines/google.py | 142 | ||||
| -rw-r--r-- | searx/engines/qwant.py | 98 | ||||
| -rw-r--r-- | searx/engines/swisscows.py | 108 | ||||
| -rw-r--r-- | searx/engines/youtube_api.py | 83 | ||||
| -rw-r--r-- | searx/engines/youtube_noapi.py | 88 |
5 files changed, 503 insertions, 16 deletions
diff --git a/searx/engines/google.py b/searx/engines/google.py index 807c58ed5..785cd5e66 100644 --- a/searx/engines/google.py +++ b/searx/engines/google.py @@ -14,18 +14,76 @@ from lxml import html from searx.poolrequests import get from searx.engines.xpath import extract_text, extract_url + # engine dependent config categories = ['general'] paging = True language_support = True +use_locale_domain = True + +# based on https://en.wikipedia.org/wiki/List_of_Google_domains and tests +default_hostname = 'www.google.com' + +country_to_hostname = { + 'BG': 'www.google.bg', # Bulgaria + 'CZ': 'www.google.cz', # Czech Republic + 'DE': 'www.google.de', # Germany + 'DK': 'www.google.dk', # Denmark + 'AT': 'www.google.at', # Austria + 'CH': 'www.google.ch', # Switzerland + 'GR': 'www.google.gr', # Greece + 'AU': 'www.google.com.au', # Australia + 'CA': 'www.google.ca', # Canada + 'GB': 'www.google.co.uk', # United Kingdom + 'ID': 'www.google.co.id', # Indonesia + 'IE': 'www.google.ie', # Ireland + 'IN': 'www.google.co.in', # India + 'MY': 'www.google.com.my', # Malaysia + 'NZ': 'www.google.co.nz', # New Zealand + 'PH': 'www.google.com.ph', # Philippines + 'SG': 'www.google.com.sg', # Singapore + # 'US': 'www.google.us', # United State, redirect to .com + 'ZA': 'www.google.co.za', # South Africa + 'AR': 'www.google.com.ar', # Argentina + 'CL': 'www.google.cl', # Chile + 'ES': 'www.google.es', # Span + 'MX': 'www.google.com.mx', # Mexico + 'EE': 'www.google.ee', # Estonia + 'FI': 'www.google.fi', # Finland + 'BE': 'www.google.be', # Belgium + 'FR': 'www.google.fr', # France + 'IL': 'www.google.co.il', # Israel + 'HR': 'www.google.hr', # Croatia + 'HU': 'www.google.hu', # Hungary + 'IT': 'www.google.it', # Italy + 'JP': 'www.google.co.jp', # Japan + 'KR': 'www.google.co.kr', # South Korean + 'LT': 'www.google.lt', # Lithuania + 'LV': 'www.google.lv', # Latvia + 'NO': 'www.google.no', # Norway + 'NL': 'www.google.nl', # Netherlands + 'PL': 'www.google.pl', # Poland + 'BR': 'www.google.com.br', # Brazil + 'PT': 'www.google.pt', # Portugal + 'RO': 'www.google.ro', # Romania + 'RU': 'www.google.ru', # Russia + 'SK': 'www.google.sk', # Slovakia + 'SL': 'www.google.si', # Slovenia (SL -> si) + 'SE': 'www.google.se', # Sweden + 'TH': 'www.google.co.th', # Thailand + 'TR': 'www.google.com.tr', # Turkey + 'UA': 'www.google.com.ua', # Ikraine + # 'CN': 'www.google.cn', # China, only from china ? + 'HK': 'www.google.com.hk', # Hong kong + 'TW': 'www.google.com.tw' # Taiwan +} # search-url -google_hostname = 'www.google.com' search_path = '/search' +maps_path = '/maps/' redirect_path = '/url' images_path = '/images' -search_url = ('https://' + - google_hostname + +search_url = ('https://{hostname}' + search_path + '?{query}&start={offset}&gbv=1') @@ -34,6 +92,7 @@ results_xpath = '//li[@class="g"]' url_xpath = './/h3/a/@href' title_xpath = './/h3' content_xpath = './/span[@class="st"]' +content_misc_xpath = './/div[@class="f slp"]' suggestion_xpath = '//p[@class="_Bmc"]' images_xpath = './/div/a' @@ -41,6 +100,7 @@ image_url_xpath = './@href' image_img_src_xpath = './img/@src' pref_cookie = '' +nid_cookie = {} # see https://support.google.com/websearch/answer/873?hl=en @@ -52,8 +112,16 @@ def get_google_pref_cookie(): return pref_cookie +def get_google_nid_cookie(google_hostname): + global nid_cookie + if google_hostname not in nid_cookie: + resp = get('https://' + google_hostname) + nid_cookie[google_hostname] = resp.cookies.get("NID", None) + return nid_cookie[google_hostname] + + # remove google-specific tracking-url -def parse_url(url_string): +def parse_url(url_string, google_hostname): parsed_url = urlparse(url_string) if (parsed_url.netloc in [google_hostname, ''] and parsed_url.path == redirect_path): @@ -63,21 +131,45 @@ def parse_url(url_string): return url_string +# returns extract_text on the first result selected by the xpath or None +def extract_text_from_dom(result, xpath): + r = result.xpath(xpath) + if len(r) > 0: + return extract_text(r[0]) + return None + + # do search-request def request(query, params): offset = (params['pageno'] - 1) * 10 if params['language'] == 'all': language = 'en' + country = 'US' else: - language = params['language'].replace('_', '-').lower() + language_array = params['language'].lower().split('_') + if len(language_array) == 2: + country = language_array[1] + else: + country = ' ' + language = language_array[0] + ',' + language_array[0] + '-' + country + + if use_locale_domain: + google_hostname = country_to_hostname.get(country.upper(), default_hostname) + else: + google_hostname = default_hostname params['url'] = search_url.format(offset=offset, - query=urlencode({'q': query})) + query=urlencode({'q': query}), + hostname=google_hostname) params['headers']['Accept-Language'] = language - if language.startswith('en'): + params['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + if google_hostname == default_hostname: params['cookies']['PREF'] = get_google_pref_cookie() + params['cookies']['NID'] = get_google_nid_cookie(google_hostname) + + params['google_hostname'] = google_hostname return params @@ -86,17 +178,30 @@ def request(query, params): def response(resp): results = [] + # detect google sorry + resp_url = urlparse(resp.url) + if resp_url.netloc == 'sorry.google.com' or resp_url.path == '/sorry/IndexRedirect': + raise RuntimeWarning('sorry.google.com') + + # which hostname ? + google_hostname = resp.search_params.get('google_hostname') + google_url = "https://" + google_hostname + + # convert the text to dom dom = html.fromstring(resp.text) # parse results for result in dom.xpath(results_xpath): title = extract_text(result.xpath(title_xpath)[0]) try: - url = parse_url(extract_url(result.xpath(url_xpath), search_url)) - parsed_url = urlparse(url) + url = parse_url(extract_url(result.xpath(url_xpath), google_url), google_hostname) + parsed_url = urlparse(url, google_hostname) if (parsed_url.netloc == google_hostname - and parsed_url.path == search_path): - # remove the link to google news + and (parsed_url.path == search_path + or parsed_url.path.startswith(maps_path))): + # remove the link to google news and google maps + # FIXME : sometimes the URL is https://maps.google.*/maps + # no consequence, the result trigger an exception after which is ignored continue # images result @@ -104,16 +209,21 @@ def response(resp): and parsed_url.path == images_path): # only thumbnail image provided, # so skipping image results - # results = results + parse_images(result) + # results = results + parse_images(result, google_hostname) pass else: # normal result - content = extract_text(result.xpath(content_xpath)[0]) + content = extract_text_from_dom(result, content_xpath) + if content is None: + continue + content_misc = extract_text_from_dom(result, content_misc_xpath) + if content_misc is not None: + content = content_misc + "<br />" + content # append result results.append({'url': url, 'title': title, 'content': content}) - except: + except Exception: continue # parse suggestion @@ -125,10 +235,10 @@ def response(resp): return results -def parse_images(result): +def parse_images(result, google_hostname): results = [] for image in result.xpath(images_xpath): - url = parse_url(extract_text(image.xpath(image_url_xpath)[0])) + url = parse_url(extract_text(image.xpath(image_url_xpath)[0]), google_hostname) img_src = extract_text(image.xpath(image_img_src_xpath)[0]) # append result diff --git a/searx/engines/qwant.py b/searx/engines/qwant.py new file mode 100644 index 000000000..872bd4e95 --- /dev/null +++ b/searx/engines/qwant.py @@ -0,0 +1,98 @@ +""" + Qwant (Web, Images, News, Social) + + @website https://qwant.com/ + @provide-api not officially (https://api.qwant.com/api/search/) + + @using-api yes + @results JSON + @stable yes + @parse url, title, content +""" + +from urllib import urlencode +from json import loads +from datetime import datetime + +# engine dependent config +categories = None +paging = True +language_support = True + +category_to_keyword = {'general': 'web', + 'images': 'images', + 'news': 'news', + 'social media': 'social'} + +# search-url +url = 'https://api.qwant.com/api/search/{keyword}?count=10&offset={offset}&f=&{query}' + + +# do search-request +def request(query, params): + offset = (params['pageno'] - 1) * 10 + + if categories[0] and categories[0] in category_to_keyword: + + params['url'] = url.format(keyword=category_to_keyword[categories[0]], + query=urlencode({'q': query}), + offset=offset) + else: + params['url'] = url.format(keyword='web', + query=urlencode({'q': query}), + offset=offset) + + # add language tag if specified + if params['language'] != 'all': + params['url'] += '&locale=' + params['language'].lower() + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if 'data' not in search_results: + return [] + + data = search_results.get('data', {}) + + res = data.get('result', {}) + + # parse results + for result in res.get('items', {}): + + title = result['title'] + res_url = result['url'] + content = result['desc'] + + if category_to_keyword.get(categories[0], '') == 'web': + results.append({'title': title, + 'content': content, + 'url': res_url}) + + elif category_to_keyword.get(categories[0], '') == 'images': + thumbnail_src = result['thumbnail'] + img_src = result['media'] + results.append({'template': 'images.html', + 'url': res_url, + 'title': title, + 'content': '', + 'thumbnail_src': thumbnail_src, + 'img_src': img_src}) + + elif (category_to_keyword.get(categories[0], '') == 'news' or + category_to_keyword.get(categories[0], '') == 'social'): + published_date = datetime.fromtimestamp(result['date'], None) + + results.append({'url': res_url, + 'title': title, + 'publishedDate': published_date, + 'content': content}) + + # return results + return results diff --git a/searx/engines/swisscows.py b/searx/engines/swisscows.py new file mode 100644 index 000000000..2d31264ca --- /dev/null +++ b/searx/engines/swisscows.py @@ -0,0 +1,108 @@ +""" + Swisscows (Web, Images) + + @website https://swisscows.ch + @provide-api no + + @using-api no + @results HTML (using search portal) + @stable no (HTML can change) + @parse url, title, content +""" + +from json import loads +from urllib import urlencode, unquote +import re + +# engine dependent config +categories = ['general', 'images'] +paging = True +language_support = True + +# search-url +base_url = 'https://swisscows.ch/' +search_string = '?{query}&page={page}' + +# regex +regex_json = re.compile('initialData: {"Request":(.|\n)*},\s*environment') +regex_json_remove_start = re.compile('^initialData:\s*') +regex_json_remove_end = re.compile(',\s*environment$') +regex_img_url_remove_start = re.compile('^https?://i\.swisscows\.ch/\?link=') + + +# do search-request +def request(query, params): + if params['language'] == 'all': + ui_language = 'browser' + region = 'browser' + else: + region = params['language'].replace('_', '-') + ui_language = params['language'].split('_')[0] + + search_path = search_string.format( + query=urlencode({'query': query, + 'uiLanguage': ui_language, + 'region': region}), + page=params['pageno']) + + # image search query is something like 'image?{query}&page={page}' + if params['category'] == 'images': + search_path = 'image' + search_path + + params['url'] = base_url + search_path + + return params + + +# get response from search-request +def response(resp): + results = [] + + json_regex = regex_json.search(resp.content) + + # check if results are returned + if not json_regex: + return [] + + json_raw = regex_json_remove_end.sub('', regex_json_remove_start.sub('', json_regex.group())) + json = loads(json_raw) + + # parse results + for result in json['Results'].get('items', []): + result_title = result['Title'].replace(u'\uE000', '').replace(u'\uE001', '') + + # parse image results + if result.get('ContentType', '').startswith('image'): + img_url = unquote(regex_img_url_remove_start.sub('', result['Url'])) + + # append result + results.append({'url': result['SourceUrl'], + 'title': result['Title'], + 'content': '', + 'img_src': img_url, + 'template': 'images.html'}) + + # parse general results + else: + result_url = result['Url'].replace(u'\uE000', '').replace(u'\uE001', '') + result_content = result['Description'].replace(u'\uE000', '').replace(u'\uE001', '') + + # append result + results.append({'url': result_url, + 'title': result_title, + 'content': result_content}) + + # parse images + for result in json.get('Images', []): + # decode image url + img_url = unquote(regex_img_url_remove_start.sub('', result['Url'])) + + # append result + results.append({'url': result['SourceUrl'], + 'title': result['Title'], + 'content': '', + 'img_src': img_url, + 'template': 'images.html'}) + + # return results + return results diff --git a/searx/engines/youtube_api.py b/searx/engines/youtube_api.py new file mode 100644 index 000000000..8fd939a25 --- /dev/null +++ b/searx/engines/youtube_api.py @@ -0,0 +1,83 @@ +# Youtube (Videos) +# +# @website https://www.youtube.com/ +# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list) +# +# @using-api yes +# @results JSON +# @stable yes +# @parse url, title, content, publishedDate, thumbnail, embedded + +from json import loads +from urllib import urlencode +from dateutil import parser + +# engine dependent config +categories = ['videos', 'music'] +paging = False +language_support = True +api_key = None + +# search-url +base_url = 'https://www.googleapis.com/youtube/v3/search' +search_url = base_url + '?part=snippet&{query}&maxResults=20&key={api_key}' + +embedded_url = '<iframe width="540" height="304" ' +\ + 'data-src="//www.youtube-nocookie.com/embed/{videoid}" ' +\ + 'frameborder="0" allowfullscreen></iframe>' + +base_youtube_url = 'https://www.youtube.com/watch?v=' + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), + api_key=api_key) + + # add language tag if specified + if params['language'] != 'all': + params['url'] += '&relevanceLanguage=' + params['language'].split('_')[0] + + return params + + +# get response from search-request +def response(resp): + results = [] + + search_results = loads(resp.text) + + # return empty array if there are no results + if 'items' not in search_results: + return [] + + # parse results + for result in search_results['items']: + videoid = result['id']['videoId'] + + title = result['snippet']['title'] + content = '' + thumbnail = '' + + pubdate = result['snippet']['publishedAt'] + publishedDate = parser.parse(pubdate) + + thumbnail = result['snippet']['thumbnails']['high']['url'] + + content = result['snippet']['description'] + + url = base_youtube_url + videoid + + embedded = embedded_url.format(videoid=videoid) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results diff --git a/searx/engines/youtube_noapi.py b/searx/engines/youtube_noapi.py new file mode 100644 index 000000000..108b8950f --- /dev/null +++ b/searx/engines/youtube_noapi.py @@ -0,0 +1,88 @@ +# Youtube (Videos) +# +# @website https://www.youtube.com/ +# @provide-api yes (https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list) +# +# @using-api no +# @results HTML +# @stable no +# @parse url, title, content, publishedDate, thumbnail, embedded + +from urllib import quote_plus +from lxml import html +from searx.engines.xpath import extract_text + +# engine dependent config +categories = ['videos', 'music'] +paging = True +language_support = False + +# search-url +base_url = 'https://www.youtube.com/results' +search_url = base_url + '?search_query={query}&page={page}' + +embedded_url = '<iframe width="540" height="304" ' +\ + 'data-src="//www.youtube-nocookie.com/embed/{videoid}" ' +\ + 'frameborder="0" allowfullscreen></iframe>' + +base_youtube_url = 'https://www.youtube.com/watch?v=' + +# specific xpath variables +results_xpath = "//ol/li/div[contains(@class, 'yt-lockup yt-lockup-tile yt-lockup-video vve-check')]" +url_xpath = './/h3/a/@href' +title_xpath = './/div[@class="yt-lockup-content"]/h3/a' +content_xpath = './/div[@class="yt-lockup-content"]/div[@class="yt-lockup-description yt-ui-ellipsis yt-ui-ellipsis-2"]' + + +# get element in list or default value +def list_get(a_list, index, default=None): + if len(a_list) > index: + return a_list[index] + else: + return default + + +# returns extract_text on the first result selected by the xpath or None +def extract_text_from_dom(result, xpath): + r = result.xpath(xpath) + if len(r) > 0: + return extract_text(r[0]) + return None + + +# do search-request +def request(query, params): + params['url'] = search_url.format(query=quote_plus(query), + page=params['pageno']) + + return params + + +# get response from search-request +def response(resp): + results = [] + + dom = html.fromstring(resp.text) + + # parse results + for result in dom.xpath(results_xpath): + videoid = list_get(result.xpath('@data-context-item-id'), 0) + if videoid is not None: + url = base_youtube_url + videoid + thumbnail = 'https://i.ytimg.com/vi/' + videoid + '/hqdefault.jpg' + + title = extract_text_from_dom(result, title_xpath) or videoid + content = extract_text_from_dom(result, content_xpath) + + embedded = embedded_url.format(videoid=videoid) + + # append result + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'embedded': embedded, + 'thumbnail': thumbnail}) + + # return results + return results |