Тайм-аут чтения с использованием либо urllib2, либо любой другой http-библиотеки
У меня есть код для чтения URL-адреса, например:
from urllib2 import Request, urlopen
req = Request(url)
for key, val in headers.items():
req.add_header(key, val)
res = urlopen(req, timeout = timeout)
# This line blocks
content = res.read()
Тайм-аут работает для вызова urlopen(). Но тогда код попадает на вызов res.read(), где я хочу прочитать данные ответа, и тайм-аут там не применяется. Таким образом, вызов чтения может зависать почти навсегда, ожидая данных с сервера. Единственное решение, которое я нашел, это использовать сигнал для прерывания read(), который не подходит для меня, поскольку я использую потоки.
Какие еще существуют опции? Есть ли библиотека HTTP для Python, которая обрабатывает таймауты чтения? Я посмотрел на httplib2 и запросы, и они, похоже, страдают от той же проблемы, что и выше. Я не хочу писать свой собственный неблокирующий сетевой код, используя модуль сокета, потому что я думаю, что для этого уже должна быть библиотека.
Обновление: Ни одно из решений ниже не делает это для меня. Вы сами можете убедиться, что установка времени ожидания сокета или urlopen не влияет на загрузку большого файла:
from urllib2 import urlopen
url = 'http://iso.linuxquestions.org/download/388/7163/http/se.releases.ubuntu.com/ubuntu-12.04.3-desktop-i386.iso'
c = urlopen(url)
c.read()
По крайней мере, в Windows с Python 2.7.3 таймауты полностью игнорируются.
Ответы
Ответ 1
Я нашел в своих тестах (используя описанный метод здесь), что тайм-аут, установленный в вызове urlopen()
, также вызывает вызов read()
:
import urllib2 as u
c = u.urlopen('http://localhost/', timeout=5.0)
s = c.read(1<<20)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.7/socket.py", line 380, in read
data = self._sock.recv(left)
File "/usr/lib/python2.7/httplib.py", line 561, in read
s = self.fp.read(amt)
File "/usr/lib/python2.7/httplib.py", line 1298, in read
return s + self._file.read(amt - len(s))
File "/usr/lib/python2.7/socket.py", line 380, in read
data = self._sock.recv(left)
socket.timeout: timed out
Может быть, это особенность более новых версий? Я использую Python 2.7 на 12.04 Ubuntu прямо из коробки.
Ответ 2
Одним из возможных (несовершенных) решений является установка глобального тайм-аута сокета, более подробно описываемого здесь:
import socket
import urllib2
# timeout in seconds
socket.setdefaulttimeout(10)
# this call to urllib2.urlopen now uses the default timeout
# we have set in the socket module
req = urllib2.Request('http://www.voidspace.org.uk')
response = urllib2.urlopen(req)
Однако это работает только в том случае, если вы хотите глобально изменить тайм-аут для всех пользователей модуля сокета. Я выполняю запрос из задачи Celery, так что это может испортить тайм-ауты для самого рабочего кода сельдерея.
Я был бы рад услышать любые другие решения...
Ответ 3
Невозможно для любой библиотеки сделать это без использования какого-либо асинхронного таймера через потоки или иначе. Причина в том, что параметр timeout
, используемый в httplib
, urllib2
и других библиотеках, устанавливает timeout
в базовый socket
. И то, что это на самом деле делает, объясняется в документации.
SO_RCVTIMEO
Устанавливает значение таймаута, определяющее максимальный период времени, в течение которого функция ввода ожидает завершения. Он принимает структуру timeval с количеством секунд и микросекундами, определяющими ограничение времени ожидания завершения операции ввода. Если операция приема заблокирована в течение этого времени без получения дополнительных данных, она должна возвращаться с частичным счетчиком или errno, установленным на [EAGAIN] или [EWOULDBLOCK], если данные не получены.
Полужирная часть является ключевой. A socket.timeout
создается только, если не был получен ни один бит в течение времени timeout
. Другими словами, это - timeout
между принятыми байтами.
Простая функция с использованием threading.Timer
может быть следующей:
import httplib
import socket
import threading
def download(host, path, timeout = 10):
content = None
http = httplib.HTTPConnection(host)
http.request('GET', path)
response = http.getresponse()
timer = threading.Timer(timeout, http.sock.shutdown, [socket.SHUT_RD])
timer.start()
try:
content = response.read()
except httplib.IncompleteRead:
pass
timer.cancel() # cancel on triggered Timer is safe
http.close()
return content
>>> host = 'releases.ubuntu.com'
>>> content = download(host, '/15.04/ubuntu-15.04-desktop-amd64.iso', 1)
>>> print content is None
True
>>> content = download(host, '/15.04/MD5SUMS', 1)
>>> print content is None
False
Помимо проверки на None
, также можно поймать исключение httplib.IncompleteRead
не внутри функции, а вне ее. Последний случай не будет работать, если HTTP-запрос не имеет заголовка Content-Length
.
Ответ 4
Я ожидаю, что это будет распространенной проблемой, и все же - никаких ответов нигде не найти... Просто построил решение для этого, используя сигнал таймаута:
import urllib2
import socket
timeout = 10
socket.setdefaulttimeout(timeout)
import time
import signal
def timeout_catcher(signum, _):
raise urllib2.URLError("Read timeout")
signal.signal(signal.SIGALRM, timeout_catcher)
def safe_read(url, timeout_time):
signal.setitimer(signal.ITIMER_REAL, timeout_time)
url = 'http://uberdns.eu'
content = urllib2.urlopen(url, timeout=timeout_time).read()
signal.setitimer(signal.ITIMER_REAL, 0)
# you should also catch any exceptions going out of urlopen here,
# set the timer to 0, and pass the exceptions on.
Кредит для сигнальной части решения идет здесь btw: тайна таймера python
Ответ 5
pycurl.TIMEOUT
работает для всего запроса:
#!/usr/bin/env python3
"""Test that pycurl.TIMEOUT does limit the total request timeout."""
import sys
import pycurl
timeout = 2 #NOTE: it does limit both the total *connection* and *read* timeouts
c = pycurl.Curl()
c.setopt(pycurl.CONNECTTIMEOUT, timeout)
c.setopt(pycurl.TIMEOUT, timeout)
c.setopt(pycurl.WRITEFUNCTION, sys.stdout.buffer.write)
c.setopt(pycurl.HEADERFUNCTION, sys.stderr.buffer.write)
c.setopt(pycurl.NOSIGNAL, 1)
c.setopt(pycurl.URL, 'http://localhost:8000')
c.setopt(pycurl.HTTPGET, 1)
c.perform()
Код вызывает ошибку тайм-аута в ~ 2 секунды. Я проверил общий тайм-аут read с сервером, который отправляет ответ в несколько фрагментов с временем, меньшим таймаута между кусками:
$ python -mslow_http_server 1
где slow_http_server.py
:
#!/usr/bin/env python
"""Usage: python -mslow_http_server [<read_timeout>]
Return an http response with *read_timeout* seconds between parts.
"""
import time
try:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer, test
except ImportError: # Python 3
from http.server import BaseHTTPRequestHandler, HTTPServer, test
def SlowRequestHandlerFactory(read_timeout):
class HTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
n = 5
data = b'1\n'
self.send_response(200)
self.send_header("Content-type", "text/plain; charset=utf-8")
self.send_header("Content-Length", n*len(data))
self.end_headers()
for i in range(n):
self.wfile.write(data)
self.wfile.flush()
time.sleep(read_timeout)
return HTTPRequestHandler
if __name__ == "__main__":
import sys
read_timeout = int(sys.argv[1]) if len(sys.argv) > 1 else 5
test(HandlerClass=SlowRequestHandlerFactory(read_timeout),
ServerClass=HTTPServer)
Я тестировал общий время соединения с http://google.com:22222
.
Ответ 6
Это не то поведение, которое я вижу. Я получаю URLError
, когда время вызова заканчивается:
from urllib2 import Request, urlopen
req = Request('http://www.google.com')
res = urlopen(req,timeout=0.000001)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ...
# raise URLError(err)
# urllib2.URLError: <urlopen error timed out>
Вы не можете уловить эту ошибку, а затем не пытаться читать res
?
Когда я пытаюсь использовать res.read()
после этого, я получаю NameError: name 'res' is not defined.
Что-то вроде этого, что вам нужно:
try:
res = urlopen(req,timeout=3.0)
except:
print 'Doh!'
finally:
print 'yay!'
print res.read()
Я полагаю, что способ выполнения таймаута вручную - через multiprocessing
, no? Если задание еще не закончено, вы можете его закончить.
Ответ 7
Любая асинхронная сетевая библиотека должна обеспечивать полный тайм-аут для любой операции ввода-вывода, например здесь пример кода gevent:
#!/usr/bin/env python2
import gevent
import gevent.monkey # $ pip install gevent
gevent.monkey.patch_all()
import urllib2
with gevent.Timeout(2): # enforce total timeout
response = urllib2.urlopen('http://localhost:8000')
encoding = response.headers.getparam('charset')
print response.read().decode(encoding)
И здесь асинхронный эквивалент:
#!/usr/bin/env python3.5
import asyncio
import aiohttp # $ pip install aiohttp
async def fetch_text(url):
response = await aiohttp.get(url)
return await response.text()
text = asyncio.get_event_loop().run_until_complete(
asyncio.wait_for(fetch_text('http://localhost:8000'), timeout=2))
print(text)
Здесь описан тестовый http-сервер.
Ответ 8
Была та же проблема с тайм-аутом сокета в инструкции read. То, что сработало для меня, заключалось в том, чтобы как заклинатель, так и прочитанный внутри заявления о попытке. Надеюсь, это поможет!