我有读取如下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(),这对我来说不合适,因为我使用的是线程。
还有什么其他选择?有没有一个用于Python的HTTP库来处理读取超时?我查看了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()
至少在Python 2.7.3的Windows上,超时被完全忽略了。
如果不通过线程或其他方式使用某种异步定时器,任何库都不可能做到这一点。原因是httplib
、urllib2
和其他库中使用的timeout
参数将timeout
设置在底层socket
上。文档中解释了它的实际作用。
SO-RCVTIMEO
设置超时值,该值指定输入函数在完成之前等待的最长时间。它接受一个timeval结构,该结构以秒和微秒为单位,指定等待输入操作完成的时间限制。如果接收操作被阻止了这么长时间而没有接收到额外的数据,则如果没有接收到数据,则应返回部分计数或设置为[EAGAIN]或[EWOULDBLOCK]的errno。
粗体部分是关键。只有在timeout
窗口的持续时间内没有接收到单个字节的情况下,才会引发socket.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
标头,则后一种情况将不起作用。
我在测试中发现(使用此处描述的技术)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
也许这是新版本的一个功能?我在12.04 Ubuntu上直接使用Python 2.7。
一个可能的(不完美的)解决方案是设置全局套接字超时,这里有更详细的解释:
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任务中运行请求,所以这样做会使Celery工作程序代码本身超时。
我很乐意听到任何其他解决方案。。。
我认为这是一个常见的问题,但在任何地方都找不到答案。。。刚刚建立了一个使用超时信号的解决方案:
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计时器神秘
任何异步网络库都应该允许强制执行任何I/O操作的总超时,例如,下面的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服务器。
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秒内引发超时错误。我已经用服务器测试了读取的总超时,该服务器在多个块中发送响应,时间小于块之间的超时:
$ 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'1n'
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
测试了连接的总超时。
这不是我看到的行为。当呼叫超时时,我得到一个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
,不是吗?如果作业尚未完成,您可以终止它。
读取语句的套接字超时也有同样的问题。对我有效的是将urlopen和read都放在try语句中。希望这能有所帮助!