Jak korzystać z bota

instrukcja obsługi scrapera i panelu statystyk
← Pulpit

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

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:

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 linger dla konta bot nie 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 konto bot.

Ż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:

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:

Rozwiązywanie problemów

Bezpieczeństwo i dostęp


Pełna dokumentacja techniczna (struktura repo, schemat bazy, uzasadnienie wyboru technologii) jest w pliku README.md w katalogu projektu.