使用httplib时出现IncompleteRead问题

26

我一直遇到一个问题,就是无法从某个网站获取RSS订阅源。我写了一个相当丑陋的程序来执行此功能,但我很好奇为什么会出现这种情况,是否有更高级别的接口可以正确处理此问题。虽然我不需要经常检索该Feed,但这个问题并不是完全可以忽略的。

我读到一种解决方案,它捕获异常并返回部分内容,但由于未完成的读取返回的字节数不同,我不能确定这种解决方案实际上是否有效。

#!/usr/bin/env python
import os
import sys
import feedparser
from mechanize import Browser
import requests
import urllib2
from httplib import IncompleteRead

url = 'http://hattiesburg.legistar.com/Feed.ashx?M=Calendar&ID=543375&GUID=83d4a09c-6b40-4300-a04b-f88884048d49&Mode=2013&Title=City+of+Hattiesburg%2c+MS+-+Calendar+(2013)'

content = feedparser.parse(url)
if 'bozo_exception' in content:
    print content['bozo_exception']
else:
    print "Success!!"
    sys.exit(0)

print "If you see this, please tell me what happened."

# try using mechanize
b = Browser()
r = b.open(url)
try:
    r.read()
except IncompleteRead, e:
    print "IncompleteRead using mechanize", e

# try using urllib2
r = urllib2.urlopen(url)
try:
    r.read()
except IncompleteRead, e:
    print "IncompleteRead using urllib2", e


# try using requests
try:
    r = requests.request('GET', url)
except IncompleteRead, e:
    print "IncompleteRead using requests", e

# this function is old and I categorized it as ...
# "at least it works darnnit!", but I would really like to 
# learn what's happening.  Please help me put this function into
# eternal rest.
def get_rss_feed(url):
    response = urllib2.urlopen(url)
    read_it = True
    content = ''
    while read_it:
        try:
            content += response.read(1)
        except IncompleteRead:
            read_it = False
    return content, response.info()


content, info = get_rss_feed(url)

feed = feedparser.parse(content)

如先前所述,这并不是一个关键任务的问题,而是一个好奇心问题。尽管我可以预料到urllib2会出现这个问题,但我对在mechanize和requests中遇到这个错误感到惊讶。feedparser模块甚至不会抛出错误,因此检查错误取决于'bozo_exception'键是否存在。

编辑:我只想提一下,无论何时wget和curl都能完美地执行该功能,正确地检索完整有效载荷。除了我的丑陋文章之外,我还没有找到纯python方法可用,并且我非常好奇httplib后台发生了什么。 几天前,我心血来潮决定也用twill尝试了一下,结果得到了相同的httplib错误。

P.S. 还有一件事情让我感到非常奇怪。 IncompleteRead在有效载荷的两个断点之一始终发生。 看起来feedparser和requests在读取926字节后失败,而mechanize和urllib2在读取1854字节后失败。 这种行为很一致,我无法解释或理解。

3个回答

26

最终,所有其他模块(feedparsermechanizeurllib2)都调用httplib,这就是异常被抛出的地方。

现在,首先我使用wget下载了这个文件,结果文件大小为1854字节。接下来,我尝试使用urllib2

>>> import urllib2
>>> url = 'http://hattiesburg.legistar.com/Feed.ashx?M=Calendar&ID=543375&GUID=83d4a09c-6b40-4300-a04b-f88884048d49&Mode=2013&Title=City+of+Hattiesburg%2c+MS+-+Calendar+(2013)'
>>> f = urllib2.urlopen(url)
>>> f.headers.headers
['Cache-Control: private\r\n',
 'Content-Type: text/xml; charset=utf-8\r\n',
 'Server: Microsoft-IIS/7.5\r\n',
 'X-AspNet-Version: 4.0.30319\r\n',
 'X-Powered-By: ASP.NET\r\n',
 'Date: Mon, 07 Jan 2013 23:21:51 GMT\r\n',
 'Via: 1.1 BC1-ACLD\r\n',
 'Transfer-Encoding: chunked\r\n',
 'Connection: close\r\n']
