Przerwa w trakcie pobierania treści omija typowy handler timeoutu w requests

Ostatnia aktualizacja: August 17, 2026
Przerwa w trakcie pobierania treści omija typowy handler timeoutu w requests
Podsumowanie AI
Szukasz błędów w istniejącym kodzie na requests? Sprawdź założenia dotyczące przekierowań, handlery łapiące tylko requests.exceptions.Timeout, ale spodziewające się objąć także zawieszenie w trakcie body, oraz konsumentów .text, którzy dostają odpowiedzi bez charsetu. Mechaniczna migracja do httpx? Zmień przestrzeń wyjątków na httpx.TimeoutException albo węższe klasy faz, zdecyduj, czy włączyć follow_redirects, i ponownie zweryfikuj założenia dekodowania. Istniejący handler w requests i tak nie łapie pokazanego przypadku mid-body; migracja nie tworzy tego konkretnego błędu. Piszesz nowy kod, który pobiera wiele URL-i? httpx ma sens, gdy potrzebujesz AsyncClient oraz osobnych timeoutów dla connect, read, write i pool.

Zapiszmy strażnika, którego pisze prawie każdy:

try:
    r = requests.get(url, timeout=0.5)
except requests.exceptions.Timeout:
    retry()

Teraz skierujmy go na serwer, który od razu odsyła status i nagłówki, a potem zawiesza się przed wysłaniem body. Odczyt przekroczy limit czasu. Strażnik nie zadziała. Zamiast tego pojawi się ConnectionError, a ConnectionError nie jest podklasą Timeout.

Ten sam scenariusz w httpx kończy się ReadTimeout, a to już jest TimeoutException, więc odpowiedni handler go przechwyci.

Chciałem sprawdzić, czym httpx różni się od requests w sposób, który potrafi wysadzić scrapery, i spodziewałem się tekstu o async. Okazało się, że async było najmniej ciekawą rzeczą na liście.

Co testowałem i jak

Osiem prób na lokalnym serwerze testowym, bo deklaracje klienta o tym, co zrobił, to jeszcze nie dowód. Serwer zlicza połączenia TCP — zwiększane raz na każdy zaakceptowany socket, zanim zostanie sparsowana pierwsza linia żądania — oraz rzeczywiście pobrane ścieżki. Reużycie połączeń i podążanie za przekierowaniami to twierdzenia o ruchu sieciowym, a sieć to miejsce, w którym można je zweryfikować.

httpx 0.28.1 z dodatkiem http2, requests 2.34.2, Python 3.14.2, macOS arm64. Oba w świeżym virtualenv, żeby jedno nie dziedziczyło po drugim żadnego śladu. Surowy wynik: httpx-probes.json.

Sześć przewidywań trafiło do harnessa jeszcze przed pierwszym uruchomieniem i zostało tam do końca. Trzy się potwierdziły, dwa były błędne, jedno było trafne dla przypadku, który miałem na myśli, ale nie dla tego, który naprawdę miał znaczenie. Rozliczenie jest w prediction-scorecard.json.

Domyślne ustawienia, które zmieniają zachowanie po przejściu na inne narzędzie

Measured results chart: Defaults that differ between clients

Zachowanierequests 2.34.2httpx 0.28.1
Domyślnie śledzi przekierowaniataknie
Wywołanie na poziomie modułu ponownie używa połączeńnienie
Zawieszenie przed nagłówkamiReadTimeoutReadTimeout
Zawieszenie w trakcie bodyConnectionErrorReadTimeout
Brak zadeklarowanego charsetuISO-8859-1utf-8
HTTP/2niedostępneopcjonalne, działa
Osobne timeouty dla connect/read/write/poolnietak

Przekierowania, ponowne użycie socketów, wyniki wyjątków, dekodowanie i negocjacja protokołu zostały zaobserwowane w testach. Kształt API timeoutów oraz brak flagi HTTP/2 w requests to obserwacje dotyczące możliwości API. httpx-probes.json.

Trzy z tych wierszy zmienią działanie Twojego kodu w dniu migracji, i to bez żadnego ostrzeżenia.

Przekierowania: wyłączone domyślnie, a serwer to potwierdza

Łańcuch przekierowań o czterech skokach, kończący się na /ok:

KlientCo zobaczył serwerZwrócony status
requests5 żądań200
httpx1 żądanie302
httpx, follow_redirects=True5 żądań200

Pięć to cztery skoki plus adres docelowy. W mojej prognozie było cztery — arytmetyka, której nie sprawdziłem; kierunek był sednem tezy, a liczba została tu poprawiona zamiast po cichu w tekście.

