Masowe sprawdzanie kodów odpowiedzi URL w Pythonie (skrypt napisany z AI)

Spis treści

Masz pytanie o swoją stronę?

Umów krótką konsultację – powiem, od czego zacząć.

Kody odpowiedzi tysięcy adresów URL sprawdzisz krótkim skryptem w Pythonie, który odczyta listę z pliku tekstowego i zapisze wyniki do CSV. Z każdego adresu zapisze kod pierwszej odpowiedzi (np. 301), finalny kod i adres po przekierowaniach oraz liczbę skoków. Poniżej znajdziesz przetestowany kod, instrukcję instalacji Pythona na Windows, macOS i Linux oraz historię tego, jak pierwszą wersję napisałem z pomocą ChatGPT.

Zawsze chciałem nauczyć się programować na tyle dobrze, abym mógł w ciągu jednego czy dwóch wieczorów stworzyć stronę internetową lub aplikację w wersji MVP, czyli Minimum Viable Product (minimalnej, działającej wersji produktu).

MVP, czyli Minimum Viable Product, to podstawowa wersja produktu, która zawiera tylko najważniejsze funkcje. Celem MVP jest przetestowanie pomysłu na rynku, zbieranie opinii użytkowników i weryfikacja założeń, zanim zainwestuje się w pełną wersję produktu. Dzięki temu można szybko wprowadzać zmiany i dostosować produkt do potrzeb klientów.

W zdecydowanej większości chodziło też o stworzenie narzędzi, które pomogą mi przy tworzeniu treści, analizowaniu stron internetowych i wielu innych zadaniach. Niezależnie od celu, programowanie zawsze było umiejętnością, którą chciałem opanować.

Nie to, że nigdy nie próbowałem. Swego czasu całkiem sporo kombinowałem w PHP. Było to o tyle naturalne, że w prosty sposób mogłem przygotowany kod HTML i CSS puścić w ruch. Nie były to jednak jakieś skomplikowane rzeczy. Z jakiegoś powodu programowanie obiektowe wykorzystujące klasy mnie przerastało. Lata mijały, a ja zamiast PHP zgłębiałem kolejne CMS-y, ustawienia wtyczek i tomy dokumentacji związanej z SEO. Miałem jeszcze krótką przygodę z językiem Ruby. Gdzieś na półce mam książki o tym języku i frameworku Ruby on Rails!

Chęć nauczenia się programowania nie malała, ale cały swój czas poświęcałem na SEO. Analizując kolejne dane, dokumentacje oraz narzędzia, zauważyłem, że do pracy z danymi, treściami i analizami coraz częściej wykorzystywany jest Python. Zacząłem więc coś w nim próbować, ale od początku wiedziałem, że przy moim braku czasu niewiele osiągnę.

Programowanie i pisanie kodu z AI

I pewnie ta historia nie miałaby happy endu, gdyby nie pojawienie się modeli językowych oraz rozwiązań takich jak ChatGPT czy Ollama. Nagle okazało się, że mając pomysł i rozbijając go na małe kroki, można otrzymać działający kod, który realizuje założenia MVP. Dziś to samo zrobisz z pomocą dowolnego asystenta AI, np. ChatGPT, Claude czy Gemini. Wygenerowany kod nadal trzeba jednak przeczytać i przetestować. Jak zobaczysz niżej, pierwsza wersja mojego skryptu miała błędy, których na pierwszy rzut oka nie widać.

Edytor VS Code z kodem skryptu w Pythonie sprawdzającego kody odpowiedzi URL i terminal z postępem analizy

A proste narzędzia? Przykładowo: potrzebujesz odpytać 20 tysięcy adresów URL o ich status. Chcesz wiedzieć, czy działają (200), mają przekierowania (301), a może zwracają jakiś błąd 4xx lub 5xx? Prosty skrypt w Pythonie, który uruchomisz na swoim komputerze, przygotujesz z pomocą AI w jeden wieczór, często szybciej.

Potrzebujesz stworzyć crawler strony, który nie będzie ograniczony do arkuszy Google? A może taki, który przeanalizuje stronę zbudowaną z wykorzystaniem dynamicznych rozwiązań jak Angular? Asystent AI przygotuje dla Ciebie punkt wyjścia, który potem dopracujesz i przetestujesz.

Jak przygotować zapytanie do AI o kod?

Pokażę Ci, jak ja to robię. Zawsze możesz dodać kolejne funkcje lub poprosić AI o rozbudowę albo zmianę swojej aplikacji. Pamiętaj też, że lepiej wiele razy poprosić o małe rzeczy, niż oczekiwać, że AI od razu zwróci Ci gotową aplikację składającą się z wielu plików.

Z mojego doświadczenia dobrze sprawdza się wskazanie w prompcie (zapytaniu do AI), co chcemy osiągnąć, jakie dane i w jaki sposób przekażemy oraz jakich danych i w jakiej formie oczekujemy. Aby to lepiej zobrazować, pokażę Ci krok po kroku, jak przygotowałem prosty skrypt w Pythonie, który pomógł mi przeanalizować ponad 20 tysięcy adresów URL w sklepie internetowym.

