使用 urllib2 或任何其他 http 库读取超时

Posted

技术标签:

【中文标题】使用 urllib2 或任何其他 http 库读取超时【英文标题】:Read timeout using either urllib2 or any other http library 【发布时间】:2012-03-21 21:00:40 【问题描述】:

我有读取这样的网址的代码:

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 和请求,它们似乎遇到了与上述相同的问题。我不想使用 socket 模块编写我自己的非阻塞网络代码,因为我认为应该已经有一个库。

更新:以下解决方案都不适合我。下载大文件的时候,你可以自己看看设置socket或者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 上,超时完全被忽略了。

【问题讨论】:

与总连接超时有关:HTTPConnection.request not respecting timeout? 这个问题是否也会影响 Python 3?有没有采取任何措施来解决它?似乎是内置 Python HTTP 库本身的问题。 【参考方案1】:

这不是我看到的行为。当通话超时时,我会收到 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() 调用处挂起,因为服务器返回数据缓慢。这就是需要超时的地方。【参考方案2】:

一个可能的(不完美的)解决方案是设置全局套接字超时,更详细的解释here:

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 工作代码本身的超时。

我很高兴听到任何其他解决方案...

【讨论】:

至少在带有 Python 2.7 的 Windows 上,它对 read() 调用没有影响。 setdefaulttimeout() does not limit the total read timeout 例如,服务器可能每 5 秒发送一个字节,并且永远不会触发超时。【参考方案3】:

我在测试中发现(使用here 描述的技术)在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。

【讨论】:

它可能会触发个别.recv() 调用(可能返回部分数据)但it does not limit the total read timeout (until EOF) 的超时。 是的,澄清是有其价值的。【参考方案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.

解决方案的信号部分的功劳在这里顺便说一句:python timer mystery

【讨论】:

但它会超时read() 调用或urlopen() 调用吗?我想测试这个解决方案,但是很难设置在客户端 recv 调用套接字期间服务器超时的情况。 Bjorn,至于读取与 urlopen - 它使读取和 urlopen 都超时。我用这个 url 对其进行了测试:“uberdns.eu”——至少在昨天,这导致我的爬虫挂起读取。这是我在套接字默认超时和 urlopen 超时都失败的情况下测试和工作的解决方案。 至于线程 - 不知道,你必须检查 setitimer 文档。【参考方案5】:

在读取语句上遇到了同样的套接字超时问题。对我有用的是将 urlopen 和 read 都放在 try 语句中。希望这会有所帮助!

【讨论】:

【参考方案6】:

如果不通过线程或其他方式使用某种异步计时器,任何库都无法做到这一点。原因是httpliburllib2等库中使用的timeout参数在底层socket上设置了timeout。 documentation 解释了这实际上做了什么。

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 标头,则后一种情况将不起作用。

【讨论】:

这里不需要 lambda:Timer(timeout, sock.shutdown, [socket.SHUT_RDWR])。您应该在超时时引发 TimeoutError 而不是返回 None @J.F.Sebastian 是的,这里有很多方法可以发出超时信号,例如引发自定义异常。感谢args 提示。 preferable 方法来表示超时:download() 函数可能会从设置其参数的位置向下埋几个堆栈帧,只能触发超时对于某些站点在某些时间 - 如果内容为无,您希望中​​间函数做什么?如果甚至有一处忘记处理错误返回值;它可能有不良副作用。异常是将错误从检测到的地方传递到知道如何处理的地方的机制。而且默认行为(错误不会被忽略)更加健壮。 顺便说一句,据我所知,您的答案是唯一限制总读取超时的答案(您可能应该将timeout 参数传递给HTTPConnection 以尝试传递给limit the connection timeout too) . class TimeoutError(EnvironmentError): pass 的缺席并不是提倡不良做法的原因。【参考方案7】:

pycurl.TIMEOUT option works for the whole request:

#!/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)

我已经测试过the total connection timeout with http://google.com:22222

【讨论】:

【参考方案8】:

任何异步网络库都应该允许对任何 I/O 操作强制执行总超时,例如,这里是 gevent code example:

#!/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)

这里是asyncio equivalent:

#!/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)

test http server is defined here。

【讨论】:

这很好用(至少 gevent sn-p)。我有一个简单的程序来抓取图像并将其与时间戳一起存储,如果 url 不可用,这可以让程序结束。谢谢!

以上是关于使用 urllib2 或任何其他 http 库读取超时的主要内容,如果未能解决你的问题,请参考以下文章

覆盖 urllib2.HTTPError 或 urllib.error.HTTPError 并读取响应 HTML

使用 Python 读取 HTTP 服务器推送流

Python 标准库 urllib2 的使用细节

在 Python 中从 Paypal 获取访问令牌 - 使用 urllib2 或请求库

Python 标准库 urllib2 的使用

爬虫urllib2库的基本使用