
Hur ofta önskar man inte att man behöver uppdatera visst material på sin hemsida, kanske en tabell med NTP-servrar man listar fram via DNS-uppslag eller att manuellt behöva sitta och rensa en lista med SearXNG-instanser man tycker är värda att rekommendera för andra? En liten Raspberry Pi av valfri modell är lösningen på detta …
WordPress
I WordPress kan man börja med att gå till användarsektionen, lägga till en ny användare som minst redaktör/editor och sedan längst ner skapa ett applikationslösenord för denna.
Därefter redigerar man den sida man önskar uppdatera med kodredigerare och adderar en starttagg:
<!-- START: searxng -->samt en sluttagg:
<!-- END: searxng -->Du kan ha flera start- och sluttaggar på en sida, bara ge dem olika ”namn”.
Notera också vilket sid-ID denna sida har, när du är inne och redigerar sidan ser du i den aktuella URL:n bland annat ”post=1406” och i detta fall har sidan ID 1406.
Nu är du klar i WordPress, du har sidans ID, du har lagt in start- och sluttagg och du har en något begränsad användare med ett applikationslösenord satt. Gott!
Pythonskriptet
I det här fallet handlar det om att skrapa uppgifter från sidan searx.space där det listas ett flertal, ofta runt 70-talet, instanser men där jag var morgon vill uppdatera en tabell med de SearXNG-instanser som håller en bra kvalité, dels vad gäller prestanda men också håller en bra säkerhetsnivå.
För denna funktion har jag valt att skapa ett Python-skript. Jag börjar med lite import av grundfunktioner …
# =========================================================
# A) IMPORTS & KONFIGURATION
# =========================================================
import sys
import time
import random
import re
import html
import requests
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup… samt anger lite WordPress-info.
# --- WordPress-konfiguration ---
WP_PAGE_ID = <sidans ID> # <-- BYT TILL RÄTT SID-ID
WP_USER = "användar-ID för redaktören"
WP_APP_PASS = "Användarlösen"
START_MARKER = "<!-- START: searxng -->"
END_MARKER = "<!-- END: searxng -->"Sedan använder jag Playwright och Stealth för att hämta sidans data från https://searx.space …
# =========================================================
# B) HÄMTA SEARX.SPACE (PLAYWRIGHT + STEALTH)
# =========================================================
def apply_stealth(page):
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
Object.defineProperty(navigator, 'plugins', { get: () => [1,2,3] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });
Object.defineProperty(navigator, 'deviceMemory', { get: () => 4 });
Object.defineProperty(navigator, 'userAgent', {
get: () => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
});
""")
def fetch_table_html():
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process"
]
)
page = browser.new_page()
apply_stealth(page)
page.goto("https://searx.space", wait_until="networkidle", timeout=30000)
page.wait_for_selector("table", timeout=15000)
html = page.inner_html("table")
browser.close()
return htmlFör att sedan extrahera det textbaserade innehållet vi hämtar som en salig soppa så använder jag lite lämpligt BeautifulSoup 😉
# =========================================================
# C) EXTRAHERA TABELLEN (BEAUTIFULSOUP)
# =========================================================
def text_or_empty(el):
return el.get_text(strip=True) if el else ""
def extract_instances(table_html):
soup = BeautifulSoup(table_html, "html.parser")
rows = soup.select("tbody tr")
instances = []
for row in rows:
url_el = row.select_one("td.column-url a")
url = url_el.get("href", "").strip() if url_el else ""
version = text_or_empty(row.select_one("td.column-version"))
tls = text_or_empty(row.select_one("td.column-tls .value-tls"))
csp = text_or_empty(row.select_one("td.column-csp .value-tls"))
html_grade = text_or_empty(row.select_one("td.column-html .value-html"))
certificate = text_or_empty(row.select_one("td.column-certificate a"))
ipv6 = text_or_empty(row.select_one("td.column-ipv6 .value-ipv6"))
country = text_or_empty(row.select_one("td.column-country span"))
network = text_or_empty(row.select_one("td.column-network .value-network"))
rt = row.select("td.column-responsetime .value-responsetime")
search_rt = text_or_empty(rt[0]) if len(rt) > 0 else ""
google_rt = text_or_empty(rt[1]) if len(rt) > 1 else ""
initial_rt = text_or_empty(rt[2]) if len(rt) > 2 else ""
uptime = text_or_empty(row.select_one("td.column-uptime .value-uptime"))
instances.append({
"URL": url,
"Version": version,
"TLS": tls,
"CSP": csp,
"HTML": html_grade,
"Certificate": certificate,
"IPv6": ipv6,
"Country": country,
"Network": network,
"Search": search_rt,
"Google": google_rt,
"Initial": initial_rt,
"Uptime": uptime,
})
return instancesDärefter behöver jag filtrera bort alla instanser med långsamma söktider, äldre instansversioner eller de med dålig säkerhet för sina användare.
# =========================================================
# D) FILTRERING
# =========================================================
def parse_version_date(version_str):
m = re.match(r"(\d{4})\.(\d{1,2})\.(\d{1,2})", version_str)
if not m:
return None
return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), tzinfo=timezone.utc)
def version_is_recent(version_str):
d = parse_version_date(version_str)
if not d:
return False
return d >= (datetime.now(timezone.utc) - timedelta(days=7))
def parse_response_time(value):
m = re.search(r"(\d+\.\d+|\d+)", value)
return float(m.group(1)) if m else 999
def parse_uptime(value):
m = re.search(r"(\d+(?:\.\d+)?)", value)
return float(m.group(1)) if m else 0.0
def is_valid(inst):
if inst["TLS"] != "A+":
return False
if inst["CSP"] != "A+":
return False
if parse_response_time(inst["Search"]) > 2:
return False
if parse_response_time(inst["Google"]) > 2:
return False
if parse_response_time(inst["Initial"]) > 2:
return False
if parse_uptime(inst["Uptime"]) < 99.0:
return False
if not version_is_recent(inst["Version"]):
return False
return TrueNu är det dags att skapa HTML-koden för sparandet av den uppdaterade tabellen med lite av den vidhängande texten.
# =========================================================
# E) GENERERA HTML
# =========================================================
def generate_html(instances):
now_se = datetime.now(ZoneInfo("Europe/Stockholm"))
timestamp = now_se.strftime("%Y-%m-%d %H:%M.")
total = len(instances)
rows = []
for inst in instances:
rows.append(
"<tr>"
f"<td><a href=\"{html.escape(inst['URL'])}\" target=\"_blank\">{html.escape(inst['URL'])}</a></td>"
f"<td>{html.escape(inst['Version'])}</td>"
f"<td>{html.escape(inst['Country'])}</td>"
"</tr>"
)
return f"""
<style>
table {{
border-collapse: collapse;
width: 100%;
}}
th, td {{
border: 1px solid #ccc;
padding: 6px;
font-family: sans-serif;
text-align: left;
}}
th {{
background: #eee;
}}
</style>
<div>Listan är uppdaterad {timestamp}</div>
<table>
<tr><th>URL</th><th>Version</th><th>Land</th></tr>
{''.join(rows)}
</table>
<p>Antal instanser hittade som uppfyller baskraven på säkerhet och prestanda, vid senaste kontrollen, är {total} stycken.</p>
"""Då HTML-koden skapats och lite text adderats så laddar jag upp koden med följande Pythonkod och adderar HTML mellan start- och slutmarkörerna.
# =========================================================
# F) UPPDATERA WORDPRESS-BLOCKET
# =========================================================
def update_wordpress_block(new_html):
url = f"https:<sökvägen till min sida>/{WP_PAGE_ID}"
page = requests.get(url + "?context=edit", auth=(WP_USER, WP_APP_PASS)).json()
# print(page) # <-- Lägg till denna rad
content = page["content"]["raw"]
pattern = f"{START_MARKER}.*?{END_MARKER}"
replacement = f"{START_MARKER}\n{new_html}\n{END_MARKER}"
updated = re.sub(pattern, replacement, content, flags=re.DOTALL)
data = {"content": updated}
requests.post(url, json=data, auth=(WP_USER, WP_APP_PASS))Slutligen ser min ”main pipeline” ut såhär där jag väljer att inte genomföra uppdateringen om inga instanser kan hämtas från https://searx.space …
# =========================================================
# G) MAIN PIPELINE
# =========================================================
def main():
table_html = fetch_table_html()
instances = extract_instances(table_html)
valid = [i for i in instances if is_valid(i)]
if len(valid) == 0:
print("Inga instanser passerade filtren — WordPress uppdateras inte.")
return
html_out = generate_html(valid)
update_wordpress_block(html_out)
print(f"Publicerat {len(valid)} instanser till WordPress.")
if __name__ == "__main__":
main()Det var alles, blir inte roligare än såhär och just https://searx.space var lite speciell att skrapa data från då all data genereras via ett klientbaserat javaskript så det var jag tvungen att ta hänsyn till … också …
Ha det gott,
//Anders