Założenia były proste. Miałem listę ponad 20 000 URL-i pobranych ze sklepu internetowego. Chciałem sprawdzić, który adres zwraca kod 200 (OK), który jest niedostępny i zwraca kod 404, a który jest przekierowany na inny (301). Sklep działał na platformie IdoSell (dawniej IAI-Shop), więc po pewnej liczbie zapytań mógł się pojawić kod 429, czyli Too Many Requests (za dużo żądań), a kolejne zapytania byłyby blokowane przez serwer. Moje IP zostało dodane do białej listy, dzięki czemu teoretycznie mogłem wysłać dowolną liczbę zapytań bez bycia „przyciętym”.

Oto jak wyglądała moja współpraca z ChatGPT

Najpierw musiałem zdefiniować swoje potrzeby, wymagania i oczekiwania. Sam zdecydowałem, że chcę korzystać z Pythona, bo, jak wspomniałem wcześniej, chcę się go nauczyć, a do tego zadania po prostu dobrze się nadaje.

Oto mój prompt z 2024 roku:

Przygotuj kod skryptu w python w taki sposób, aby realizował działania kolejno:

1. Pobierz adres z pliku urls.txt
2. Wybierz pierwsze, nie używane ostatnio proxy
3. Połącz się ze stroną i sprawdź nagłówek odpowiedzi (2xx, 3xx, 4xx, 5xx)
4. Po uzyskaniu poprawnego kodu (innego niż 429), dodaj go do bazy danych SQL. Dane zapisuj wg schematu ID;Data połączenia; URL; kod odpowiedzi
5. W przypadku wystąpienia kodu odpowiedzi 3xx, dodaj dodatkowy element w SQL po "kod odpowiedzi" w postaci "adres docelowy" i wstaw tam adres, do którego kieruje przekierowanie
6. W sytuacji, gdy adres zwraca błąd 429 lub inny związany z timeout, oznacz odpowiednio te adresy. Jeżeli błąd powtórzy się dla 10 adresów pod rząd - odczekaj 60 sekund przy wykonywaniu kolejnych operacji.
7. Po zakończeniu połączenia dla jednego URL, pobierz kolejny URL i wybierz następne dostępne PROXY oraz kontynuuj od punktu 3.

W konsoli podawaj stan procentowy na bazie obliczeń wszystkich adresów, aktualnie przetwarzanego adresu, wykorzystywanego proxy oraz czasu jaki minął od wywołania kodu.

Dlaczego chciałem, aby skrypt tak funkcjonował?

Po pierwsze, trzymanie ponad 20 tysięcy URL-i w pliku ze skryptem to kiepskie rozwiązanie dla czytelności kodu. Po drugie, gdy adresy są w osobnym pliku, do skryptu można podać dowolną listę i działa on w sposób uniwersalny.

Druga sprawa: pomyślałem, że w moim skrypcie można dodać obsługę proxy.

Proxy to pośrednik między urządzeniem użytkownika a internetem. Gdy korzystasz z proxy, Twoje żądania są najpierw przekazywane do serwera pośredniczącego, który następnie przesyła je dalej do docelowego serwera. Otrzymane odpowiedzi również przechodzą przez proxy, zanim dotrą do Ciebie. Proxy może służyć do zwiększenia prywatności, omijania blokad regionalnych lub poprawy bezpieczeństwa.

Po co proxy w tak prostym skrypcie? Przy tej liczbie adresów i obostrzeniach serwera wydawało mi się rozsądne zabezpieczenie przed zablokowaniem jednego adresu IP. Liczyłem też, że rozłożenie zapytań na kilka adresów IP przyspieszy całość, bo limit zapytań zwykle liczony jest na jedno IP.

Przy jednym zapytaniu na sekundę 20 tysięcy URL-i to około 20 000 sekund, czyli mniej więcej 5,5 godziny. Krótsza przerwa lub kilka równoległych zapytań skraca ten czas wielokrotnie, ale tylko wtedy, gdy serwer to wytrzyma i nie zacznie odpowiadać kodem 429.

Zdradzę Ci jednak, że przy wykonywaniu tego skryptu, zwłaszcza na platformie sklepowej z limitami, ostatecznie zrezygnowałem z korzystania z proxy.

Wracając do samego promptu, trzecia sprawa to zapis danych. Przerabiałem już skrypty, które generowały dokumenty HTML z tabelą i odpowiednimi danymi. Dotyczyło to jednak zdecydowanie mniejszych list, w porywach 500 adresów.

Tabela HTML wygenerowana skryptem: lista adresów URL z kodem odpowiedzi 200

Mając wiele tysięcy adresów, zdecydowałem, że najlepiej będzie trzymać zebrane dane w bazie danych. Zamiast stawiać MySQL czy PostgreSQL, skorzystałem z SQLite, czyli bazy SQL zapisanej w jednym pliku.

Na koniec poprosiłem jeszcze o zabezpieczenie, które po 10 błędnych zapytaniach (np. zwracających kod 429) wstrzyma działanie skryptu na 60 sekund. Miało to dać serwerowi czas na „ochłonięcie”.

W efekcie otrzymałem od ChatGPT taki oto kod (wersja z 2024 roku, nie używaj jej, poprawiona wersja jest niżej):

import requests
import sqlite3
import time
from datetime import datetime

# Ścieżka do pliku z URL-ami i proxy
url_file = 'urls.txt'
proxy_file = 'proxies.txt'

# Zainicjalizowanie bazy danych SQLite
conn = sqlite3.connect('responses.db')
c = conn.cursor()

