Jak korzystać z bota / scrapera
Ten dokument tłumaczy, jak na co dzień korzystać z bota (scrapera) i z panelu
statystyk, który go monitoruje — bez zaglądania w kod. Techniczny opis dla
programisty (struktura repo, wybór technologii) jest w README.md w repo;
tutaj chodzi o obsługę: jak sprawdzić, czy scraper działa, jak nim sterować
i jak czytać to, co pokazuje dashboard.
Czym jest ten system
- Scraper (bot) — program w Pythonie, który odwiedza strony i zbiera z
nich dane. NIE działa w tle jako usługa: uruchamiasz go ręcznie
(
./.venv/bin/python scraper_example.py) albo zlecasz przezPOST /api/scrape— wtedy wykonuje go proces panelu. - Panel statystyk (ten dashboard) — serwer HTTP, który czyta to, co
scraper zapisuje o swojej pracy, i pokazuje to na wykresie i w tabelach.
To on działa jako usługa
systemd --usero nazwiescraper-stats.
Oba procesy dzielą jedną bazę SQLite (data/stats.db): scraper zapisuje
zdarzenia (start przebiegu, postęp, błędy, koniec), panel je odczytuje.
Sam serwer nasłuchuje tylko w sieci netbird i bezpośrednio nie jest widoczny
ani z internetu, ani z LAN-u. Ale publiczny adres do podawania komukolwiek
istnieje — to https://scrap-1.syspir.pl (WAF, peer netbirda, przekazuje
ruch do serwera po tej samej sieci). Panel (ten dokument, dashboard,
/api-keys, /docs) nie wymaga żadnego logowania — dostęp do niego pilnuje
sieć/WAF, nie ta aplikacja. Ten dokument używa dalej adresu WAF-a we
wszystkich przykładach.
Wymóg dla tego adresu: WAF przepuszcza dalej tylko żądania z nagłówkiem
X-Scrap-Key: <sekret> — bez niego żądanie w ogóle nie dotrze do serwera.
W przykładach niżej i w /przyklady ten nagłówek jest tylko
placeholderem: realną wartość weź z .env (SCRAP_WAF_KEY) na serwerze albo
od administratora WAF-a — traktuj ją jako tajną, tak samo jak klucz API
z /api-keys. Z sieci netbird bezpośrednio (surowe IP zamiast WAF-a) ten
nagłówek nie jest wymagany.
Sprawdzanie, czy scraper działa
Najszybciej: otwórz dashboard i spójrz na kropkę w prawym górnym rogu.
| Stan | Kropka | Co oznacza |
|---|---|---|
scrapuje |
zielona, pulsująca | scraper właśnie pracuje |
zakończony |
niebieska | ostatni przebieg skończył się sukcesem — i tak zostaje, choćby minęły godziny (przerwa między przebiegami to normalny stan, nie awaria) |
błąd |
czerwona | ostatni przebieg zakończył się błędem albo właśnie wystąpił błąd w trakcie pracy |
offline |
pomarańczowa | przebieg umilkł w trakcie — od ostatniego zdarzenia minęło ponad 120 s, a przebieg nie zapisał zakończenia. To sygnał realnej awarii (scraper padł, host stracił łączność, pisze do innej bazy) |
brak danych |
szara | baza jest pusta — scraper jeszcze nigdy nic nie zapisał |
nieznany |
szara | ostatnie zdarzenie ma nieczytelny znacznik czasu (rzadkie, patrz niżej) |
Pod kropką widać, ile czasu minęło od ostatniego zdarzenia (licznik tyka także między odświeżeniami), a pod nagłówkiem — treść ostatniego komunikatu, jeśli jakiś był. Najedź na kropkę, żeby zobaczyć dokładną godzinę ostatniego zdarzenia i identyfikator przebiegu. W kaflach powyżej: ile elementów zebrano, ile było błędów, ile przebiegów wykonano i jaka jest skuteczność (w oknie 24 h, 7 dni i łącznie). Wykres „Historia” pokazuje to samo w czasie — przełączasz zakres przyciskami 24 h / 7 dni / 30 dni.
Tabela „Przebiegi” zwija każdy przebieg do jednego wiersza: kiedy się zaczął, kto go zlecił, jakie adresy odwiedził, co odpowiedziały, ile ważyła pobrana treść, ile to trwało i jak się skończył. Kliknięcie wiersza (albo trójkącika z lewej, także Enterem lub spacją) rozwija go na wiersze poszczególnych celów — jeden na adres, z własnym kodem HTTP, rozmiarem i czasem. Rozwinięcie przeżywa automatyczne odświeżanie co 10 sekund.
Co jest w kolumnach:
- Klucz API — nazwa klucza, którym zlecono przebieg przez
POST /api/scrape, z numerem#idw podpowiedzi (ten sam numer widać przy nazwie na liście/api-keys; po regeneracji starsze przebiegi wskazują poprzedni klucz z dopiskiem „unieważniony”).ręcznieoznacza przebieg uruchomiony na serwerze (scraper_example.py), a–— przebieg z API sprzed wprowadzenia tej kolumny. - URL — pierwszy cel, a przy wielu celach dopisek
+N. Pełna lista jest w rozwinięciu. - Status — kody HTTP zebrane zbiorczo, np.
200 ×2, 404.–znaczy, że kodu nie ma: cel odrzuciła ochrona SSRF, host się nie rozwiązał, minął limit czasu albo przebieg pochodzi sprzed wprowadzenia metryk. To celowe — patrz „Bezpieczeństwo i dostęp”. - Rozmiar — suma pobranych stron (1 kB = 1000 bajtów). Mierzy treść
<body>po wykonaniu skryptów, czyli dokładnie to, co wraca w polubodyodpowiedzi API — nie rozmiar transferu, więc nie porównuj go z tym, co pokazuje zakładka „Sieć” w przeglądarce. - Czas — czas całego przebiegu. Jest większy niż suma czasów celów: start i zamknięcie przeglądarki nie należą do żadnego celu.
Wynik to nie to samo co kod HTTP. Cel z kodem 400 lub wyższym liczy się
jako błąd — jego treść i tak jest zwracana przez API, ale statystyki mają
pokazywać, że serwer odpowiedział błędem. Bywa też odwrotnie: „błąd” z kodem
200 znaczy, że serwer odpowiedział poprawnie, ale scraper nie zdążył odczytać
treści (np. limit czasu na doładowanie strony).
Wynik niedomknięty (pomarańczowy wiersz) oznacza przebieg, który nie zapisał
zakończenia — został ubity twardo, przez brak pamięci albo restart usługi
w trakcie pracy. Takie przebiegi obniżają skuteczność, więc warto na nie
patrzeć.
Tabela „Ostatnie zdarzenia” pod spodem to surowy log: co, kiedy i z jakim komunikatem scraper zapisał. Przyciskami nad nią zawężasz widok do samych błędów albo do startów i zakończeń przebiegów — przydaje się, gdy jeden przebieg zapisał kilkadziesiąt zdarzeń i wypchnął starszy błąd z listy. Przycisk „Pokaż więcej” pod tabelą wydłuża listę (25 → 50 → 100 → 200); gdy napis zmieni się na „To wszystkie zdarzenia”, dalej nic już nie ma. Długie komunikaty są ucięte — pełną treść pokazuje podpowiedź po najechaniu.
Na wykresie pierwszy i ostatni słupek zwykle pokazują wycinek swojej godziny (albo doby): pierwszy zaczyna się przed początkiem zakresu, ostatni dopiero się zbiera. Podpowiedź po najechaniu mówi wtedy „kubełek niepełny” — to nie spadek wydajności scrapera.
Dashboard sam odświeża się co 10 sekund — nie trzeba nic klikać. Gdy połączenie z serwerem padnie, liczby przygaszają się i pojawia się „błąd połączenia”, żeby nie czytać starych danych jako bieżących.
Sterowanie usługą panelu
Usługa scraper-stats to panel statystyk i API (dashboard, /api/*,
/healthz) — nie sam scraper. Jej zatrzymanie gasi dashboard i wszystkie
endpointy (monitoring dostanie „connection refused”), ale nie zatrzymuje
żadnego bota, bo scraper nie działa w tle. Zainstalowana jako
systemd --user na koncie bot:
systemctl --user status scraper-stats # czy działa
systemctl --user restart scraper-stats # restart, np. po zmianie kodu
systemctl --user stop scraper-stats # zatrzymanie
journalctl --user -u scraper-stats -f # logi na żywo (Ctrl+C, żeby wyjść)
Usługa ma ustawione Restart=always — jeśli proces panelu się wywali,
systemd sam go podniesie po 5 sekundach. Nie trzeba tego robić ręcznie.
Uwaga: dopóki
lingerdla kontabotnie jest włączony (wymaga jednorazowej komendy roota:sudo loginctl enable-linger bot), usługa po restarcie serwera nie wstanie sama, dopóki ktoś się nie zaloguje na kontobot.
Żeby uruchomić scraper ręcznie (np. do testów, z podglądem na żywo w terminalu) — usługi NIE trzeba zatrzymywać, to dwa niezależne procesy piszące i czytające tę samą bazę:
cd /home/bot/app-bot && ./.venv/bin/python scraper_example.py
Usługę zatrzymuje się tylko wtedy, gdy chcesz uruchomić sam panel ręcznie
z terminala — wtedy trzeba zwolnić port (systemctl --user stop
scraper-stats, potem ./.venv/bin/python run.py).
Ręczny przebieg domyka się także po Ctrl+C i po kill (SIGTERM): scraper
zapisuje wtedy zakończenie przebiegu, więc nie zostaje w statystykach jako
„niedomknięty”.
Kopia zapasowa bazy
Baza (data/stats.db) trzyma nie tylko statystyki, ale też klucze API — i to
w formie, z której nie da się odtworzyć samych kluczy. Dlatego raz na dobę
powstaje jej kopia w data/backups/ (zostają kopie z ostatnich 14 dni):
systemctl --user list-timers stats-backup.timer # kiedy następna kopia
systemctl --user start stats-backup.service # kopia teraz, bez czekania
journalctl --user -u stats-backup -n 20 # co się zapisało
Kopia leży na tym samym dysku co baza — ratuje przed uszkodzeniem pliku albo przypadkowym usunięciem danych, ale nie przed awarią dysku. Jeśli kopia ma trafiać poza ten host, trzeba najpierw ustalić gdzie.
Usuwanie starych zdarzeń (retencja) jest wyłączone i uruchamiane tylko ręcznie — to operacja nieodwracalna. Szczegóły w README.
Jak scraper zgłasza statystyki
To dotyczy tylko modyfikowania/pisania własnego scrapera — do samego korzystania z dashboardu ta sekcja nie jest potrzebna.
Scraper zgłasza swoją pracę przez małą bibliotekę stats_writer.py, która
zapisuje bezpośrednio do bazy SQLite (bez żadnego wywołania sieciowego):
from stats_writer import StatsWriter
stats = StatsWriter() # domyślnie data/stats.db
with stats.run(target="allegro") as run: # zgłasza run_start / run_end
run.progress(queue_size=128, target="allegro/strona-2")
run.items(10) # +10 zebranych elementów
run.error("TimeoutError: nawigacja > 30 s")
Wyjątek rzucony wewnątrz bloku with jest automatycznie zapisany jako
run_end ze statusem error — nie trzeba go łapać ręcznie.
Ważne: run.items(n) i run.error(...) to przyrosty od ostatniego
zgłoszenia, nie liczniki skumulowane. Zgłoszenie tej samej sztuki dwa razy
zawyży statystyki na dashboardzie.
Oba wywołania przyjmują dodatkowo metryki jednego celu — http_status,
body_bytes i duration_ms (run.items(1, target=url, http_status=200,
body_bytes=51234, duration_ms=1870)). To nie są przyrosty: podaj je raz,
w tym samym wywołaniu, które zgłasza dany cel, a pojawią się w rozwinięciu
tabeli „Przebiegi”.
Pełny, działający przykład (Patchright + raportowanie) jest w
scraper_example.py w repo.
Przykładowe skrypty: pobranie HTML i zdjęć
Dwa gotowe skrypty do szybkiego pobrania HTML-a strony albo serii zdjęć z
artykułu (bez raportowania do dashboardu) — z instrukcją użycia pod
/przyklady.
Zarządzanie kluczami API
Otwórz /api-keys. Lista pokazuje tylko aktywne klucze —
unieważniony znika z niej od razu (nadal istnieje w bazie, po prostu nie ma
go po co pokazywać w panelu do zarządzania), ale jego nazwa zostaje widoczna
przy jego dawnych przebiegach w tabeli „Przebiegi”. Przy każdej nazwie jest
numer #id — ten sam, który pokazuje kolumna „Klucz API”, bo nazwy nie są
unikalne (regeneracja tworzy nowy klucz o tej samej nazwie). Tam:
- Tworzysz klucz — podaj nazwę (np. „monitoring”) i kliknij „Utwórz klucz”. Pełna wartość pokazuje się tylko raz, zaraz po utworzeniu — skopiuj ją od razu, drugi raz jej nie zobaczysz. Lista poniżej pokazuje już tylko zamaskowany prefiks.
- Unieważniasz klucz — przyciskiem „Unieważnij” przy danym wierszu.
Działa natychmiast; klient używający tego klucza dostanie
401przy następnym żądaniu. - Regenerujesz klucz — przyciskiem „Regeneruj” przy danym wierszu. Unieważnia stary klucz i od razu tworzy nowy o tej samej nazwie; nowa pełna wartość znów pokazuje się tylko raz, tak jak przy tworzeniu. Stary klucz przestaje działać natychmiast — zaktualizuj klienta nowym kluczem, zanim go użyjesz.
Bez ani jednego aktywnego klucza całe /api/* odpowiada 503.
Korzystanie z API
Dashboard w przeglądarce jest otwarty bez logowania (patrz „Czym jest ten
system” wyżej). Do automatycznego pobierania danych (skrypty, inne
narzędzia) służy osobne API z kluczem utworzonym w /api-keys (patrz
wyżej):
curl -H "Authorization: Bearer <klucz z /api-keys>" \
-H "X-Scrap-Key: <sekret WAF-a (SCRAP_WAF_KEY z .env)>" \
https://scrap-1.syspir.pl/api/stats?range=7d
To samo w Pythonie (pip install requests — to zależność klienta, nie
tego projektu):
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
resp = requests.get(
"https://scrap-1.syspir.pl/api/stats",
params={"range": "7d"},
headers={
"Authorization": f"Bearer {API_KEY}",
"X-Scrap-Key": WAF_KEY,
},
timeout=10,
)
resp.raise_for_status()
print(resp.json())
| Endpoint | Co zwraca |
|---|---|
GET /api/status |
stan na żywo: running, success, error, offline, no_data |
GET /api/stats?range=24h\|7d\|30d |
status + liczniki + historia + przebiegi + ostatnie zdarzenia |
GET /api/history?range=24h\|7d\|30d |
sam szereg czasowy do wykresu |
GET /api/runs?limit=20 |
ostatnie przebiegi, każdy jako jeden wiersz (czas trwania, elementy, błędy, wynik, klucz API i lista celów) |
POST /api/scrape |
wyzwala scrapowanie podanych celów na żądanie — patrz niżej |
GET /api/stats przyjmuje dodatkowo events=all|errors|runs (zawężenie
tabeli zdarzeń) i limit=1..200 (jej długość) — oba opcjonalne, domyślnie
all i 25. GET /api/runs zwraca dla każdego przebiegu status:
success, error, running albo abandoned (przebieg bez zapisanego
zakończenia — ubity twardo lub przerwany).
Każdy przebieg niesie też to, co widać w tabeli „Przebiegi”: api_key_id,
api_key_name i api_key_revoked (kto zlecił), duration_ms (czas całości),
body_bytes_total (suma rozmiarów) oraz targets[] — listę celów z polami
target, status, http_status, body_bytes, duration_ms i error.
Lista jest ucięta do 50 celów: targets_total mówi, ile ich było naprawdę,
a targets_truncated sygnalizuje ucięcie.
Klucz API jest wymagany tylko przez /api/* — dashboard w przeglądarce go
nie potrzebuje (nie wymaga żadnego logowania). X-Scrap-Key to osobny
mechanizm, sprawdzany przez WAF zanim ruch w ogóle dotrze do tej aplikacji —
potrzebny tylko, gdy łączysz się przez https://scrap-1.syspir.pl; z sieci
netbird bezpośrednio nie jest wymagany.
Jak zlecić scrapowanie na żądanie
POST /api/scrape każe scraperowi realnie odwiedzić podane adresy — i
czeka, aż skończy, zwracając wynik w tej samej odpowiedzi (nie trzeba nic
odpytywać osobno):
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"]}'
To samo w Pythonie:
import requests
API_KEY = "<klucz z /api-keys>"
WAF_KEY = "<sekret WAF-a (SCRAP_WAF_KEY z .env)>"
resp = requests.post(
"https://scrap-1.syspir.pl/api/scrape",
json={"targets": ["https://example.com", "https://example.org"]},
headers={
"Authorization": f"Bearer {API_KEY}",
"X-Scrap-Key": WAF_KEY,
},
timeout=200, # żądanie czeka na wynik — patrz ograniczenia niżej
)
resp.raise_for_status()
print(resp.json())
Przykładowa odpowiedź:
{
"run_id": "a1b2c3d4e5f6",
"status": "error",
"items_scraped": 1,
"errors": 1,
"results": [
{
"target": "https://example.com/",
"status": "ok",
"title": "Example Domain",
"head": {
"title": "Example Domain",
"lang": "en",
"meta": {"viewport": "width=device-width, initial-scale=1"},
"canonical": null
},
"body": "<body>...</body>",
"error": null,
"http_status": 200,
"body_bytes": 1256,
"duration_ms": 1843,
"load_complete": true
},
{
"target": "https://example.org/brak",
"status": "error",
"title": "404 Not Found",
"head": {"title": "404 Not Found", "lang": null, "meta": {}, "canonical": null},
"body": "<body>...</body>",
"error": "HTTP 404",
"http_status": 404,
"body_bytes": 310,
"duration_ms": 622,
"load_complete": true
}
]
}
head to wyciągnięte metadane strony (tytuł, meta tagi — w tym Open Graph,
bo łapane po name i property, canonical, lang), nie surowy HTML
znacznika <head>. body to za to surowy outerHTML całego <body> —
przy dużych stronach potrafi to być sporo danych w jednej odpowiedzi.
Do tego każdy cel ma http_status (kod odpowiedzi), body_bytes (rozmiar
body w bajtach) i duration_ms (czas pobrania). null znaczy „nieznane”:
brak odpowiedzi, odrzucenie przez ochronę adresów albo cel pominięty z braku
czasu.
Pole load_complete mówi, czy strona wczytała się do końca (zdarzenie
load). Scraper czeka na nie najwyżej ok. 10 s od chwili, gdy dokument jest
już gotowy. Gdy jakiś dodatek strony (obrazek, ramka, skrypt) nie skończy się
w tym czasie, dostajesz treść mimo to — "status": "ok", wypełnione title,
head i body — oraz "load_complete": false: to, co strona dopisuje
skryptami dopiero na samym końcu, może wtedy nie być w body. null znaczy,
że treści nie ma. Flaga dotyczy strony, z której pochodzi treść: jeśli strona
sama przeszła pod inny adres (np. przekierowała skryptem), mówi o stronie
docelowej, a nie o adresie, który podałeś.
Ważne ograniczenia:
- Żądanie czeka — przy wielu celach odpowiedź może przyjść dopiero po dziesiątkach sekund albo (w skrajnym przypadku) do ok. 3 minut.
- Maksymalnie 10 celów na jedno żądanie, najwyżej 30 s na cel.
- Tylko jedno scrapowanie naraz — drugie, równoległe żądanie dostaje
429 Too Many Requests; spróbuj ponownie za chwilę. - Cel, który nie jest publicznie dostępnym adresem
http/https(np. wskazuje na sam serwer, sieć lokalną albo jest nieczytelny), dostaje wpis"status": "error"w wyniku — reszta celów w żądaniu i tak się wykonuje. - Kod HTTP 400 lub wyższy to też błąd celu (
"status": "error","error": "HTTP 404") — aletitle,headibodysą wtedy nadal wypełnione, bo strona została pobrana. Jeśli chcesz treść stron błędów, sprawdzajhttp_status, a nie samstatus. - Ten przebieg pojawia się na dashboardzie i w
/api/statstak samo jak każdy inny (celapi-scrape).
Rozwiązywanie problemów
- Dashboard pokazuje „offline”, choć scraper właśnie działał — sprawdź,
czy nie minęło więcej niż 120 s od ostatniego zdarzenia (
HEARTBEAT_TIMEOUT_SECONDSw.env) i czy scraper faktycznie pisze do tego samego pliku bazy, co czyta serwer (DB_PATHw.envserwera vs.STATS_DB_PATH/DB_PATHw środowisku scrapera). - „Brak danych” mimo że scraper działa od dawna — zwykle literówka w
ścieżce do bazy: scraper pisze do jednego pliku, serwer czyta z innego.
Przy starcie serwer ostrzega w logu (
journalctl --user -u scraper-stats), jeśli plik bazy w ogóle nie istnieje. /healthzzwraca 503 — baza jest uszkodzona albo niedostępna (np. zły plik podDB_PATH, brak uprawnień do katalogu). Serwer mimo to wstaje i odpowiada, więc widać to w sondzie zamiast dostawać „connection refused” i cichą pętlę restartów. Szczegóły są w logu serwera.- 401 w API — klucz nieprawidłowy albo unieważniony; sprawdź listę w
/api-keysi w razie potrzeby utwórz nowy. - 503 „Brak aktywnych kluczy API” — nikt jeszcze nie utworzył żadnego
klucza (albo wszystkie zostały unieważnione) — utwórz jeden w
/api-keys. - 429 na
POST /api/scrape— inne scrapowanie już trwa (tylko jedno naraz); spróbuj ponownie za chwilę. - Panel (dashboard/
/pomoc//api-keys) nie odpowiada spodhttps://scrap-1.syspir.pl— sprawdź, czy dodałeś nagłówekX-Scrap-Key; bez niego WAF w ogóle nie przepuści żądania (to nie jest błąd tej aplikacji, więc nie da się tego zdiagnozować z jej logów).
Bezpieczeństwo i dostęp
- Serwer nasłuchuje wyłącznie na adresie netbirda
(
100.103.2.116:8080) — sam proces nie jest wystawiony do LAN-u ani do internetu. Publicznie dostępny jest przez WAF podhttps://scrap-1.syspir.pl, który jako peer netbirda przekazuje ruch do serwera po tej samej sieci. - Panel (dashboard, ta strona,
/przyklady,/api-keys,/docs) nie wymaga logowania na poziomie aplikacji — świadoma decyzja: dostępu pilnuje sieć (netbird) i WAF (X-Scrap-Key), więc osobny login byłby trzecią, redundantną warstwą tego samego. Kto ma dostęp do netbirda albo znaX-Scrap-Key, widzi cały panel, łącznie z tworzeniem/unieważnianiem kluczy API w/api-keys. - Klucze API (Bearer, dla
/api/*) to osobny mechanizm — w bazie SQLite jako hash SHA-256 (nie plaintext), pełna wartość istnieje tylko raz, w chwili utworzenia, w odpowiedzi/api-keys. /healthznie wymaga logowania (tak, żeby monitoring mógł go odpytywać bez sekretów), ale nie ujawnia nic pozaok/error— szczegóły trafiają tylko do logu serwera.- Cel odrzucony przez ochronę SSRF pokazuje ogólny komunikat („adres celu
jest niedozwolony (sieć wewnętrzna)”), bez adresu IP. Ten sam tekst trafia do
tabeli zdarzeń i do
/api/runs, które widać bez logowania, więc konkretny adres zostaje wyłącznie w logu usługi — inaczej każdy z kluczem API mógłby tą drogą odpytywać wewnętrzny DNS.
Pełna dokumentacja techniczna (struktura repo, schemat bazy, uzasadnienie
wyboru technologii) jest w pliku README.md w katalogu projektu.