如何在Python 3中处理urllib的超时?

34

首先,我的问题与这个问题非常相似。我想让urllib.urlopen()的超时生成一个异常,以便我可以处理。

这不是URLError吗?

try:
    response = urllib.request.urlopen(url, timeout=10).read().decode('utf-8')
except (HTTPError, URLError) as error:
    logging.error(
        'Data of %s not retrieved because %s\nURL: %s', name, error, url)
else:
    logging.info('Access successful.')

错误信息:

resp = urllib.request.urlopen(req, timeout=10).read().decode('utf-8')
File "/usr/lib/python3.2/urllib/request.py", line 138, in urlopen
return opener.open(url, data, timeout)
File "/usr/lib/python3.2/urllib/request.py", line 369, in open
response = self._open(req, data)
File "/usr/lib/python3.2/urllib/request.py", line 387, in _open
'_open', req)
File "/usr/lib/python3.2/urllib/request.py", line 347, in _call_chain
result = func(*args)
File "/usr/lib/python3.2/urllib/request.py", line 1156, in http_open
return self.do_open(http.client.HTTPConnection, req)
File "/usr/lib/python3.2/urllib/request.py", line 1141, in do_open
r = h.getresponse()
File "/usr/lib/python3.2/http/client.py", line 1046, in getresponse
response.begin()
File "/usr/lib/python3.2/http/client.py", line 346, in begin
version, status, reason = self._read_status()
File "/usr/lib/python3.2/http/client.py", line 308, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
File "/usr/lib/python3.2/socket.py", line 276, in readinto
return self._sock.recv_into(b)
socket.timeout: timed out

Python 3中有一个重大变化,当时他们将urlliburllib2模块重组为了urllib。您是否可能遇到了由此引起的问题?


2
发现异常类型的简单方法是except Exception as e: print(type(e))。假设您可以复现您的异常。 - polvoazul
3个回答

48
使用明确的子句捕获不同的异常,并通过URLError检查异常的原因(感谢Régis B.Daniel Andrzejewski)。
from socket import timeout
from urllib.error import HTTPError, URLError

try:
    response = urllib.request.urlopen(url, timeout=10).read().decode('utf-8')
except HTTPError as error:
    logging.error('HTTP Error: Data of %s not retrieved because %s\nURL: %s', name, error, url)
except URLError as error:
    if isinstance(error.reason, timeout):
        logging.error('Timeout Error: Data of %s not retrieved because %s\nURL: %s', name, error, url)
    else:
        logging.error('URL Error: Data of %s not retrieved because %s\nURL: %s', name, error, url)
else:
    logging.info('Access successful.')

NB 最近的评论中,原始帖子提到了需要使用socket.timeout来显式捕获超时错误的Python 3.2版本。例如


    # Warning - python 3.2 code
    from socket import timeout
    
    try:
        response = urllib.request.urlopen(url, timeout=10).read().decode('utf-8')
    except timeout:
        logging.error('socket timed out - URL %s', url)


2
这是完全不正确的!在Python 3.9中,只捕获第一个异常。也许在3和3.9之间引入了一些变化? - Otheus
1
在Python3中:从urllib.error导入HTTPError、URLError。 - undefined

17

之前的答案没有正确地拦截超时错误。超时错误会被抛出为URLError,所以如果我们想特别捕捉它们,我们需要编写:

from urllib.error import HTTPError, URLError
import socket

try:
    response = urllib.request.urlopen(url, timeout=10).read().decode('utf-8')
except HTTPError as error:
    logging.error('Data not retrieved because %s\nURL: %s', error, url)
except URLError as error:
    if isinstance(error.reason, socket.timeout):
        logging.error('socket timed out - URL %s', url)
    else:
        logging.error('some other error happened)
else:
    logging.info('Access successful.')

请注意,ValueError 可能会单独发生,例如 URL 无效。像 HTTPError 一样,它与超时无关。


3
尽管这段代码是正确的并且捕获了大多数超时错误,但我曾经遇到过一次socket.timeout异常,它没有被这段代码捕获。这只发生了很少的几次尝试中的一次。这是在Python 3.7.2下发生的。**为了更安全起见,总而言之,我也会捕获socket.timeout**。 - Asclepius

0
什么是“超时”?从整体上来看,我认为它的意思是“由于负载过高,服务器未能及时响应,因此值得再次重试的情况”。
HTTP状态504“网关超时”就是这个定义下的超时。它通过HTTPError传递。
HTTP状态码429“太多请求”也是这个定义下的超时。它也通过HTTPError传递。
否则,我们所说的超时是什么意思?我们是否包括在通过DNS解析器解析域名时的超时?在尝试发送数据时发生的超时?等待数据返回时发生的超时?
我不知道如何审计urllib的源代码以确保我可能考虑的每种超时方式都被以我可以捕获的方式引发。在没有检查异常的语言中,我不知道该怎么做。我有一种预感,也许连接到DNS错误可能会返回为socket.timeout,而连接到远程服务器错误可能会作为URLError(socket.timeout)返回?这只是一个可能解释早期观察结果的猜测。

所以我退回到一些非常防御性的编程。 (1) 我正在处理一些指示超时的HTTP状态码。 (2) 有报告称,某些超时是通过socket.timeout异常传递的,而某些是通过URLError(socket.timeout)异常传递的,因此我捕获了两者。 (3) 以防万一,我还加入了HTTPError(socket.timeout)。

while True:
    reason : Optional[str] = None
    try:
        with urllib.request.urlopen(url) as response:
            content = response.read()
            with open(cache,"wb") as file:
                file.write(content)
            return content
    except urllib.error.HTTPError as e:
        if e.code == 429 or e.code == 504: # 429=too many requests, 504=gateway timeout
            reason = f'{e.code} {str(e.reason)}'
        elif isinstance(e.reason, socket.timeout):
            reason = f'HTTPError socket.timeout {e.reason} - {e}'
        else:
            raise
    except urllib.error.URLError as e:
        if isinstance(e.reason, socket.timeout):
            reason = f'URLError socket.timeout {e.reason} - {e}'
        else:
            raise
    except socket.timeout as e:
        reason = f'socket.timeout {e}'
    except:
        raise
    netloc = urllib.parse.urlsplit(url).netloc # e.g. nominatim.openstreetmap.org
    print(f'*** {netloc} {reason}; will retry', file=sys.stderr)
    time.sleep(5)

3
这个答案有六个问号,两个"I don't know"和一个"I think"。应该不太有信心将此代码复制粘贴到自己的程序中。为了测试复制的代码,可能需要很多黑客技术来破解一个工作系统。更糟糕的是,还有一个带有5秒睡眠的无限循环。 - WinEunuuchs2Unix
1
有六个问号,因为我在询问其他答案没有考虑到的问题!我同意不复制+粘贴。相反,您应该编写任何代码,然后验证它是否足够处理我提出的问题,如果不是,则进行调整。如果最终得到与我的代码不同的代码,则应自问原因。(附:很容易审计无限循环仅在504、429、socket.timeout等有限情况下发生,因为这就是我想要的,如果您不想要那样,那么需要更改的内容是清楚的!) - Lucian Wischik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接