To zgadza się z dokumentacją httpx i ma sens projektowy — przekierowanie to coś, o czym wywołujący może chcieć wiedzieć. Jednocześnie to najprostszy sposób, żeby migracja rozjechała się bez żadnego wyjątku. Kod dostaje 302, response.text jest puste, parser nie znajduje wierszy, a log mówi 200 OK… no właśnie nie: mówi 302, tylko nikt wcześniej nie pilnował statusu, bo w requests nie było czego pilnować.

Wynik timeoutu, który odczytałem odwrotnie

Przewidywałem, że httpx nazwie konkretną fazę, która się wyłożyła, a requests wrzuci oba przypadki do jednej klasy. Jest odwrotnie.

Oficjalne źródło: dokumentacja timeoutów Requests.

System diagram: Where the Timeout Lands

Oficjalne źródło: dokumentacja timeoutów HTTPX.

Zawieszenierequestshttpx
Przed linią statusuReadTimeoutReadTimeout
W trakcie body, po wysłaniu nagłówkówConnectionErrorReadTimeout

httpx nadaje ten sam, poprawny opis obu przypadkom. requests rozdziela je — i robi to na granicy, na której zwykle opiera się kod retry.

Konsekwencja nie wynika tu z samej hierarchii klas. Uruchomiłem strażnika:

Zawieszenieexcept requests.exceptions.Timeoutexcept httpx.TimeoutException
Przed linią statusułapiełapie
W trakcie bodyucieka jako ConnectionErrorłapie

timeout-retry-guard.json. requests.exceptions.ConnectionError nie jest podklasą requests.exceptions.Timeout; httpx.ReadTimeout jest podklasą httpx.TimeoutException.

Komunikat wyjątku w requests mówi Read timed out. wewnątrz ConnectionError. Biblioteka wie, co się stało. Po prostu nie przekazuje tego typom, a to właśnie typ sprawdza Twoje except.

Ten przypadek jest bardzo konkretny: nagłówki przychodzą, a potem postęp body zatrzymuje się na tyle długo, że przekracza read timeout. Odpowiedź, która dalej dostarcza porcje danych w oknie timeoutu, w tym strumień celowo rozciągany w czasie, może zachowywać się inaczej i nie była tu testowana.

Pooling połączeń: cała różnica tkwi w API klienta

Dziesięć GET-ów, cztery sposoby, sockety liczone po stronie serwera:

SposóbOtwarte sockety
httpx.get() × 1010
requests.get() × 1010
httpx.Client()1
requests.Session()1

To identyczny wynik i warto to zaznaczyć, bo to najczęstszy element obiegowej wiedzy o tej parze — że httpx buforuje połączenia, a requests nie. Na poziomie modułu nie robi tego żaden z nich. Oba korzystają z poolingu przez obiekt klienta. Jeśli dziś wywołujesz requests.get() w pętli, przejście na httpx.get() w pętli nic nie zmieni w sposobie zużywania socketów.

System diagram: Pooling Lives in the Client

HTTP/2 jest jawne i wymaga dodatkowego pakietu

Dla jednego publicznego endpointu HTTP/2 zapisanego w artefakcie:

Oficjalne źródło: RFC 9113: HTTP/2.

KlientWynegocjowano
httpx.Client(http2=True)HTTP/2
httpx.Client(http2=False)HTTP/1.1
requestsHTTP/1.1, nie ma takiej flagi

Potrzebujesz dodatku httpx[http2]. Założyłem, że zwykłe pip install httpx da klienta, który po cichu dogaduje się przez 1.1, i sprawdziłem to zanim cokolwiek napisałem:

ImportError: Using http2=True, but the 'h2' package is not installed.
Make sure to install httpx using `pip install httpx[http2]`.

Wyjątek pojawia się przy tworzeniu Client, zanim zostanie wykonane choć jedno żądanie, a komunikat wskazuje poprawkę. To jest dobra wersja takiego błędu, a ja miałem odwrotne oczekiwanie (http2-extra-missing.json).

Ten test potwierdza poprawną negocjację protokołu na tym endpointcie. Nie dowodzi przewagi prędkości scrapowania; nie testowano porównywalnego workloadu HTTP/1.1.

Przepustowość sekwencyjna i współbieżna na fixture

Dwadzieścia żądań do endpointu, który śpi 0,3 s:

TrybCzas ściennySockety
Sync, jeden Client6.138 s1
Async, jeden AsyncClient0.357 s20

Wersja współbieżna zakończyła się po 0,357 sekundy wobec 6,138 sekundy dla sekwencyjnej. Jednocześnie otworzyła dwadzieścia połączeń, podczas gdy klient synchroniczny użył jednego ponownie, więc eksperyment mierzy zmianę modelu wykonania i efektywnej współbieżności, a nie samą szybkość biblioteki.

