@@ -10,7 +10,7 @@ from html.parser import HTMLParser
10from urllib.parse import urljoin, urlparse
10from urllib.parse import urljoin, urlparse
12from lxml import html
12from lxml import html
13−from lxml.etree import XPath, _ElementStringResult, _ElementUnicodeResult
13+from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
14from babel.core import get_global
14from babel.core import get_global
@@ -18,6 +18,7 @@ from searx import settings
18from searx.data import USER_AGENTS
18from searx.data import USER_AGENTS
19from searx.version import VERSION_STRING
19from searx.version import VERSION_STRING
20from searx.languages import language_codes
20from searx.languages import language_codes
21+from searx.exceptions import SearxXPathSyntaxException, SearxEngineXPathException
21from searx import logger
22from searx import logger
@@ -33,6 +34,13 @@ xpath_cache = dict()
33lang_to_lc_cache = dict()
34lang_to_lc_cache = dict()
41+NOTSET = NotSetClass()
36def searx_useragent():
44def searx_useragent():
37 """Return the searx User Agent"""
45 """Return the searx User Agent"""
38 return 'searx/{searx_version} {suffix}'.format(
46 return 'searx/{searx_version} {suffix}'.format(
@@ -125,7 +133,7 @@ def html_to_text(html_str):
125 return s.get_text()
133 return s.get_text()
128−def extract_text(xpath_results):
136+def extract_text(xpath_results, allow_none=False):
129 """Extract text from a lxml result
137 """Extract text from a lxml result
131 * if xpath_results is list, extract the text from each result and concat the list
139 * if xpath_results is list, extract the text from each result and concat the list
@@ -133,22 +141,27 @@ def extract_text(xpath_results):
133 ( text_content() method from lxml )
141 ( text_content() method from lxml )
134 * if xpath_results is a string element, then it's already done
142 * if xpath_results is a string element, then it's already done
136− if type(xpath_results) == list:
144+ if isinstance(xpath_results, list):
137 # it's list of result : concat everything using recursive call
145 # it's list of result : concat everything using recursive call
138 result = ''
146 result = ''
139 for e in xpath_results:
147 for e in xpath_results:
140 result = result + extract_text(e)
148 result = result + extract_text(e)
141 return result.strip()
149 return result.strip()
142− elif type(xpath_results) in [_ElementStringResult, _ElementUnicodeResult]:
150+ elif isinstance(xpath_results, ElementBase):
144− return ''.join(xpath_results)
146 # it's a element
151 # it's a element
147 text = html.tostring(
152 text = html.tostring(
148 xpath_results, encoding='unicode', method='text', with_tail=False
153 xpath_results, encoding='unicode', method='text', with_tail=False
150 text = text.strip().replace('\n', ' ')
155 text = text.strip().replace('\n', ' ')
151 return ' '.join(text.split())
156 return ' '.join(text.split())
157+ elif isinstance(xpath_results, (_ElementStringResult, _ElementUnicodeResult, str, Number, bool)):
158+ return str(xpath_results)
159+ elif xpath_results is None and allow_none:
161+ elif xpath_results is None and not allow_none:
162+ raise ValueError('extract_text(None, allow_none=False)')
164+ raise ValueError('unsupported type')
154def normalize_url(url, base_url):
167def normalize_url(url, base_url):
@@ -170,7 +183,7 @@ def normalize_url(url, base_url):
170 >>> normalize_url('', 'https://example.com')
183 >>> normalize_url('', 'https://example.com')
171 'https://example.com/'
184 'https://example.com/'
172 >>> normalize_url('/test', '/path')
185 >>> normalize_url('/test', '/path')
173− raise Exception
186+ raise ValueError
176 * lxml.etree.ParserError
189 * lxml.etree.ParserError
@@ -194,7 +207,7 @@ def normalize_url(url, base_url):
195 # add a / at this end of the url if there is no path
208 # add a / at this end of the url if there is no path
196 if not parsed_url.netloc:
209 if not parsed_url.netloc:
197− raise Exception('Cannot parse url')
210+ raise ValueError('Cannot parse url')
198 if not parsed_url.path:
211 if not parsed_url.path:
199 url += '/'
212 url += '/'
@@ -224,17 +237,17 @@ def extract_url(xpath_results, base_url):
224 >>> f('', 'https://example.com')
237 >>> f('', 'https://example.com')
225 raise lxml.etree.ParserError
238 raise lxml.etree.ParserError
226 >>> searx.utils.extract_url([], 'https://example.com')
239 >>> searx.utils.extract_url([], 'https://example.com')
227− raise Exception
240+ raise ValueError
230− * Exception
243+ * ValueError
231 * lxml.etree.ParserError
244 * lxml.etree.ParserError
234 * str: normalized URL
247 * str: normalized URL
236 if xpath_results == []:
249 if xpath_results == []:
237− raise Exception('Empty url resultset')
250+ raise ValueError('Empty url resultset')
239 url = extract_text(xpath_results)
252 url = extract_text(xpath_results)
240 return normalize_url(url, base_url)
253 return normalize_url(url, base_url)
@@ -256,25 +269,6 @@ def dict_subset(d, properties):
256 return result
269 return result
259−def list_get(a_list, index, default=None):
260− """Get element in list or default value
263− >>> list_get(['A', 'B', 'C'], 0)
265− >>> list_get(['A', 'B', 'C'], 3)
267− >>> list_get(['A', 'B', 'C'], 3, 'default')
269− >>> list_get(['A', 'B', 'C'], -1)
272− if len(a_list) > index:
273− return a_list[index]
278def get_torrent_size(filesize, filesize_multiplier):
272def get_torrent_size(filesize, filesize_multiplier):
@@ -310,7 +304,7 @@ def get_torrent_size(filesize, filesize_multiplier):
310 filesize = int(filesize * 1000 * 1000)
304 filesize = int(filesize * 1000 * 1000)
311 elif filesize_multiplier == 'KiB':
305 elif filesize_multiplier == 'KiB':
312 filesize = int(filesize * 1000)
306 filesize = int(filesize * 1000)
313− except:
307+ except ValueError:
314 filesize = None
308 filesize = None
316 return filesize
310 return filesize
@@ -506,20 +500,110 @@ def get_engine_from_settings(name):
506 return {}
500 return {}
509−def get_xpath(xpath_str):
503+def get_xpath(xpath_spec):
510 """Return cached compiled XPath
504 """Return cached compiled XPath
512 There is no thread lock.
506 There is no thread lock.
513 Worst case scenario, xpath_str is compiled more than one time.
507 Worst case scenario, xpath_str is compiled more than one time.
510+ * xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
513+ * result (bool, float, list, str): Results.
516+ * TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
517+ * SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
519+ if isinstance(xpath_spec, str):
515− result = xpath_cache.get(xpath_str, None)
520+ result = xpath_cache.get(xpath_spec, None)
516− if result is None:
521+ if result is None:
517− result = XPath(xpath_str)
523+ result = XPath(xpath_spec)
524+ except XPathSyntaxError as e:
525+ raise SearxXPathSyntaxException(xpath_spec, str(e.msg))
518− xpath_cache[xpath_str] = result
526+ xpath_cache[xpath_spec] = result
529+ if isinstance(xpath_spec, XPath):
532+ raise TypeError('xpath_spec must be either a str or a lxml.etree.XPath')
535+def eval_xpath(element, xpath_spec):
536+ """Equivalent of element.xpath(xpath_str) but compile xpath_str once for all.
537+ See https://lxml.de/xpathxslt.html#xpath-return-values
540+ * element (ElementBase): [description]
541+ * xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
544+ * result (bool, float, list, str): Results.
547+ * TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
548+ * SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
549+ * SearxEngineXPathException: Raise when the XPath can't be evaluated.
551+ xpath = get_xpath(xpath_spec)
553+ return xpath(element)
554+ except XPathError as e:
555+ arg = ' '.join([str(i) for i in e.args])
556+ raise SearxEngineXPathException(xpath_spec, arg)
559+def eval_xpath_list(element, xpath_spec, min_len=None):
560+ """Same as eval_xpath, check if the result is a list
563+ * element (ElementBase): [description]
564+ * xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
565+ * min_len (int, optional): [description]. Defaults to None.
568+ * TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
569+ * SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
570+ * SearxEngineXPathException: raise if the result is not a list
573+ * result (bool, float, list, str): Results.
575+ result = eval_xpath(element, xpath_spec)
576+ if not isinstance(result, list):
577+ raise SearxEngineXPathException(xpath_spec, 'the result is not a list')
578+ if min_len is not None and min_len > len(result):
579+ raise SearxEngineXPathException(xpath_spec, 'len(xpath_str) < ' + str(min_len))
519 return result
580 return result
522−def eval_xpath(element, xpath_str):
583+def eval_xpath_getindex(elements, xpath_spec, index, default=NOTSET):
523− """Equivalent of element.xpath(xpath_str) but compile xpath_str once for all."""
584+ """Call eval_xpath_list then get one element using the index parameter.
524− xpath = get_xpath(xpath_str)
585+ If the index does not exist, either aise an exception is default is not set,
525− return xpath(element)
586+ other return the default value (can be None).
589+ * elements (ElementBase): lxml element to apply the xpath.
590+ * xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath.
591+ * index (int): index to get
592+ * default (Object, optional): Defaults if index doesn't exist.
595+ * TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
596+ * SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
597+ * SearxEngineXPathException: if the index is not found. Also see eval_xpath.
600+ * result (bool, float, list, str): Results.
602+ result = eval_xpath_list(elements, xpath_spec)
603+ if index >= -len(result) and index < len(result):
604+ return result[index]
605+ if default == NOTSET:
606+ # raise an SearxEngineXPathException instead of IndexError
607+ # to record xpath_spec
608+ raise SearxEngineXPathException(xpath_spec, 'index ' + str(index) + ' not found')