# Tworzenie tabeli, jeśli nie istnieje
c.execute('''CREATE TABLE IF NOT EXISTS responses (
    id INTEGER PRIMARY KEY,
    connection_date TEXT,
    url TEXT,
    response_code INTEGER,
    target_url TEXT
)''')
conn.commit()

# Funkcja do ładowania URL-i z pliku
def load_urls(file_path):
    with open(file_path, 'r') as f:
        return [line.strip() for line in f.readlines()]

# Funkcja do ładowania proxy z pliku
def load_proxies(file_path):
    with open(file_path, 'r') as f:
        return [line.strip() for line in f.readlines()]

# Funkcja do wyboru nieużywanego proxy
def select_proxy(proxy_list, last_used_proxy):
    for proxy in proxy_list:
        if proxy != last_used_proxy:
            return proxy
    return proxy_list[0]

# Funkcja do sprawdzania URL-a z użyciem proxy
def check_url(url, proxy):
    try:
        response = requests.get(url, proxies={'http': proxy, 'https': proxy}, timeout=10)
        return response.status_code, response.url if response.history else None
    except requests.exceptions.RequestException as e:
        return None, None

# Funkcja do dodawania rekordu do bazy danych
def add_to_db(url, status_code, target_url=None):
    connection_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    c.execute('INSERT INTO responses (connection_date, url, response_code, target_url) VALUES (?, ?, ?, ?)',
              (connection_date, url, status_code, target_url))
    conn.commit()

# Funkcja obsługująca proces
def process_urls(urls, proxies):
    last_used_proxy = None
    timeout_count = 0
    total_urls = len(urls)
    start_time = time.time()

    for i, url in enumerate(urls):
        # Wybieramy nieużywane proxy
        proxy = select_proxy(proxies, last_used_proxy)
        last_used_proxy = proxy
        
        # Sprawdzamy URL
        status_code, target_url = check_url(url, proxy)

        # Logika dla błędów i timeout
        if status_code == 429 or status_code is None:
            timeout_count += 1
            if timeout_count >= 10:
                print(f'Wystąpiło 10 kolejnych błędów. Oczekiwanie 60 sekund.')
                time.sleep(60)
                timeout_count = 0
        else:
            timeout_count = 0

            # Dodanie wyniku do bazy danych
            if status_code and status_code != 429:
                if 300 <= status_code < 400 and target_url:
                    add_to_db(url, status_code, target_url)
                else:
                    add_to_db(url, status_code)

        # Obliczenia procentowe i czas
        elapsed_time = time.time() - start_time
        progress = (i + 1) / total_urls * 100
        print(f'Progres: {progress:.2f}%, URL: {url}, Proxy: {proxy}, Czas: {elapsed_time:.2f} sek')

# Główne wywołanie
urls = load_urls(url_file)
proxies = load_proxies(proxy_file)
process_urls(urls, proxies)

# Zamknięcie połączenia z bazą
conn.close()

Żeby była jasność: to w 100% wygenerowany przez ChatGPT skrypt i wersja wstępna. Gdy po czasie przeczytałem go dokładnie, znalazłem w nim błędy, które dobrze pokazują, dlaczego kod od AI trzeba sprawdzać:

  • Przekierowania nigdy nie zostałyby zapisane. Biblioteka requests domyślnie podąża za przekierowaniami, więc status_code to kod ostatniej odpowiedzi (np. 200), a nie 301. Warunek dla kodów 3xx nie mógł się spełnić. Widać to na zrzucie wyżej: same kody 200 i „Docelowy URL: None”.
  • Rotacja proxy nie działała. Funkcja select_proxy w praktyce przełącza się tylko między dwoma pierwszymi adresami z listy.
  • Plik PHP to nie proxy w rozumieniu biblioteki requests. Parametr proxies= oczekuje serwera proxy HTTP, a nie adresu skryptu z parametrem ?url=.
  • Błędy znikały bez śladu. Adresy z timeoutem lub kodem 429 nie trafiały do bazy, więc w wynikach nie było widać, czego nie udało się sprawdzić.

Do tego dochodził osobny plik proxy. Poprosiłem AI, aby został napisany w PHP, tak abym mógł go wgrać na zwykłe serwery z Apache, LiteSpeed czy Nginx. Oto prompt, który napisałem:

Potrzebuję abyś przygotować kod PHP dla pliku proxy który umieszczę na kilku serwerach HTTP. Plik proxy będzie wykorzystywany przez skrypt Python do łączenia się ze wskazanymi w pliku urls.txt adresami.
Muszę skorzystać z proxy ponieważ strona z którą się łaczę posiada limitację 1 zautomatyzowanego odpytania z adresu IP na około 2 sekundy. Wykorzystam do tego celu około 5 różnych serwerów.
Potrzebuję aby serwer proxy pobrał tylko kod odpowiedzi strony lub inne zadane przeze mnie/przez skrypt python dane.

Nie jest to zbyt rozbudowane zapytanie, wiem. Potrzebowałem jednak wiedzieć, jak mniej więcej mógłby wyglądać kod takiego pliku. Oto co zaproponował ChatGPT:

<?php
// Funkcja do wysyłania żądania HTTP do podanego URL
function fetch_url($target_url) {
    $ch = curl_init();
    
    // Ustawienie opcji cURL
    curl_setopt($ch, CURLOPT_URL, $target_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Odbieranie odpowiedzi jako string
    curl_setopt($ch, CURLOPT_HEADER, true);         // Pobieranie nagłówków odpowiedzi
    curl_setopt($ch, CURLOPT_NOBODY, true);         // Tylko nagłówki, bez ciała odpowiedzi
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);          // Timeout dla połączenia
    
    // Wykonanie żądania
    $response = curl_exec($ch);

    // Sprawdzenie, czy wystąpił błąd
    if(curl_errno($ch)) {
        return ['error' => 'Error: ' . curl_error($ch)];
    }

    // Pobranie kodu odpowiedzi HTTP
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    // Zamknięcie połączenia cURL
    curl_close($ch);

    // Zwracanie tylko kodu odpowiedzi HTTP
    return ['http_code' => $http_code];
}

// Sprawdzenie, czy URL został przekazany przez GET
if (isset($_GET['url'])) {
    $target_url = $_GET['url'];

    // Wywołanie funkcji pobierającej kod odpowiedzi
    $result = fetch_url($target_url);

    // Zwrócenie wyniku w formacie JSON
    header('Content-Type: application/json');
    echo json_encode($result);
} else {
    // Gdy nie podano URL, wyślij komunikat o błędzie
    echo json_encode(['error' => 'Brak URL. Podaj parametr ?url=']);
}
?>

Zwróć uwagę, że taki plik PHP, wystawiony publicznie bez żadnego zabezpieczenia, pozwala każdemu wysyłać zapytania do dowolnych adresów z Twojego serwera. Jeśli już z niego korzystasz, ogranicz dostęp (np. tajnym kluczem w parametrze albo listą dozwolonych IP) i usuń go po zakończeniu pracy. W praktyce dla większości stron proxy nie jest potrzebne. Wystarczy rozsądne tempo zapytań.

Bez opóźnień i z wieloma wątkami bardzo łatwo przeciążyć własny serwer. Z jednego komputera to nie jest DDoS w ścisłym sensie, ale efekt dla małego hostingu może być podobny: strona zwalnia lub przestaje odpowiadać prawdziwym użytkownikom.

DDoS (Distributed Denial of Service) to rodzaj cyberataku, w którym wiele zainfekowanych urządzeń jednocześnie wysyła ogromną ilość żądań do jednego serwera lub strony internetowej. Celem jest przeciążenie systemu, co prowadzi do jego spowolnienia lub całkowitego zablokowania, przez co usługi stają się niedostępne dla prawdziwych użytkowników.

Skrypt w Pythonie do sprawdzania kodów odpowiedzi (wersja 2026)

Poniżej znajdziesz nową wersję, którą przepisałem i przetestowałem w październiku 2026 roku na Pythonie 3.14 i bibliotece requests 2.34. Kod korzysta tylko z biblioteki standardowej i requests, która oficjalnie wspiera Pythona od 3.10 do 3.15, więc powinien bez problemu działać także na świeżym Pythonie 3.15. Zrezygnowałem z proxy i bazy SQLite. Wyniki trafiają do pliku CSV, który otworzysz w Excelu albo Arkuszach Google. Skrypt:

  • wczytuje adresy z pliku urls.txt, pomijając puste linie, komentarze i duplikaty,
  • wysyła zapytania z własnym nagłówkiem User-Agent i limitem czasu (timeout), więc nie zawiesi się na niedziałającym serwerze,
  • zapisuje kod pierwszej odpowiedzi (np. 301), kod i adres finalny, liczbę przekierowań i cały łańcuch,
  • wykrywa pętle przekierowań (ponad 10 skoków),
  • po kodzie 429 czeka (zgodnie z nagłówkiem Retry-After, jeśli serwer go poda) i ponawia zapytanie do 3 razy,
  • sprawdza kilka adresów równolegle (ThreadPoolExecutor, domyślnie 3 wątki, maksymalnie 10),
  • zapisuje każdy wynik na bieżąco. Gdy przerwiesz pracę skrótem Ctrl+C, dotychczasowe dane zostaną w pliku.

Zapisz poniższy kod jako status_check.py:

"""Masowe sprawdzanie kodów odpowiedzi HTTP dla listy adresów URL.

Użycie:
    python status_check.py                       # czyta urls.txt, zapisuje wyniki.csv
    python status_check.py -i lista.txt -o raport.csv -w 2 -d 1
"""
import argparse
import csv
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

import requests

# Zmień na własną nazwę i adres - administrator serwera będzie wiedział, kto pyta.
USER_AGENT = "Mozilla/5.0 (compatible; StatusCheck/1.0; +https://twoja-domena.pl/)"
TIMEOUT = (5, 15)       # (czas na połączenie, czas na odpowiedź) w sekundach
MAX_REDIRECTS = 10      # więcej skoków = pętla lub błąd konfiguracji
MAX_RETRIES_429 = 3     # ile razy ponowić zapytanie po kodzie 429

_local = threading.local()


def get_session():
    """Każdy wątek dostaje własną sesję (requests.Session nie jest w pełni bezpieczna wątkowo)."""
    if not hasattr(_local, "session"):
        s = requests.Session()
        s.headers.update({"User-Agent": USER_AGENT})
        s.max_redirects = MAX_REDIRECTS
        _local.session = s
    return _local.session