To uczciwy opis tego wyniku. To pomiar współbieżności na celowo wolnym endpointcie, a nie pomiar samego httpx. Każdy klient z działającym async wyląduje w podobnym miejscu, a dla szybkiego endpointu różnica się kurczy.

Przypadek charsetu, którego nie przewidziałem

Założyłem, że odpowiedź z kłamliwym nagłówkiem — charset=iso-8859-1 przy bajtach UTF-8 — da identyczny bełkot tekstowy w obu bibliotekach. I tak właśnie jest. Obie zwracają Café Ubersetzung â naïve résumé, podczas gdy źródło zawiera Café Ubersetzung — naïve résumé.

Nieprzewidziany przypadek to ten ważny:

Odpowiedźrequests dekodujehttpx dekoduje
charset=utf-8, bajty utf-8poprawniepoprawnie
charset=iso-8859-1, bajty utf-8krzakikrzaki
brak charsetukrzakipoprawnie

requests wraca do ISO-8859-1, gdy nagłówek nic nie mówi, a httpx domyślnie używa UTF-8. W fixture bez charsetu oba klienty zwracały więc inny tekst po .text; odbiorcy korzystający z response.content zachowują te same oryginalne bajty.

Pamięć, bo to łatwo zmierzyć

Peak RSS, /usr/bin/time -l, jeden świeży proces na komórkę:

Komórkarequestshttpx
Tylko import36.0 MiB30.6 MiB
Import + jedno GET35.8 MiB40.7 MiB

To migawki z jednego procesu, a to, że requests po jednym GET ma wynik minimalnie niższy niż przy samym imporcie, pokazuje szum pomiarowy. Nie dają podstaw do wniosków o kierunku różnicy pamięci; potrzebne byłyby wielokrotne próbki i zakresy.

Co to oznacza przy wyborze

Szukasz błędów w istniejącym kodzie na requests? Sprawdź założenia dotyczące przekierowań, handlery łapiące tylko requests.exceptions.Timeout, ale spodziewające się objąć także zawieszenie w trakcie body, oraz konsumentów .text, którzy dostają odpowiedzi bez charsetu.

Mechaniczna migracja do httpx? Zmień przestrzeń wyjątków na httpx.TimeoutException albo węższe klasy faz, zdecyduj, czy włączyć follow_redirects, i ponownie zweryfikuj założenia dekodowania. Istniejący handler w requests i tak nie łapie pokazanego przypadku mid-body; migracja nie tworzy tego konkretnego błędu.

Piszesz nowy kod, który pobiera wiele URL-i? httpx ma sens, gdy potrzebujesz AsyncClient oraz osobnych timeoutów dla connect, read, write i pool. Te fazy mówią, gdzie nastąpiło oczekiwanie — przy zestawianiu połączenia, postępie body odpowiedzi, wysyłaniu żądania albo lokalnym oczekiwaniu na pool — ale nie tłumaczą, dlaczego zdalny host tak się zachował.

Piszesz coś małego i synchronicznego? requests jest w porządku i jest wszędzie. Powód do migracji nie leży w prędkości.

Cokolwiek wybierzesz, korzystaj z obiektu klienta, a nie z funkcji na poziomie modułu. To jedyna zmiana z tej listy, która jest wygraną w obu bibliotekach.

Gdzie pasuje zarządzane API

Wszystko powyżej dotyczy warstwy pobierania, a to akurat jest najłatwiejsza część. Żadna z tych opcji nie renderuje JavaScriptu, nie obsługuje challenge anty-botowego i nie zamienia HTML-a w wiersze, których potrzebujesz.

Nota autora: Thunderbit to nasza zarządzana opcja do renderowania i ekstrakcji URL-in. Nie była testowana w tym harnessie dla klientów HTTP. Traktuj tę kategorię jako rozwiązanie wtedy, gdy problemem jest pobranie strony lub ustrukturyzowana ekstrakcja — a nie semantyka klienta HTTP.

Jeśli pobierasz zwykłe strony i samodzielnie je parsujesz, oba klienty nadal wchodzą w grę. Usługa zarządzana to osobna decyzja build-versus-buy, a nie argument za wyborem jednej z tych bibliotek.

Szerszy przegląd znajdziesz w naszym zestawieniu API do web scrapingu, a po stronie własnego hostingu — w pillarze o open-source scraperach.

Wypróbuj Thunderbit do ekstrakcji danych z sieci

Werdykt

