Naar de inhoud springen

Backlink-data als JSON: $100 gratis tegoed, geen creditcard, aanmelden met Google.

SEOMasters.dev
SEOMasters.devAanmelden

Bulk DR voor een klantenlijst, als een Python-job van 40 regels

Je hebt een spreadsheet met domeinen en een dashboard dat één getal per rij nodig heeft. Een backlink-domeincontrole voor die taak is vooral infrastructuur, en dit script is die infrastructuur.

Handen sorteren houten blokken in stapels op een werkbank

Het script

Het leest domains.txt, verzendt 200 domeinen per verzoek, bewaart elke ruwe respons in SQLite en geeft de kosten van de run weer. We slaan het volledige payload op omdat veldnamen het beste uit een echte respons komen, niet uit een blogpost.

python
import os, sqlite3, time, json, requests

BASE = "https://seomasters.dev/api.php"
KEY = os.environ["SEO_API_KEY"]
BATCH = 200

db = sqlite3.connect("dr.db")
db.execute("create table if not exists raw (ts int, batch text, body text)")

domains = [d.strip() for d in open("domains.txt") if d.strip()]
spent = 0
for i in range(0, len(domains), BATCH):
    chunk = domains[i:i + BATCH]
    for attempt in range(4):
        try:
            r = requests.get(BASE, timeout=30,
                params={"action": "bulk_dr", "domains": ",".join(chunk)},
                headers={"Authorization": "Bearer " + KEY})
            break
        except requests.RequestException:
            time.sleep(2 ** attempt)
    else:
        raise SystemExit(f"network gave up at batch {i}")
    if r.status_code in (401, 402):
        raise SystemExit(f"HTTP {r.status_code}: fix the key or top up, do not retry")
    body = r.json()
    if not body.get("ok"):
        raise SystemExit(f"ok=false at batch {i}: {json.dumps(body)[:300]}")
    db.execute("insert into raw values (?,?,?)", (int(time.time()), str(i), r.text))
    db.commit()
    print(body.get("_credits"))

Wat elk onderdeel doet

action=bulk_dr

Eén credit per aanroep. De gedocumenteerde limiet is 2.000 domeinen per aanroep.

domains=

Een GET-parameter hier. Het endpoint accepteert ook een POST-body, wat de betere keuze is zodra lijsten lang worden. Test de scheidingsteken met twee domeinen voordat je een volledige run uitvoert.

BATCH = 200

Onze gewoonte, geen limiet: 200 korte domeinen houden de URL ruim onder de veelvoorkomende proxy-limieten.

_credits

Aanwezig bij geauthenticeerde responsen: de kosten van de aanroep en wat er nog over is. Het afdrukken ervan per batch is het goedkoopste audittrail.

Ruwe body-kolom

Je parseert later, één keer, tegen een opgeslagen sample. Een schema-aanpassing aan onze kant kost je dan een query, geen volledige run.

Messing weegschaal die gewichten balanceert om precieze berekening te tonen

Een hypothetische run, met de rekenkunde

Dit is een voorbeeld, geen klantverhaal. Stel je een bureau voor dat 1.800 client- en prospectdomeinen bijhoudt in een Google Sheet en elke maandag de DR wil vernieuwen.

Bij 200 domeinen per batch zijn dat 9 verzoeken. Negen verzoeken à 1 credit zijn 9 credits, oftewel $0,0009 per run, en over 52 maandagen is het totaal 468 credits, wat afrondt tot minder dan vijf cent.

Kosten zijn niet de beperkende factor. Versheid wel. De index wordt ongeveer maandelijks herbouwd, dus wekelijkse runs tonen drie weken op vier dezelfde waarden, en een maandelijkse run vangt dezelfde beweging op voor ongeveer een kwart van de aanroepen.

Voer het dus maandelijks uit en houd het wekelijkse sheet aan voor menselijke bewerkingen. Details over waarom de score stil kan staan staan in de DR-referentie.

Dingen die fout kunnen gaan in dit script