def load_urls(path):
    """Wczytuje adresy (jeden na linię), pomija puste linie, komentarze (#) i duplikaty."""
    seen, urls = set(), []
    with open(path, encoding="utf-8-sig") as f:
        for line in f:
            url = line.strip()
            if url and not url.startswith("#") and url not in seen:
                seen.add(url)
                urls.append(url)
    return urls


def check_url(url, delay):
    """Zwraca kod pierwszej odpowiedzi, finalny kod i URL oraz liczbę przekierowań."""
    result = {"url": url}
    session = get_session()
    for attempt in range(MAX_RETRIES_429 + 1):
        result.update({
            "first_status": "", "final_status": "", "final_url": "", "redirects": 0,
            "chain": "", "error": "", "time_ms": "",
            "checked_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        })
        if delay:
            time.sleep(delay)
        start = time.perf_counter()
        try:
            # GET zamiast HEAD - część serwerów źle obsługuje HEAD.
            # stream=True: pobieramy nagłówki, bez ściągania całej treści strony.
            with session.get(url, timeout=TIMEOUT, allow_redirects=True, stream=True) as r:
                hops = r.history  # odpowiedzi 3xx po drodze do finalnego adresu
                result.update({
                    "first_status": hops[0].status_code if hops else r.status_code,
                    "final_status": r.status_code,
                    "final_url": r.url,
                    "redirects": len(hops),
                    "chain": " > ".join(f"{h.url} ({h.status_code})" for h in hops + [r]) if hops else "",
                    "time_ms": round((time.perf_counter() - start) * 1000),
                })
                if r.status_code == 429 and attempt < MAX_RETRIES_429:
                    # 429 = za dużo zapytań. Czekamy tyle, ile każe serwer (Retry-After) lub coraz dłużej.
                    retry_after = r.headers.get("Retry-After", "")
                    wait = int(retry_after) if retry_after.isdigit() else 10 * (attempt + 1)
                    time.sleep(min(wait, 120))
                    continue
        except requests.exceptions.TooManyRedirects:
            result["error"] = f"Ponad {MAX_REDIRECTS} przekierowań (pętla?)"
        except requests.exceptions.Timeout:
            result["error"] = "Timeout"
        except requests.exceptions.RequestException as e:
            result["error"] = type(e).__name__
        return result
    return result


def main():
    p = argparse.ArgumentParser(description="Masowe sprawdzanie kodów odpowiedzi HTTP")
    p.add_argument("-i", "--input", default="urls.txt", help="plik z adresami (domyślnie urls.txt)")
    p.add_argument("-o", "--output", default="wyniki.csv", help="plik CSV z wynikami")
    p.add_argument("-w", "--workers", type=int, default=3, help="liczba równoległych wątków (1-10)")
    p.add_argument("-d", "--delay", type=float, default=0.5, help="przerwa przed każdym zapytaniem w wątku [s]")
    args = p.parse_args()

    workers = max(1, min(args.workers, 10))  # górny limit, żeby nie przeciążyć serwera
    urls = load_urls(args.input)
    total = len(urls)
    print(f"Adresów do sprawdzenia: {total}, wątki: {workers}, przerwa: {args.delay} s")

    fields = ["url", "first_status", "final_status", "final_url", "redirects",
              "chain", "error", "time_ms", "checked_at"]
    start = time.time()
    # utf-8-sig + średnik: plik otworzy się poprawnie w polskim Excelu i w Arkuszach Google
    with open(args.output, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=fields, delimiter=";")
        writer.writeheader()
        pool = ThreadPoolExecutor(max_workers=workers)
        futures = [pool.submit(check_url, u, args.delay) for u in urls]
        try:
            for done, fut in enumerate(as_completed(futures), 1):
                row = fut.result()
                writer.writerow(row)
                f.flush()  # każdy wynik od razu trafia na dysk
                status = row["error"] or f'{row["first_status"]} -> {row["final_status"]}'
                print(f"[{done}/{total} {done / total:.0%}] {status} {row['url']}")
        except KeyboardInterrupt:
            print("\nPrzerwano (Ctrl+C). Zapisane wyniki zostają w pliku CSV.")
            pool.shutdown(wait=False, cancel_futures=True)
            return
        pool.shutdown()

    print(f"Gotowe w {time.time() - start:.1f} s. Wyniki: {args.output}")


if __name__ == "__main__":
    main()

Przed pierwszym uruchomieniem zmień w kodzie wartość USER_AGENT na własną nazwę i adres. Dzięki temu administrator serwera wie, kto wysyła zapytania. Skrypt pobiera tylko nagłówki odpowiedzi (stream=True) zamiast całych stron, co oszczędza transfer i czas.

Ile wątków ustawić? Domyślne wartości (3 wątki, 0,5 s przerwy w każdym wątku) to maksymalnie około 6 zapytań na sekundę, w praktyce mniej, bo dochodzi czas odpowiedzi serwera. Na sklepach SaaS i słabszych hostingach zacznij ostrożniej, np. -w 2 -d 1, i zwiększaj tempo dopiero wtedy, gdy w wynikach nie pojawiają się kody 429 ani timeouty. Kod 429 oznacza, że serwer prosi o zwolnienie, a nagłówek Retry-After mówi, ile sekund odczekać. Sprawdzaj w ten sposób swoje strony albo strony, do których analizy masz zgodę właściciela.

Jak uruchomić skrypt w Python na własnym urządzeniu?

Najpierw potrzebujesz Pythona. W chwili tej aktualizacji (13 października 2026) najnowszą stabilną wersją na python.org jest Python 3.15.0 z 9 października 2026. Seria 3.14 (ostatnio 3.14.8 z 30 września) też jest nadal wspierana, a Python 3.10 nie dostaje już poprawek. Instaluj najnowszą stabilną wersję, którą proponuje python.org – skrypt z tego wpisu nie wymaga żadnych nowości językowych, potrzebuje tylko Pythona 3.10 lub nowszego.


Instalacja Pythona na Windows

Od wersji 3.14 zalecanym sposobem instalacji na Windows jest Python install manager. Klasyczny instalator .exe jest przestarzały i według dokumentacji nie będzie wydawany dla Pythona 3.16 i nowszych.

Krok 1: Pobranie Pythona

  • Wejdź na python.org/downloads i pobierz Python install manager albo zainstaluj go z Microsoft Store (wyszukaj „Python Install Manager”).
  • Uruchom pobrany plik i zainstaluj menedżera.

Krok 2: Instalacja

  • Otwórz terminal (PowerShell lub wiersz poleceń: Win + R, wpisz cmd i naciśnij Enter).
  • Wpisz py install 3.15 (albo py install default, żeby pobrać wersję domyślną). Listę zainstalowanych wersji pokaże py list.
  • Przy pierwszej instalacji menedżer może zapytać o dodanie katalogu do zmiennej PATH. To opcjonalne: komendy py i python działają i bez tego.

Krok 3: Sprawdzenie instalacji

  • W terminalu wpisz: python --version
  • Jeśli zobaczysz numer wersji Pythona, instalacja zakończyła się pomyślnie.

Instalacja Pythona na macOS

Krok 1: Sprawdzenie zainstalowanego Pythona

  • Otwórz Terminal (Cmd + Spacja, wpisz „Terminal”, naciśnij Enter) i wpisz: python3 --version
  • Nawet jeśli zobaczysz numer wersji, może to być starszy Python dostarczany przez Apple razem z narzędziami Xcode. Nie usuwaj go, bo korzysta z niego system, ale do własnych skryptów zainstaluj aktualną wersję.

Krok 2: Instalacja z python.org lub przez Homebrew

  • Sposób 1: pobierz instalator .pkg z python.org i przejdź przez kreator. Na koniec w folderze Aplikacje → Python 3.15 (numer zależy od zainstalowanej wersji) kliknij dwukrotnie Install Certificates.command. Bez tego połączenia HTTPS ze skryptu mogą kończyć się błędem certyfikatu.
  • Sposób 2: jeśli używasz Homebrew (popularnego menedżera pakietów dla macOS), zainstaluj go poleceniem z brew.sh:
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  • Po instalacji Homebrew wpisz: brew install python

Krok 3: Sprawdzenie instalacji

  • Otwórz nowe okno terminala i wpisz: python3 --version
  • Powinieneś zobaczyć numer właśnie zainstalowanej wersji.

Instalacja Pythona na Linux (Debian, Ubuntu)

Krok 1: Sprawdzenie zainstalowanej wersji Pythona

  • Otwórz terminal (Ctrl + Alt + T) i wpisz:
  python3 --version
  • Większość dystrybucji Linuksa ma już zainstalowanego Pythona 3.

Krok 2: Instalacja Pythona (jeśli go nie ma)

  • Użyj menedżera pakietów. Pakiet python3-venv przyda się do utworzenia środowiska wirtualnego (o tym za chwilę):
  sudo apt update
  sudo apt install python3 python3-venv

Krok 3: Sprawdzenie instalacji

  • W terminalu wpisz: python3 --version. Powinieneś zobaczyć wersję Pythona 3. Wersja z repozytorium dystrybucji bywa starsza niż ta z python.org, ale do tego skryptu w zupełności wystarczy.

Tworzenie i uruchamianie skryptów Python

Skrypt Python to plik tekstowy z rozszerzeniem .py, który zawiera kod w języku Python. Możesz go utworzyć w dowolnym edytorze tekstu.

Krok 1: Wybór lokalizacji na plik

  • Plik .py możesz umieścić w dowolnym folderze na komputerze.
  • Aby ułatwić sobie pracę, stwórz osobny folder na skrypty, np.:
  • Windows: C:\MojeSkrypty
  • macOS: /Users/nazwa_użytkownika/MojeSkrypty, Linux: /home/nazwa_użytkownika/MojeSkrypty

Krok 2: Tworzenie pliku

  • Otwórz edytor tekstu (polecam VS Code, o nim niżej).
  • W nowym pliku wpisz prosty kod, np.:
  print("Hello, world!")
  • Zapisz plik z rozszerzeniem .py, np. jako hello.py, w wybranym folderze.

Krok 3: Otwórz terminal i przejdź do folderu

  • Windows: naciśnij Win + R, wpisz cmd i naciśnij Enter (albo otwórz PowerShell).
  • macOS: Cmd + Spacja, wpisz „Terminal”. Linux: Ctrl + Alt + T.

W terminalu przejdź do folderu, w którym zapisałeś skrypt:

  cd C:\MojeSkrypty      # Windows
  cd ~/MojeSkrypty       # macOS/Linux

Krok 4: Uruchom skrypt

  python hello.py     # Windows
  python3 hello.py    # macOS/Linux

Jeśli wszystko działa poprawnie, zobaczysz w terminalu komunikat Hello, world!.

Wybór edytora tekstu

Do pisania skryptów w Pythonie możesz używać różnych edytorów, od prostych po zaawansowane IDE. Według mnie najlepiej sprawdzi się:

Visual Studio Code (VS Code):

Darmowy, bardzo popularny edytor z obsługą rozszerzeń, w tym wsparciem dla Pythona.
Instalacja: VS Code
Aby ułatwić pisanie kodu w Pythonie, zainstaluj rozszerzenie Python dostępne w Marketplace.

Jak użyć mojego skryptu do analizy adresów?

Instrukcja dotyczy nowej wersji skryptu z tego wpisu (status_check.py). Potrzebujesz tylko dwóch plików w jednym folderze: skryptu i listy adresów.

Uruchamianie skryptu do analizy adresów

1. Utwórz środowisko wirtualne i zainstaluj bibliotekę requests

Skrypt korzysta z biblioteki requests, której nie ma w standardowej instalacji Pythona. Najbezpieczniej zainstalować ją w środowisku wirtualnym (venv), czyli osobnym folderze z bibliotekami dla danego projektu. Homebrew na macOS oraz nowsze wersje Debiana i Ubuntu blokują instalowanie pakietów w systemowym Pythonie i zamiast tego pokazują błąd externally-managed-environment. Venv omija ten problem. W folderze ze skryptem wpisz:

# Windows
python -m venv .venv
.venv\Scripts\python -m pip install requests

# macOS / Linux
python3 -m venv .venv
.venv/bin/python -m pip install requests

2. Przygotuj plik z URL-ami (urls.txt)

Utwórz plik urls.txt w tym samym folderze co skrypt i wpisz w nim adresy do sprawdzenia, po jednym w każdej linii. Może ich być 100, 1000 albo 50 000. Im więcej adresów, tym dłużej potrwa analiza. Linie zaczynające się od # są pomijane.

https://domena.pl/kategoria/
https://domena.pl/kategoria/podstrona1
https://domena.pl/kategoria/podstrona2
https://domena.pl/blog/wpis-na-blogu

3. Uruchom skrypt

.venv\Scripts\python status_check.py

lub dla macOS/Linux:

.venv/bin/python status_check.py

Ustawienia zmienisz parametrami, np. -i lista.txt (plik wejściowy), -o raport.csv (plik wynikowy), -w 2 (liczba wątków) i -d 1 (przerwa w sekundach). Pełną listę pokaże --help.

4. Poczekaj, aż skrypt skończy pracę. Nie zamykaj terminala i nie usypiaj komputera. W konsoli zobaczysz postęp, np. [3/5 60%] 301 -> 200 http://semurai.pl/.

Terminal z postępem pierwszej wersji skryptu: procent analizy, adres URL i kod odpowiedzi
Zrzut z pierwszej wersji skryptu (2024) – nowa wersja wypisuje postęp w formacie [3/5 60%] 301 -> 200 adres.

5. Wynik pracy skryptu

W folderze ze skryptem pojawi się plik wyniki.csv (kodowanie UTF-8, średnik jako separator). Otworzysz go w Excelu, zaimportujesz do Arkuszy Google albo wczytasz do własnego narzędzia. Tak wyglądał wynik mojego testu na kilku adresach:

urlfirst_statusfinal_statusfinal_urlredirectserror
https://semurai.pl/200200https://semurai.pl/0–
https://semurai.pl/nie-istnieje-test/404404https://semurai.pl/nie-istnieje-test/0–
http://semurai.pl/301200https://semurai.pl/1–
http://www.semurai.pl/akademia/blad-404-410-4xx301200https://semurai.pl/akademia/blad-404-410-4xx/2–
https://nie-istnieje-domena-xyz123.pl/–––0ConnectionError

W pliku CSV są jeszcze kolumny chain (cały łańcuch przekierowań, np. http://www.semurai.pl/... (301) > https://semurai.pl/... (301) > https://semurai.pl/.../ (200)), time_ms (czas odpowiedzi) i checked_at (data sprawdzenia).

Jak czytać wyniki?

  • first_status 3xx, final_status 200: adres przekierowuje na działającą stronę. Sprawdź, czy przekierowanie jest stałe (301/308) czy tymczasowe (302/307). Więcej o tym w artykule o przekierowaniach 301, 302, 307 i 308.
  • redirects większe niż 1: łańcuch przekierowań, jak w moim teście (http + www → https bez www → adres ze slashem). Warto skierować linki wewnętrzne od razu na adres finalny.
  • final_status 404 lub 410: strona nie istnieje. Zdecyduj, czy przekierować ją na odpowiednik, czy zostawić błąd. Pomoże w tym wpis o błędach 404, 410 i innych kodach 4xx.
  • 5xx, Timeout, ConnectionError: problem po stronie serwera lub sieci (co oznaczają poszczególne kody, opisuję we wpisie o błędach 5xx). Sprawdź te adresy ponownie po jakimś czasie, zanim wyciągniesz wnioski.
  • 429: serwer ograniczył liczbę zapytań. Uruchom skrypt ponownie dla tych adresów z mniejszą liczbą wątków i dłuższą przerwą.

Taka lista to dopiero początek. Jeśli po migracji sklepu lub zmianie struktury adresów wyniki pokazują setki przekierowań i błędów 404, a nie wiesz, od czego zacząć, mogę to uporządkować w ramach audytu SEO.

Pierwotna wersja skryptu (2024). Poniżej zostawiam paczki z pierwszą wersją: z obsługą proxy i bez. Mają one opisane wyżej błędy (m.in. nie zapisują kodów przekierowań), więc traktuj je jako materiał archiwalny. Do pracy używaj kodu z sekcji o wersji 2026.

Dane z CSV możesz dalej przetwarzać dowolnie: wczytać do arkusza Google, załadować do swojej aplikacji albo przygotować widok HTML z czytelną tabelą. Kod do takich zadań również pomoże Ci napisać asystent AI. Tak wyglądała moja prośba o eksport danych z bazy SQLite do CSV w pierwszej wersji skryptu:

Rozmowa z ChatGPT: prośba o skrypt eksportujący dane z bazy SQLite do pliku CSV

Co jeszcze mogę osiągnąć wykorzystując Python oraz ChatGPT?

W zasadzie co chcesz, jak to w programowaniu. Ja napisałem sobie na przykład proste narzędzie do pobierania wybranych danych ze wskazanych stron internetowych. Dzięki temu uzyskałem wiele cennych danych tabelarycznych, które pozwoliły mi utworzyć stronę internetową z unikalną treścią.

Możesz też stworzyć prosty crawler i pobierać informacje o dowolnej witrynie, śledzić zmiany na stronie, cyklicznie sprawdzać jej status, łączyć się z różnymi API i wiele więcej. Wszystko zależy od Twoich potrzeb, możliwości i tego, jak dobrze rozumiesz działanie technologii i programowania.

Lokalna strona z listą producentów zbudowana na podstawie danych pobranych skryptem w Pythonie

Niedawno, praktycznie bez mojego udziału, wygenerował się plik .htaccess z wymaganymi przekierowaniami 301 w sklepie internetowym! Podałem tylko adresy z mapy strony oraz nowe adresy ze strony w wersji produkcyjnej, dodałem wzorce, których należało się trzymać, i w mgnieniu oka miałem gotowy, działający plik .htaccess. Zamiast ręcznie dopasowywać każdy adres, stworzyłem sobie narzędzie, które zrobiło za mnie większość pracy. Mnie zostało zweryfikowanie i sprawdzenie poprawności pliku, i tego kroku nie warto pomijać.

Na koniec: nie bój się testować! Czasem kilka linijek kodu pozwala zaoszczędzić mnóstwo czasu.

FAQ: sprawdzanie kodów odpowiedzi w Pythonie

Czy do tego skryptu muszę umieć programować?

Nie. Wystarczy zainstalować Pythona, skopiować kod do pliku i uruchomić go w terminalu według instrukcji z tego wpisu. Jeśli chcesz coś zmienić, np. dodać kolumnę z nagłówkiem X-Robots-Tag, możesz poprosić o to asystenta AI. Zmieniony skrypt przetestuj na kilku adresach, zanim puścisz go na całą listę.

Dlaczego skrypt pokazuje dwa kody odpowiedzi?

Bo przy przekierowaniu serwer najpierw odpowiada kodem 3xx, a dopiero adres docelowy zwraca 200 (albo błąd). Kolumna first_status pokazuje, co zwraca sprawdzany adres, a final_status to, gdzie ostatecznie trafia użytkownik i robot wyszukiwarki.

Na jakiej wersji Pythona działa skrypt?

Na Pythonie 3.10 lub nowszym – tego wymaga aktualna biblioteka requests (2.34.2). Skrypt przetestowałem na Pythonie 3.14. Nie korzysta z żadnych nowości językowych, a requests oficjalnie wspiera już Pythona 3.15 (wydanego 9 października 2026 r.), więc na nowej wersji też powinien działać bez zmian.

Ile adresów mogę sprawdzić naraz?

Długość listy nie jest ograniczona. Ograniczeniem jest tempo, jakie wytrzyma serwer. Przy domyślnych ustawieniach (3 wątki, 0,5 s przerwy) i odpowiedziach serwera w około 0,3 s sprawdzenie 20 000 adresów zajmie mniej więcej półtorej godziny. Jeśli pojawiają się kody 429 lub timeouty, zmniejsz liczbę wątków i wydłuż przerwę.

Czy zamiast skryptu mogę użyć gotowego narzędzia?

Tak. Crawlery SEO, np. Screaming Frog, też sprawdzają kody odpowiedzi z listy adresów. Skrypt przydaje się, gdy chcesz mieć pełną kontrolę nad tempem i formatem wyników, uruchamiać sprawdzanie cyklicznie albo nie masz licencji na płatne narzędzie.

Tomasz Stopka

Tomasz Stopka

Specjalista SEO od 2008 roku · autor książki „Content Marketing”

Pracowałem w agencjach, w portalu Onet i na własny rachunek jako SEMURAI. W Akademii dzielę się tym, co sprawdziło się w praktyce – konkretnie i bez ściemy.

Czytaj także