Bulk DR för en klientlista, som ett Python-jobb på 40 rader
Du har ett kalkylblad med domäner och en dashboard som behöver ett tal per rad. Ett verktyg för att kontrollera backlink-domäner för det jobbet är mest plumbing, och detta skript är plumbinget.

Skriptet
Det läser domains.txt, skickar 200 domäner per begäran, sparar varje rått svar i SQLite och skriver ut vad körningen kostade. Vi lagrar hela payloaden eftersom fältnamn bäst kopieras från ett verkligt svar, inte från ett blogginlägg.
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"))Vad varje del gör
action=bulk_dr
En kredit per anrop. Den dokumenterade gränsen är 2 000 domäner per anrop.
domains=
En GET-parameter här. Slutpunkten accepterar också en POST-body, vilket är det bättre valet när listorna blir långa. Testa avgränsaren med två domäner innan en full körning.
BATCH = 200
Vår vana, inte en gräns: 200 korta domäner håller URL:en väl under vanliga proxygränser.
_credits
Närvarande på autentiserade svar: kostnaden för anropet och vad som finns kvar. Att skriva ut det varje batch är den billigaste revisionsstigen.
Rå body-kolumn
Du parser senare, en gång, mot ett lagrat exempel. En schemaförändring på vår sida kostar dig då en fråga, inte en omkörning.

Ett hypotetiskt körningsexempel, med aritmetiken
Detta är ett exempel, inte en kundberättelse. Tänk dig ett byrå som spårar 1 800 klient- och prospectsdomäner i ett Google Sheet och vill ha DR uppdaterat varje måndag.
Med 200 domäner per batch blir det 9 anrop. Nio anrop till 1 kredit vardera är 9 kredit, eller $0,0009 per körning, och över 52 måndagar blir totalen 468 kredit, vilket avrundas till under fem cent.
Kostnad är inte begränsningen. Fräschhet är det. Indexet byggs om ungefär månatligen, så veckokörningar kommer att visa samma värden tre veckor av fyra, och en månatlig körning fångar samma rörelse för ungefär en fjärdedel av anropen.
Så kör det månatligen och behåll det veckovis sheetet för manuella redigeringar. Detaljer om varför poängen kan stå stilla finns i DR-referensen.
Saker som kan gå fel i detta skript
| Symtom | Orsak | Åtgärd |
|---|---|---|
SystemExit: HTTP 402 | Saldo är noll | Ladda upp från kontosidan, kör om; färdiga batchar finns redan i SQLite |
SystemExit: HTTP 401 | Miljövariabel tom eller felaktig nyckel | Skriv ut len(KEY) och kontrollera prefixet sk_ |
ok=false på batch 3 | Felaktigt värde i listan, t.ex. en fullständig URL | Ta bort schema och sökväg innan du skickar, se felmattan |
| Samma DR som förra veckan | Indexet har inte byggts om än | Förväntat; jämför månatligt |
| Lång URL avvisad | Batch för stor för GET | Sänk BATCH eller flytt till en POST-body |
Batchstorlek, GET och POST för långa listor
Takgränsen är 2 000 domäner per anrop, och skriptet skickar en tiondel av det. Varje anrop kostar samma fasta kredit, så batchstorlek är en fråga om fel och payload, inte om pris.
Varför 200 och inte 2 000
En mindre batch misslyckas billigt. Om en körning stannar mitt i, är svansen som ska köras om bara en batch lång, inte en hel lista lång.
Kortare listor håller också varje sparad body liten nog att skannas för ögat när ett tal ser fel ut, och det är viktigare än ett par sparade anrop.
Om du senare höjer batchstorleken, gör det en gång, i en konstant, och kör om tvådomänstestet. Ett större nummer är ingen anledning att hoppa över testet.
Planera batchen mot den körningslängd du kan tolerera, inte mot takgränsen. En nattlig jobb som måste vara klart innan en morgonrapport kan ha mindre, säkrare batchar än en engångsbackfill.
GET för korta listor, POST för långa
GET-formulären lägger varje domän i URL:en. Det fungerar tills en proxy eller en logglinje trunkerar frågan, vilket är varför tabellen ovan säger att sänka batchen först. När listorna växer, skicka samma domains-värde i en POST-body.
Testa avgränsaren och kodningen med två domäner innan en full körning. En vilkommande kommatecken syns i den första batchen, och det är billigare att se det där.
En POST-body håller också listan borta från åtkomstloggar som många värdar sparar som standard. Det är en liten vinst i integritet för klientlistor, och det kostar en extra header-rad i skriptet.
När du byter till POST, behåll GET-versionen av skriptet som en fallback för korta listor. Två kodvägar låter tunga, men en liten funktion som väljer en baserat på längd är lätt att läsa och testa.
Trimma och ta bort dubbletter innan första begäran
Ta bort mellanslag, gör värdnamnet till gemener och ta bort dubbletter innan något lämnar din maskin. Dubbletter tar upp utrymme under 2 000-domängränsen, och de får antalet i ditt sheet att stämma överens med antalet i svaret.
Välj en regel för www. och tillämpa den i en enda funktion, så att samma webbplats aldrig slås upp under två stavningar. Alla backlink-domänskontroller som skalar över några hundra namn behöver den disciplinen.
Vad körningsloggen bör säga
Skriv ut _credits-värdet efter varje batch, och skriv batchnumret, domänantalet och tiden bredvid det. De tre talen svarar på nästan alla frågor en finansiell läsare kan ha om en körning.
En körning som inte skriver ut något förrän i slutet ger dig ingen ledtråd när den fastnar. En rad per batch är billig, och den gör en tyst hängning till ett batchnummer du kan namnge.
Behåll loggen i samma mapp som SQLite-filen, så att en backup kopierar båda. En logg som finns någon annanstans är den som försvinner under en incident.