SymptoomOorzaakOplossing
SystemExit: HTTP 402Saldo is nulLaad op via de accountpagina, voer opnieuw uit; voltooide batches zijn al in SQLite opgeslagen
SystemExit: HTTP 401Omgevingsvariabele leeg of typefout in sleutelPrint len(KEY) en controleer het prefix sk_
ok=false op batch 3Foute waarde in de lijst, zoals een volledige URLVerwijder protocol en pad voordat je verzendt, zie de foutentabel
Zelfde DR als vorige weekIndex is nog niet herbouwdVerwacht; vergelijk maandelijks
Lange URL afgewezenBatch te groot voor GETVerlaag BATCH of ga over op een POST-body

Batchgrootte, GET en POST voor lange lijsten

De limiet is 2.000 domeinen per aanroep, en het script verzendt een tiende daarvan. Elke aanroep kost dezelfde vaste credit, dus de batchgrootte is een kwestie van fouten en payload, niet van prijs.

Waarom 200 en niet 2.000

Een kleinere batch faalt goedkoop. Als een run halverwege stopt, is de staart die opnieuw moet worden uitgevoerd slechts één batch lang, niet de hele lijst lang.

Kortere lijsten houden ook elke opgeslagen body klein genoeg om met het oog te scannen als een getal verkeerd lijkt, en dat weegt zwaarder dan een paar bespaarde aanroepen.

Als je later de batchgrootte verhoogt, doe dat dan één keer, in één constante, en voer de test met twee domeinen opnieuw uit. Een groter getal is geen reden om de test over te slaan.

Plan de batch tegen de runlengte die je kunt verdragen, niet tegen de limiet. Een nightly job die klaar moet zijn voor een ochtendrapport kan kleinere, veiligere batches verdragen dan een eenmalige backfill.

GET voor korte lijsten, POST voor lange

De GET-vorm zet elk domein in de URL. Dat werkt tot een proxy of een logregel de query afkapt, vandaar dat de tabel hierboven zegt om eerst de batch te verlagen. Zodra lijsten groeien, verzend je dezelfde domains-waarde in een POST-body.

Test het scheidingsteken en de codering met twee domeinen voordat je een volledige run uitvoert. Een dwaalende komma komt naar voren in de eerste batch, en dat is goedkoper om daar te zien.

Een POST-body houdt de lijst ook buiten access logs die veel hosts standaard bijhouden. Dat is een klein privacyvoordeel voor klantenlijsten, en het kost één extra headerregel in het script.

Als je overschakelt naar POST, houd dan de GET-versie van het script aan als fallback voor korte lijsten. Twee codepaden klinken zwaar, maar een enkele kleine functie die er één kiest op basis van lengte is makkelijk te lezen en te testen.

Trim en dedupliceer voordat je het eerste verzoek doet

Verwijder spaties, maak de host lowercase en verwijder duplicaten voordat er iets je machine verlaat. Duplicaten nemen ruimte in onder de limiet van 2.000 domeinen, en ze zorgen ervoor dat het aantal in je sheet niet overeenkomt met het aantal in de respons.

Kies één regel voor www. en pas die toe in één functie, zodat dezelfde site nooit onder twee schrijfwijzen wordt opgezocht. Elke backlink-domeincontrole die schaalbaar is voor meer dan een paar honderd namen heeft die discipline nodig.

Wat de runlog moet zeggen

Print de _credits-waarde na elke batch, en schrijf het batchnummer, het aantal domeinen en de tijd ernaast. Die drie getallen beantwoorden bijna elke vraag die een financiële lezer heeft over een run.

Een run die niets afdrukt tot het einde geeft je geen idee wanneer het vastloopt. Een regel per batch is goedkoop, en het verandert een stille hang in een batchnummer dat je kunt noemen.

Houd de log in dezelfde map als het SQLite-bestand, zodat één back-up beide kopieert. Een log die ergens anders staat is degene die kwijt raakt tijdens een incident.

Voltooide houten verbinding apart van ruw houtstaand

Drie extensies die de middag waard zijn

Sla het batchnummer op naast de timestamp. Het staat al in de tabel hierboven, en het stelt je in staat om alleen de batch opnieuw uit te voeren die faalde na een 402 halverwege de run.

