Att pusha uppdateringar till WordPress?

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 html

Fö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 instances

Dä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 True

Nu ä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