>>> f.read()
< Full traceback cut >
IncompleteRead: IncompleteRead(1854 bytes read)

所以它读取了全部1854个字节,但是认为还有更多要读取的内容。如果我们明确告诉它只读取1854个字节,它就可以正常工作:

>>> f = urllib2.urlopen(url)
>>> f.read(1854)
'\xef\xbb\xbf<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">...snip...</rss>'

显然,只有在我们始终知道确切的长度时,这才有用。我们可以利用部分读取作为异常属性返回的事实来捕获整个内容:

>>> try:
...     contents = f.read()
... except httplib.IncompleteRead as e:
...     contents = e.partial
...
>>> print contents
'\xef\xbb\xbf<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">...snip...</rss>'

这篇博客文章指出了这是服务器的问题,并描述如何使用上文中的try..except块来对httplib.HTTPResponse.read()方法进行猴子补丁处理:

import httplib

def patch_http_response_read(func):
    def inner(*args):
        try:
            return func(*args)
        except httplib.IncompleteRead, e:
            return e.partial

    return inner

httplib.HTTPResponse.read = patch_http_response_read(httplib.HTTPResponse.read)

我应用了这个补丁,之后feedparser就可以工作了:

>>> import feedparser
>>> url = 'http://hattiesburg.legistar.com/Feed.ashx?M=Calendar&ID=543375&GUID=83d4a09c-6b40-4300-a04b-f88884048d49&Mode=2013&Title=City+of+Hattiesburg%2c+MS+-+Calendar+(2013)'
>>> feedparser.parse(url)
{'bozo': 0,
 'encoding': 'utf-8',
 'entries': ...
 'status': 200,
 'version': 'rss20'}

这种方式可能不是最好的,但它似乎可以工作。我对HTTP协议并不是足够专业,不能确定服务器是否做错了事情,或者httplib是否处理边缘情况不当。


虽然我同意这不是一种好的做法,但肯定比我之前用的方法好多了(我真的需要更频繁地练习使用装饰器)。我也不是HTTP协议的专家,也不知道httplib是否正确处理了这个问题,所以我觉得这可能是一个好问题需要问。顺便说一下,在这个网站上,除了访问rss url时在他们的http服务器上出现了这个问题,其他页面都很好用。 - umeboshi
@umeboshi - 也许与响应的内容类型有关,即服务器配置的方式。text/html响应正常工作,但text/xml不行?如果没有更全面的答案出现,您可以尝试将此问题发布到Python邮件列表中,看看是否有人能够给出诊断。 - Blair

7

我发现在我的情况下,发送一个HTTP/1.0请求,修复了问题,只需将以下内容添加到代码中即可:

import httplib
httplib.HTTPConnection._http_vsn = 10
httplib.HTTPConnection._http_vsn_str = 'HTTP/1.0'

在我发出请求后:

req = urllib2.Request(url, post, headers)
filedescriptor = urllib2.urlopen(req)
img = filedescriptor.read()

如果连接支持HTTP 1.1,当我回到HTTP 1.1时:

httplib.HTTPConnection._http_vsn = 11
httplib.HTTPConnection._http_vsn_str = 'HTTP/1.1'

也对我有用!非常感谢! 你有任何想法为什么会发生这种情况吗?1.0 版本中未完成读取有什么特别之处吗? - Alexander Dzyoba
你强制使用旧的连接类型,你强制不使用 HTTP 1.1 的能力,这样类似分块读取的操作,在尝试下载较大文件时经常会发生... - Sérgio
并非所有服务器都接受http 1.0 - 我从其中一个服务器收到了404错误。 - Vitaly Zdanevich

0

我已经通过使用HTTPS而不是HTTP来解决了问题,现在它可以正常工作。没有需要更改的代码。


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