Do nowej warstwy pobierania w Pythonie, która potrzebuje współbieżności async, timeoutów rozdzielonych na fazy i fallbacku do UTF-8, moja domyślna rekomendacja przy tych testach to httpx. requests nadal jest dobrym wyborem dla dojrzałego kodu synchronicznego, gdzie ryzyko migracji przeważa nad tymi korzyściami. Proxy, polityki retry, fingerprinting TLS, streaming, uploady i realistyczna zmienność sieci nie były testowane, więc nie jest to uniwersalny ranking klientów do scrapingu.

Powód, by uważać, to domyślne zachowanie przy przekierowaniach, i jest to prawdziwe zagrożenie właśnie dlatego, że jest to dobre rozwiązanie projektowe. Jasne jest lepsze od domyślnego — aż do momentu, gdy domyślne okazało się nośne w kodzie, który już wysłałeś do produkcji.

Prerejestrowana karta wyników zakończyła się trzema trafnymi przewidywaniami, dwoma błędnymi i jednym niepełnym. Najbardziej użyteczna korekta dotyczyła wyjątku mid-body; resztę decyzji powinno się opierać na zaobserwowanym zachowaniu, a nie na narracji wokół scorecardu.

Wypróbuj Thunderbit do ekstrakcji danych z sieci Get Started Free

FAQ

Czy httpx naprawdę nie śledzi przekierowań? Nie domyślnie. Serwer zliczył jedno żądanie dla łańcucha z czterema skokami, a odpowiedź wróciła jako 302. Przekaż follow_redirects=True przy wywołaniu albo ustaw to raz na Client. To zachowanie jest udokumentowane i zamierzone; nadal pozostaje najprostszym powodem cichej awarii przy migracji, bo zamiast wyjątku dostajesz pusty parse.

Czy except requests.exceptions.Timeout naprawdę nie wystarczy? Nie w przypadku serwera, który zawiesza się po wysłaniu nagłówków. Wtedy pojawia się ConnectionError, a to nie jest podklasa Timeout, więc strażnik tego nie złapie — pokazane bezpośrednio, a nie wywnioskowane. Jeśli chcesz łapać oba przypadki, użyj requests.exceptions.RequestException, ale zaakceptuj, że przechwycisz też rzeczy, które timeoutami nie są.

Czy httpx jest szybszy od requests? Nie w praktyczny sposób przy pojedynczym żądaniu — od tego nie jest. Wynik 17,2× z tego testu to pomiar dwudziestu współbieżnych żądań do endpointu z opóźnieniem 0,3 s, czyli pomiar współbieżności. Jeśli Twoje obciążenie jest sekwencyjne, nie spodziewaj się przyspieszenia i wybieraj na podstawie domyślnych ustawień.

Czy potrzebuję dodatku http2? Tylko jeśli chcesz HTTP/2 — a jeśli ustawisz http2=True bez niego, httpx przy tworzeniu Client rzuci ImportError z komunikatem o instalacji httpx[http2]. Bez cichego downgrade’u. Spodziewałem się cichego przejścia na niższy protokół i sprawdziłem to zamiast zgadywać.

Czego tutaj nie testowano? Zachowania proxy, które ma ogromne znaczenie w scrapingu i wymaga własnego harnessa. Retry — httpx nie ma własnej logiki retry, a requests dostaje ją z urllib3, więc uczciwe porównanie to tak naprawdę porównanie dwóch bibliotek retry. Fingerprinting TLS, czyli oś, na którą faktycznie patrzą systemy anty-botowe, a której żadna z tych bibliotek nie rozwiązuje. Streaming i uploadów plików. I wszystko, co dotyczy jednego komputera, jednej wersji Pythona i localhosta w sześciu z ośmiu prób — liczby opóźnień z serwera testowego mówią o projekcie, a nie o Twojej sieci.

Ke
Ke
CTO w Thunderbit | Starszy data scientist i ekspert ML Dzięki prawie dziesięciu latom doświadczenia w uczeniu maszynowym i data science, Ke Shen jest absolwentem Columbia University i byłym starszym data scientistą w Walmart Labs. Dysponując dogłębną, uznaną przez branżowych ekspertów wiedzą w zakresie Python, R, Java i statystyki, dzieli się sprawdzonymi w boju spostrzeżeniami na temat wdrażania złożonych algorytmów AI — od teorii po architekturę gotową do produkcji.
Spis treści
Thunderbit · Agent AI do danych z internetu

Wyciągaj dane z dowolnej strony w 1 klik

Zaufało nam ponad 250 000 użytkowników
dostępny darmowy plan
Od strony do arkusza kalkulacyjnego
Opisz, czego potrzebujesz — Agent AI Thunderbit zbierze to i wyeksportuje do Excela, Google Sheets, Airtable lub Notion. Start jest darmowy.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week