xpath_flex.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """
  3. xPath Flex Engine (xpath_flex)
  4. Aims to be a more flexible engine that covers xpath use cases for all categories
  5. (as it allows to override the result template).
  6. The engine configuration expects you to specify a 'field_definition' dictionary with the following properties:
  7. * field_name: the template field that should be set by this entry.
  8. * xpath: a xpath expression to apply
  9. * (optional) single_element: if True, will execute the xpath expression to expect a single node, not a list
  10. * (optional) extract: a string specifying an extraction/conversion of the value, the following is supported:
  11. "url": extracts an url using base search url
  12. "boolean": will convert the value passed to the template to True if the xpath matches any node
  13. "boolean_negate": will convert the value passed to the template to True if the xpath does NOT match a node
  14. by default, text will be extracted from the found node(s)
  15. an example would look like this (for an example shopping site with paging):
  16. - name : xpath-flex-example
  17. engine : xpath_flex
  18. paging : True
  19. search_url : https://myl-shopping.site/search?q={query}&p={pageno}
  20. template : products.html
  21. results_xpath : //div[@class="listing--container"]/div[@class="listing"]/div[contains(@class,"product--box")]
  22. field_definition:
  23. - field_name: url
  24. xpath: (.//a[contains(@class,"product--image")])/@href
  25. extract: url
  26. - field_name: title
  27. xpath: (.//a[contains(@class,"product--image")])/@title
  28. - field_name: content
  29. xpath: .//div[@class="product--description"]/text()
  30. - field_name: price
  31. xpath: .//div[@class="product--price"]/span/text()
  32. - field_name: thumbnail
  33. xpath: substring-before( (.//span[@class="image--media"]/img)/@srcset, ", ")
  34. extract: url
  35. single_element: True
  36. - field_name: has_stock
  37. xpath: .//a[contains(@class,"buynow")][not(contains(@class,"is--disabled"))]
  38. extract: boolean
  39. single_element: True
  40. """
  41. from lxml import html
  42. from urllib.parse import urlencode
  43. from searx import logger
  44. from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list
  45. logger = logger.getChild('xpath_general engine')
  46. search_url = None
  47. paging = False
  48. results_xpath = ''
  49. soft_max_redirects = 0
  50. template = 'default.html'
  51. unresolvable_value = '' # will be set if expression cannot be resolved
  52. default_field_settings = {'single_element': False}
  53. field_definition = {}
  54. # parameters for engines with paging support
  55. #
  56. # number of results on each page
  57. # (only needed if the site requires not a page number, but an offset)
  58. page_size = 1
  59. # number of the first page (usually 0 or 1)
  60. first_page_num = 1
  61. def request(query, params):
  62. query = urlencode({'q': query})[2:]
  63. fp = {'query': query}
  64. if paging and search_url.find('{pageno}') >= 0:
  65. fp['pageno'] = (params['pageno'] - 1) * page_size + first_page_num
  66. params['url'] = search_url.format(**fp)
  67. params['query'] = query
  68. params['soft_max_redirects'] = soft_max_redirects
  69. return params
  70. def response(resp):
  71. results = []
  72. dom = html.fromstring(resp.text)
  73. for result in eval_xpath_list(dom, results_xpath):
  74. single_result = {
  75. 'template': template
  76. }
  77. for single_field in field_definition:
  78. single_field = {**default_field_settings, **single_field}
  79. try:
  80. if single_field['single_element']:
  81. node = eval_xpath(result, single_field['xpath'])
  82. else:
  83. node = eval_xpath_list(result, single_field['xpath'])
  84. if 'extract' in single_field and single_field['extract'] == 'url':
  85. value = extract_url(node, search_url)
  86. elif 'extract' in single_field and single_field['extract'] == 'boolean':
  87. value = (isinstance(node, list) and len(node) > 0)
  88. elif 'extract' in single_field and single_field['extract'] == 'boolean_negate':
  89. value = (isinstance(node, list) and len(node) < 1)
  90. else:
  91. value = extract_text(node)
  92. single_result[single_field['field_name']] = value
  93. except Exception as e:
  94. logger.warning('error in resolving field %s:\n%s', single_field['field_name'], e)
  95. single_result[single_field['field_name']] = unresolvable_value
  96. results.append(single_result)
  97. return results