Schrijf een kleine view over de ruwe tabel zodra je een echte respons hebt geïnspecteerd: domein, score, opgehaald-op. Dashboards lezen de view, en de ruwe kolom blijft als bewijs als een getal ooit in twijfel wordt getrokken.

Voer het uit vanuit GitHub Actions of een gewone cron op een kleine VPS. Het maakt niet uit. Wat wel telt is dat de job een non-zero exit geeft bij elke fout, omdat een stille fout er drie weken later uitziet als een verouderde grafiek.

De uitvoer sorteren is een front-endkwestie. Rangschik de portfolio op score, groepeer het, of vergelijk het met dr_distribution (1 credit) om te zien waar elke client staat binnen alle 181.132.097 domeinen.

Veldverschuiving en de ruwe tabel

Een langlopende job faalt stil wanneer een veld van vorm verandert. De ruwe kolom is er zodat die fout luid en goedkoop op te lossen blijft.

Sla de ruwe body eerst op

De ruwe kolom is het vangnet. Als een veld hernoemd wordt of er een nieuw bij komt, voer je een query uit over opgeslagen responsen in plaats van de hele lijst opnieuw te betalen. Later parseren is goedkoop, opnieuw kopen niet.

Houd ook één opgeslagen sample per week in een apart bestand bij. Als een parse faalt, diff je dit weeksample tegen het laatste dat werkte.

Schrijf de body exact zoals ontvangen, zonder pretty-printing en zonder trimmen. Elke wijziging die je aanbrengt in de tekst voordat je hem opslaat, is een wijziging die je later niet ongedaan kunt maken.

Hoe drift er in de praktijk uitziet

Stel je voor dat een dashboard plotseling lege scores toont voor één batch. De ruwe rij is nog steeds aanwezig. Je opent hem, vergelijkt de toetsen met het sample van vorige week en vindt het verschil in enkele minuten.

De oplossing is dan één regel in de view, geen volledige run van de lijst. Dat is het hele punt van het opslaan van de body.

Houd een korte notitie naast de view over welk veld deze leest en wanneer je het voor het laatst hebt gecontroleerd tegen een echte respons. De notitie kost een minuut om te schrijven en bespaart later een lange zoektocht.

Orde de migraties met de data

Maak de ruwe tabel aan voordat de view, en de view voordat elke dashboardquery. Houd de create-statements in één bestand, voer ze in volgorde uit en registreer het batchnummer bij elke rij.

Gescheiden databases per client verschuiven op dezelfde manier als één gedeelde database. De regel blijft hetzelfde: één opgeslagen body per aanroep, en één plek om het te parseren.

Houd de parser naast de tests

Zet de parse-functie in dezelfde repository als een test die één opgeslagen body leest. De test is de plek waar een verschoven veld voor het eerst naar voren komt, lang voordat een grafiek dat doet.

Noem de velden die de parser verwacht in één constante bovenaan het bestand. Een reviewer kan dan elke afhankelijkheid van de response-vorm op één scherm zien.

Eerste run, in volgorde

  1. 1

    Test één domein

    Roep bulk_dr aan met één domein dat je kent, en lees de volledige respons. Noteer de toetsen voordat je enige parsercode schrijft.

  2. 2

    Voer twee batches uit

    Geef het script een lijst van 400 domeinen, zodat precies twee batches draaien. Controleer of de _credits-daling overeenkomt met twee aanroepen van elk één credit.

  3. 3

    Tel de opgeslagen rijen

    Open het SQLite-bestand en vergelijk het aantal rijen met het aantal batches. Een mismatch betekent dat een batch is overgeslagen of dubbel is opgeslagen, en dat ontdek je voordat de dashboard dat doet.

  4. 4

    Plan de volledige lijst

    Voeg pas de volledige lijst en de scheduler toe nadat de testruns schoon zijn. Houd de non-zero exit aan, zodat een fout de eerste nacht luid is.

Voordat je het plant

  • Dedupliceer en maak de lijst lowercase eerst
  • Bespreek één keer, in één functie, of www. blijft of gaat
  • Houd de sleutel in een secret store, niet in crontab
  • Alert op een non-zero exit code
  • Voer de credit budget getallen uit voor je eigen volume