Tre tillägg som är värda eftermiddagen
Spara batchnumret bredvid tidsstämpeln. Det finns redan i tabellen ovan, och det låter dig köra om bara batchen som misslyckades efter en 402 mitt i körningen.
Skriv en liten vy över råtabellen när du har inspekterat ett verkligt svar: domän, poäng, hämtad-vid. Dashboards läser vyn, och råkolumnen finns kvar som ditt bevis om ett tal någonsin ifrågasätts.
Kör det från GitHub Actions eller en vanlig cron på en liten VPS. Båda är bra. Det viktiga är att jobbet avslutas med icke-noll vid något fel, eftersom ett tyst fel ser ut som en inaktuell graf tre veckor senare.
Att sortera utdata är en front-end-fråga. Rangordna portföljen efter poäng, bucketa den, eller jämför den med dr_distribution (1 kredit) för att se var varje klient sitter bland alla 181 132 097 domäner.
Fältförskjutning och råtabellen
Ett långkörande jobb misslyukas tyst när ett fält ändrar form. Råkolumnen finns där så att felet förblir högljutt och billigt att fixa.
Spara den rå body:n först
Råkolumnen är säkerhetsnätet. Om ett fält döps om eller ett nytt dyker upp, kör du en fråga över lagrade svar istället för att betala för hela listan igen. Att parsa senare är billigt, medan att köpa om är det inte.
Behåll ett sparat exempel per vecka i en egen fil också. När en parsing bryts, diff:a denna veckas exempel mot den sista som fungerade.
Skriv body:n exakt som den mottogs, utan vacker utskrift eller trimning. Any change you make to the text before storing it is a change you cannot undo later.
Vad en förskjutning ser ut i praktiken
Säg att en dashboard plötsligt visar tomma poäng för en batch. Den råda raden finns fortfarande kvar. Du öppnar den, jämför dess nycklar med förra veckans exempel, och hittar skillnaden på några minuter.
Åtgärden är då en enda rad i vyn, inte en omkörning av hela listan. Det är hela poängen med att lagra body:n.
Behåll en kort anteckning bredvid vyn som säger vilket fält den läser och när du senast kontrollerade den mot ett verkligt svar. Anteckningen tar en minut att skriva och sparar en lång sökning senare.
Ordna migrationerna med datan
Skapa råtabellen före vyn, och vyn före någon dashboard-fråga. Håll create-satserna i en fil, kör dem i ordning och registrera batchnumret med varje rad.
Separata databaser per klient förskjuts på samma sätt som en delad databas gör. Regeln förblir densamma: en lagrad body per anrop, och en plats att parsa den.
Håll parsaren bredvid testerna
Placera parse-funktionen i samma repository som ett test som läser en sparad body. Testet är den plats där ett förskjutet fält först visas, långt innan en graf gör det.
Namnge fälten som parsaren förväntar sig i en konstant nära toppen av filen. En granskare kan då se alla beroenden på response-formen på en enda skärm.
Första körningen, i ordning
- 1
Testa en domän
Ring
bulk_drmed en enda domän du känner till, och läs hela svaret. Skriv ner nycklarna innan du skriver någon parseringskod. - 2
Kör två batchar
Ge skriptet en lista på 400 domäner, så att exakt två batchar körs. Kontrollera att
_credits-minskningen matchar två anrop till en kredit vardera. - 3
Räkna de lagrade raderna
Öppna SQLite-filen och jämför radantalet med batchantalet. En mismatch betyder att en batch hoppades över eller sparades två gånger, och du hittar det innan dashboards gör det.
- 4
Schemalägg hela listan
Bara efter att testkörningarna är rena, lägg till hela listan och schemaläggaren. Behåll icke-noll-avslutningen, så att ett fel blir högljutt på första natten.
Innan du schemalägger det
- Ta bort dubbletter och gör listan till gemener först
- Bestäm en gång, i en funktion, om
www.ska finnas kvar eller inte - Behåll nyckeln i ett secret store, inte i crontab
- Varna vid ett icke-noll-avslutningskod
- Kör kreditbudgeten talen för din egen volym