#!/usr/bin/env python3 """Execute searches via a user-supplied SearXNG instance's JSON API. Calls GET/POST /search?q=...&format=json on the chosen instance. Auto-retries on failure with exponential backoff + tries next instance. Falls back to HTML scraping if the instance blocks JSON output. Instance URLs are REQUIRED (see --instance / SEARXNG_INSTANCE / config file). """ import argparse import csv import io import json import logging import os import random import re import sys import threading import time import urllib.error import urllib.parse import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed from html.parser import HTMLParser from pathlib import Path # Allow running standalone from any working directory sys.path.insert(0, str(Path(__file__).resolve().parent)) from _config import SCHEMA_VERSION, USER_AGENT, VERSION from common import ( RETRYABLE_STATUS, RETRY_BACKOFF_BASE, MAX_RETRIES, RECOVERY_HINTS, apply_proxy, build_auth_headers, build_wayback_url, classify_error, classify_fetch_error, compute_backoff_delay, detect_charset, emit_progress, force_utf8_stdout, is_hard_blocked_domain, is_similar, texts_are_similar, parse_retry_after, resolve_auth_basic, resolve_auth_bearer, set_progress_enabled, setup_logging, should_try_wayback, E_CONFIG, E_AUTH, E_NETWORK, E_RATE_LIMIT, E_PARSE, E_EMPTY, E_INPUT, E_INTERNAL, ) from fetch import extract_text, fetch_url import cache as cache_module logger = logging.getLogger("searxng.search") # ----- Auth helpers ----- def _merge_headers(*header_dicts: dict) -> dict: """Merge multiple header dicts, with later dicts overriding earlier ones.""" result = {} for d in header_dicts: if d: result.update(d) return result # ----- HTML fallback: result extractor ----- class SearXNGHTMLParser(HTMLParser): """Extract search results + metadata from SearXNG's simple theme HTML. Matches:
...

Title

Snippet...

...
...
The ``published_date`` field is populated from the ``