Przykłady: API przez HTTPS i skrypty lokalne
Ten dokument (do pobrania w całości jako plik .md — przycisk niżej na
stronie /przyklady) zbiera gotowy, kopiuj-wklej kod:
- komplet przykładów API przez HTTPS (
curli Python) na każdy endpoint/api/*, korzystających z publicznego adresu WAF-ahttps://scrap-1.syspir.pl— to jedyny właściwy sposób łączenia się spoza sieci netbird; - dwa lokalne skrypty (Patchright) do szybkiego pobrania HTML-a albo
zdjęć z dowolnej strony — nie raportują do dashboardu, niezależne od
scraper_example.py.
Korzystanie z API przez HTTPS (WAF)
Wszystkie przykłady niżej łączą się przez publiczny adres WAF-a —
https://scrap-1.syspir.pl — bo to jedyny właściwy sposób dostępu spoza
sieci netbird (surowego IP netbirda nie podawaj nikomu spoza tej sieci).
Każde żądanie do /api/* wymaga dwóch nagłówków naraz:
| Nagłówek | Skąd wziąć | Sprawdza |
|---|---|---|
Authorization: Bearer <klucz> |
/api-keys |
ta aplikacja (FastAPI) |
X-Scrap-Key: <sekret> |
SCRAP_WAF_KEY z .env na serwerze (albo od administratora WAF-a) |
sam WAF, zanim ruch dotrze do serwera |
Brak X-Scrap-Key → żądanie nie dotrze do aplikacji (błąd po stronie
WAF-a). Brak/zły Authorization → aplikacja odpowie 401. W przykładach
niżej X-Scrap-Key jest tylko placeholderem — realną wartość weź z .env
(SCRAP_WAF_KEY) na serwerze albo od administratora WAF-a; nie wklejaj jej
do tego dokumentu ani do repozytorium. Poniższe przykłady
zakładają Pythona z zainstalowanym requests (pip install requests —
to zależność Twojego środowiska, nie tego projektu).
1. Stan na żywo — GET /api/status
curl -s https://scrap-1.syspir.pl/api/status \
-H "Authorization: Bearer <klucz z /api-keys>" \
-H "X-Scrap-Key: <sekret WAF-a (SCRAP_WAF_KEY z .env)>"
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "X-Scrap-Key": WAF_KEY}
resp = requests.get(
"https://scrap-1.syspir.pl/api/status", headers=HEADERS, timeout=10
)
resp.raise_for_status()
stan = resp.json()
print(stan["state"], "—", stan["seconds_since_last_event"], "s temu")
Pole state to jedno z: running, success, error, offline,
no_data, unknown — znaczenie każdego jest opisane w /pomoc.
2. Liczniki, historia i ostatnie zdarzenia — GET /api/stats
curl -s "https://scrap-1.syspir.pl/api/stats?range=7d" \
-H "Authorization: Bearer <klucz z /api-keys>" \
-H "X-Scrap-Key: <sekret WAF-a (SCRAP_WAF_KEY z .env)>" \
| python3 -m json.tool
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "X-Scrap-Key": WAF_KEY}
for zakres in ("24h", "7d", "30d"):
resp = requests.get(
"https://scrap-1.syspir.pl/api/stats",
params={"range": zakres},
headers=HEADERS,
timeout=10,
)
resp.raise_for_status()
dane = resp.json()
print(zakres, "-> punktów w historii:", len(dane["history"]))
# `range` w zapytaniu steruje TYLKO długością `history` (wykresu).
# `counters` wraca zawsze z tymi samymi trzema oknami, niezależnie od `range`:
print(dane["counters"]["last_24h"])
print(dane["counters"]["last_7d"])
print(dane["counters"]["all_time"])
3. Sam szereg czasowy do wykresu — GET /api/history
curl -s "https://scrap-1.syspir.pl/api/history?range=30d" \
-H "Authorization: Bearer <klucz z /api-keys>" \
-H "X-Scrap-Key: <sekret WAF-a (SCRAP_WAF_KEY z .env)>" \
-o historia_30d.json
import csv
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "X-Scrap-Key": WAF_KEY}
resp = requests.get(
"https://scrap-1.syspir.pl/api/history",
params={"range": "30d"},
headers=HEADERS,
timeout=10,
)
resp.raise_for_status()
seria = resp.json()["history"] # lista {"bucket": ts, "items": int, "errors": int}
with open("historia_30d.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["bucket", "items", "errors"])
writer.writeheader()
writer.writerows(seria)
4. Zlecenie scrapowania na żądanie — POST /api/scrape
curl -X POST https://scrap-1.syspir.pl/api/scrape \
-H "Authorization: Bearer <klucz z /api-keys>" \
-H "X-Scrap-Key: <sekret WAF-a (SCRAP_WAF_KEY z .env)>" \
-H "Content-Type: application/json" \
-d '{"targets": ["https://example.com", "https://example.org"]}'
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "X-Scrap-Key": WAF_KEY}
resp = requests.post(
"https://scrap-1.syspir.pl/api/scrape",
json={"targets": ["https://example.com", "https://example.org"]},
headers=HEADERS,
timeout=200, # żądanie CZEKA na wynik — do ok. 3 minut przy wielu celach
)
resp.raise_for_status()
wynik = resp.json()
for r in wynik["results"]:
# `http_status`, `body_bytes` i `duration_ms` ma KAŻDY wynik (None = nieznane)
print(r["target"], "->", r["http_status"], r["body_bytes"], "B", r["duration_ms"], "ms")
if r["status"] != "ok":
print(" BŁĄD:", r["error"])
# Uwaga: kod >= 400 to też błąd, ale treść jest wtedy pobrana —
# `title`, `head` i `body` są wypełnione, więc da się ich użyć.
if r["http_status"] is None:
continue
print(" tytuł:", r["title"])
print(" meta:", r["head"]["meta"]) # np. opis, tagi Open Graph
print(" canonical:", r["head"]["canonical"])
print(" body:", len(r["body"]), "znaków HTML")
Każdy udany cel niesie oprócz title też head (wyciągnięte metadane:
title, lang, meta — w tym Open Graph, bo łapane po name i
property, oraz canonical) i body (surowy outerHTML całego
<body> — potrafi być spory przy dużych stronach). Pełny opis kształtu
odpowiedzi jest w /pomoc.
Niezależnie od wyniku każdy cel ma http_status (kod odpowiedzi strony),
body_bytes (rozmiar body w bajtach) i duration_ms (czas pobrania) —
te same liczby widać w rozwinięciu tabeli „Przebiegi” w panelu. null
znaczy „nieznane”: brak odpowiedzi, odrzucenie przez ochronę adresów albo
cel pominięty z braku czasu. Pole load_complete (true/false/null)
mówi, czy strona doczekała się końca wczytywania — false to wciąż udany
cel z treścią, tylko bez dodatków, które nie zdążyły w ok. 10 s (zob.
/pomoc).
Cel potrafi zostać odrzucony także wtedy, gdy sama strona wczytała się poprawnie: jeśli któryś jej podzasób (obrazek, skrypt, XHR) został pobrany z adresu w sieci wewnętrznej — zwykle przez przekierowanie — cel kończy się błędem i nie zwraca treści. Komunikat jest wtedy celowo ogólny, bez adresu.
Ograniczenia (pełny opis w /pomoc): maksymalnie 10 celów na
żądanie, tylko jedno scrapowanie naraz (drugie równoległe dostaje 429),
odpowiedź przychodzi dopiero po zakończeniu całego przebiegu.
Skrypty lokalne: pobranie HTML-a i zdjęć
Poniższe dwa skrypty uruchamia się bezpośrednio na serwerze (nie przez HTTP) — do szybkiego podejrzenia, co widzi scraper na danej stronie.
Pobranie pełnego HTML-a strony
./.venv/bin/python example_fetch_html.py https://www.twoje-miasto.pl > strona.html
Domyślny URL (bez argumentu) to https://www.twoje-miasto.pl. HTML — po
wykonaniu JS, dokładnie taki, jaki widzi przeglądarka — trafia na stdout;
> strona.html zapisuje go do pliku.
Pobranie serii zdjęć z artykułu
./.venv/bin/python example_fetch_photos.py https://www.twoje-miasto.pl/art-.../tytul-i123456
Zapisuje zdjęcia do katalogu ./photos/ (można zmienić przez --out-dir
DIR). Szuka zdjęć w pełnym rozmiarze przez link lightboxa (<a data-lity
href="...">), którego używa twoje-miasto.pl do powiększania zdjęć —
działa identycznie dla artykułu z jednym zdjęciem głównym i dla
prawdziwej wieloelementowej galerii.
Jak są „ucharakteryzowane” (żeby wyglądały jak realny użytkownik)
Oba skrypty korzystają z tej samej konfiguracji przeglądarki co
produkcyjny scraper (scraper_core.py):
| Element | Wartość |
|---|---|
| Przeglądarka | realny, systemowo zainstalowany Google Chrome (nie zbundlowany Chromium) |
| System (deklarowany) | Windows 11 |
| Rozdzielczość | 1920×1080 (najpopularniejsza w Polsce — StatCounter, 2026) |
| Język | polski (pl-PL), strefa czasowa Europe/Warsaw |
| Sesja | cookies/localStorage przetrwałe między uruchomieniami (data/browser_state.json) |
Znane ograniczenie: starsze API navigator.platform w przeglądarce i
tak zdradza, że fizyczny serwer działa na Linuksie, nie na Windows — to
architektoniczne ograniczenie samego Chromium, którego nie da się obejść
z poziomu tego stacku. W praktyce: myli to proste sprawdzenia oparte na
nagłówkach/User-Agencie, ale nie przetrwa dokładnego zestawienia z tym
starszym API. Pełne techniczne uzasadnienie tych wyborów (i pomiary
fingerprintu TLS/HTTP2) jest w README.md w repo.
Instrukcja obsługi całego systemu (dashboard, API, klucze) jest pod
/pomoc.