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

Senaste …


Sök


Nordh Tech
Privacy Overview

GDPR och kakor (cookies)

Den 25 maj 2018 började dataskyddsförordningen (GDPR) att gälla i Sverige och övriga EU.

Det innebär bland annat att inget företag eller organisation får spara personuppgifter som inte behövs för att leverera det du bett om och att du har rätt att ”bli glömd” så snart du ber om det.

För din kontakt med Nordh.Tech innebär det inte så mycket.

Vi sparar en kaka (cookie) vilket innebär en liten textfil med information om vad du gjort när du surfar hos oss, vi gör det för att kunna få statistik rörande vad du gjort när du besöker oss och för att vi skall kunna ge dig nytt innehåll och inte till exempel behöva fråga dig var gång du besöker oss om du godkänner våra GDPR-villkor eller att det sparas en ”kaka” på din dator.

Vi sparar också din epostadress för att kunna skicka dig vårt nyhetsbrev eller hantera din inloggning i vårt forum, om du bett om det vill säga. Då du prenumererar på nyhetsbrevet eller begär inloggning till vårt forum anger du också ditt förnamn eller inloggningsnamn men här kan du ange vilket namn som helst. Vissa anger även efternamn men dessa raderas så att endast förnamnet kommer att användas i hälsningsfrasen i varje nyhetsbrev.

Hoppas detta gör att du kan känna dig trygg med ditt besök hos mig här på Internet.