Python Scrapy - 基于MIME类型的过滤器,以避免下载非文本文件

6

我有一个正在运行的scrapy项目,但它消耗带宽很大,因为它试图下载许多二进制文件(zip、tar、mp3等)。

我认为最好的解决方案是根据mimetype(Content-Type:) HTTP头过滤请求。我查看了scrapy代码,并找到了这个设置:

DOWNLOADER_HTTPCLIENTFACTORY = 'scrapy.core.downloader.webclient.ScrapyHTTPClientFactory'

我把它改为: DOWNLOADER_HTTPCLIENTFACTORY = 'myproject.webclients.ScrapyHTTPClientFactory'
并对ScrapyHTTPPageGetter进行了一些修改,这里是修改的部分:
class ScrapyHTTPPageGetter(HTTPClient):
    # this is my edit
    def handleEndHeaders(self):
        if 'Content-Type' in self.headers.keys():
            mimetype = str(self.headers['Content-Type'])
            # Actually I need only the html, but just in 
            # case I've preserved all the text
            if mimetype.find('text/') > -1: 
                # Good, this page is needed
                self.factory.gotHeaders(self.headers)
            else:
                self.factory.noPage(Exception('Incorrect Content-Type'))

我觉得这样做是错误的,我需要更多Scrapy友好的方式来在确定不需要的MIME类型后立即取消/丢弃请求。而不是等待整个数据下载完毕。
编辑: 我特别询问这一部分代码self.factory.noPage(Exception('Incorrect Content-Type'))是否是取消请求的正确方式。
更新1: 我的当前设置已经导致Scrapy服务器崩溃,请不要尝试使用上面的代码来解决问题。
更新2: 我已经建立了一个基于Apache的测试网站,使用以下结构:
/var/www/scrapper-test/Zend -> /var/www/scrapper-test/Zend.zip (symlink)
/var/www/scrapper-test/Zend.zip

我注意到Scrapy会放弃带有.zip扩展名的文件,但是会获取没有.zip扩展名的文件,即使它只是一个指向该文件的符号链接。

1
这真的不起作用吗?handleEndHeaders应该在下载正文之前被调用。 - fmoo
@fmoo 请查看修改后的内容,我已经更加具体了。 - Omar Al-Ithawi
我的当前设置已经导致了Scrapy服务器崩溃,请不要尝试使用上述相同的代码来解决问题。 - Omar Al-Ithawi
3个回答

11

我编写了这个中间件,用于排除不在正则表达式白名单中的响应类型:

from scrapy.http.response.html import HtmlResponse
from scrapy.exceptions import IgnoreRequest
from scrapy import log
import re

class FilterResponses(object):
    """Limit the HTTP response types that Scrapy dowloads."""

    @staticmethod
    def is_valid_response(type_whitelist, content_type_header):
        for type_regex in type_whitelist:
            if re.search(type_regex, content_type_header):
                return True
        return False

    def process_response(self, request, response, spider):
        """
        Only allow HTTP response types that that match the given list of 
        filtering regexs
        """
        # each spider must define the variable response_type_whitelist as an
        # iterable of regular expressions. ex. (r'text', )
        type_whitelist = getattr(spider, "response_type_whitelist", None)
        content_type_header = response.headers.get('content-type', None)
        if not type_whitelist:
            return response
        elif not content_type_header:
            log.msg("no content type header: {}".format(response.url), level=log.DEBUG, spider=spider)
            raise IgnoreRequest()
        elif self.is_valid_response(type_whitelist, content_type_header):
            log.msg("valid response {}".format(response.url), level=log.DEBUG, spider=spider)
            return response
        else:
            msg = "Ignoring request {}, content-type was not in whitelist".format(response.url)
            log.msg(msg, level=log.DEBUG, spider=spider)
            raise IgnoreRequest()

要使用它,请将其添加到settings.py中:

DOWNLOADER_MIDDLEWARES = {
    '[project_name].middlewares.FilterResponses': 999,
}

1
谢谢。这会在下载前还是后取消文件?即,如果一个文件大小为30GB,它会立即在下载头文件后取消,还是必须等待完整的响应。 - Omar Al-Ithawi
首先请注意我刚刚纠正的错误。(我必须编辑几次,因为我搞糊涂了markdown)。是的,根据我在Scrapy执行期间将日志消息投入此中间件的实验,这将在文件下载之前停止响应。 - saxman01
太好了。您介意将下面的答案标记为已采纳吗?(而不是您的回答)? - Omar Al-Ithawi
2
这只是虚假的网络积分。为了未来的谷歌搜索者,请选择你认为最好的答案来回答你的问题。 - saxman01
process_response?这应该是在文件下载完成后挂钩,或者最好是在下载开始时。只需使用process_request即可,参见https://dev59.com/jGfWa4cB1Zd3GeqPj7tf - Christian Pao.

0
也许现在有点晚了。你可以使用 Accept 头来过滤你要查找的数据。

不是这样的,我还在寻找答案。如果服务器理解或忽略“Accept:”头部呢? - Omar Al-Ithawi
也许这是一个错误的想法。你可以将下载过程分为两个阶段:1. 使用HEADER方法发送请求,2. 如果内容类型正常,则使用Get方法发送请求。祝好。 - Badarau Petru
我目前正在尝试使用下载器中间件以Scrapy友好的方式正确地完成它。但是将请求分成两个阶段将会使Scrapy设置变得复杂。我宁愿放置一个Web代理服务器并执行过滤逻辑,而不是搞乱python-twisted。 - Omar Al-Ithawi
我有另一个想法。您可以使用process_response方法定义中间件类,该方法将在Downloader类中验证响应,并在必要时生成新请求。您必须将此中间件类附加到default_settings.py的DOWNLOADER_MIDDLEWARES_BASE字典中,并设置值大于1000。 - Badarau Petru

-1

解决方案是设置一个Node.js代理,并通过http_proxy环境变量配置Scrapy使用它。

代理应该做的是:

  • 接收来自Scrapy的HTTP请求并将其发送到正在爬取的服务器。然后将响应返回给Scrapy,即拦截所有HTTP流量。
  • 对于二进制文件(基于您实现的启发式算法),它向Scrapy发送403 Forbidden错误并立即关闭请求/响应。这有助于节省时间、流量和避免Scrapy崩溃。

示例代理代码

真正可行的代码!

http.createServer(function(clientReq, clientRes) {
    var options = {
        host: clientReq.headers['host'],
        port: 80,
        path: clientReq.url,
        method: clientReq.method,
        headers: clientReq.headers
    };


    var fullUrl = clientReq.headers['host'] + clientReq.url;
    
    var proxyReq = http.request(options, function(proxyRes) {
        var contentType = proxyRes.headers['content-type'] || '';
        if (!contentType.startsWith('text/')) {
            proxyRes.destroy();            
            var httpForbidden = 403;
            clientRes.writeHead(httpForbidden);
            clientRes.write('Binary download is disabled.');
            clientRes.end();
        }
        
        clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
        proxyRes.pipe(clientRes);
    });

    proxyReq.on('error', function(e) {
        console.log('problem with clientReq: ' + e.message);
    });

    proxyReq.end();
  
}).listen(8080);

2
有没有在Python Twisted或下载器扩展中实现此过滤器的选项? - Frederic Bazin

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