几个名词

在分析 Scrapy 如何使用代理之前,我们先明确几个名词(Web 领域):代理(Proxy)、网关(Gateway)和 隧道(Tunnel):

  • 代理 - 它位于客户端和服务器之间,接收所有客户端的 HTTP 请求,并将这些请求转发给服务器(它可 能会对请求进行修改之后再转发)。对用户来说,这样的应用程序就是一个代理,它会代表用户访问目标服务器。

  • 网关 - 它是一种特殊的服务器,作为其他服务的中间实例使用。通常用于将 HTTP 流量转换成其他的协 议。网关接受请求时就好像自己是资源的源端服务器一样。客户端可能并不知道自己正在与一个网关通信。

  • 隧道 - 它会在两条连接之间对原始数据进行盲转发(HTTP CONNECT 方法)的 HTTP 应用程序。HTTP 隧 道通常用来在一条或多条 HTTP 连接上转发非 HTTP 数据,转发时不会窥探数据。

为了方便行文,本文将这三种不同的 Web 组件统一称为代理服务,并将「代理」、「隧道」作为代理服务的 模式使用。

设置请求代理

从 0.8 开始,Scrapy 内建提供了一个 下载中间件 HttpProxyMiddleware。启用了这个中间件后,用户可 以通过系统环境变量 http_proxyhttps_proxy 来为所有 Scrapy 发出的 HTTP 请求设置代理服务,也 可通过 Requestmeta 中添加字段 proxy 的方式为单个请求设置代理服务。

其中,代理服务 URL 格式为: http://username:password@some_proxy_server:port

在 Scrapy 爬虫运行过程中,HttpProxyMiddleWare 中间件优先检查 Request.meta["proxy"] 是否为空。 如果该字段不为空,HttpProxyMiddleware 将此字段值作为代理服务 URL 使用,它会根据代理服务 URL 中 的认证信息构造请求包头 Proxy-Authorization。如果该字段未被设置,Scrapy 使用系统环境变量的设置 为请求选择代理服务:如果是 HTTP 请求,选择环境变量 http_proxy 指定的代理服务;如果是 HTTPS 请 求,选择环境变量 https_proxy 指定的代理服务;当然了,如果环境变量为空,Scrapy 不会为请求使用 任何代理服务。

请求处理流程

scrapy.core.downloader.handlers.DownloadHandlers 负责发起网络请求。它根据请求使用的协议 (即 scheme)从 DOWNLOAD_HANLDERS 配置项中选择一个合适的下载处理类,并使用其实例完成网络请求。

DOWNLOADER = 'scrapy.core.downloader.Downloader'
DOWNLOAD_HANDLERS = {}
DOWNLOAD_HANDLERS_BASE = {
    'data': 'scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler',
    'file': 'scrapy.core.downloader.handlers.file.FileDownloadHandler',
    'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
    'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
    's3': 'scrapy.core.downloader.handlers.s3.S3DownloadHandler',
    'ftp': 'scrapy.core.downloader.handlers.ftp.FTPDownloadHandler',
}

我们将 Scrapy 处理网络请求的代码流程简略摘录如下(执行顺序由上到下):

## scrapy.core.engine.ExecutionEngine
def __init__(self, crawler, spider_closed_callback):
    ...
    downloader_cls = load_object(self.settings["DOWNLOADER"])
    self.downloader = downloader_cls(crawler)
    ...

def _download(self, request, spider):
    ...
    dwld = self.downloader.fetch(request, spider)
    ...

## scrapy.core.downloader.Downloader

def __init__(self, crawler):
    ...
    self.handlers = DownloadHandlers(crawler)

def fetch(self, request, spider):
    ...
    dfd = self.middleware.download(self._enqueue_request, request, spider)
    ...

def _enqueue_request(self, request, spider):
    ...
    self._process_queue(spider, slot):
    ...

def _process_queue(self, spider, slot):
    ...
    while slot.queue and slot.free_transfer_slots() > 0:
        ...
        dfd = self._download(slot, request, spider)
        ...

def _download(self, slot, request, spider):
    dfd = mustbe_deferred(self.handlers.download_request, request, spider)
    ...

## scrapy.core.downloader.handlers.DownloadHandlers
def __init__(self, crawler):
    ...
    handlers = without_none_values(
        crawler.settings.getwithbase("DOWNLOAD_HANDLERS"))
    for scheme, clspath in six.iteritems(handlers):
        self._schemes[scheme] = clspath
    ...

def download_request(self, request, spider):
    scheme = urlparse_cached(request).scheme
    handler = self._get_handler(scheme)
    ...
    return handler.download_request(request, spider)

由上面代码流程可以看出,请求最终会由 DOWNLOAD_HANDLERS 中配置的各种 scheme 对应的处理类处理。 针对 HTTP/HTTPS,Scrapy 提供了 HTTP10DownloadHandlerHTTP11DownloadHandler 两个处理类,分 别实现了 HTTP 1.0 和 HTTP 1.1 协议逻辑。

随着 HTTP 1.0 逐渐淡出互联网,Scrapy 官方文档目前(2018 年)建议使用 HTTP11DownloadHandler 处 理 HTTP 协议。

https://docs.scrapy.org/en/latest/topics/settings.html#downloader-httpclientfactory
HTTP/1.0 is rarely used nowadays so you can safely ignore this setting, unless you use Twisted<11.1, or if you
really want to use HTTP 1.0 and override DOWNLOAD_HANDLDERS_BASE for http(s) scheme accordingly, i.e. to
'scrapy.core.downloader.handlers.http.HTTP10DownloadHandler'.

随后,HTTP11DownloadHandler.download_request 函数将请求委托给 ScrapyAgent 实例的 download_request 方法,这个方法选择使用 AgentProxyAgent 或者 TunnelingAgent 继续处理请求:

## scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler
def download_request(self, request, spider):
    agent = ScrapyAgent(contextFactory=self._contextFactory, pool=self._pool,
        maxsize=getattr(spider, 'download_maxsize', self._default_maxsize),
        warnsize=getattr(spider, 'download_warnsize', self._default_warnsize),
        fail_on_dataloss=self._fail_on_dataloss)
    return agent.download_request(request)

## scrapy.core.downloader.handlers.http11.ScrapyAgent
def download_reqeust(self, request):
    ...
    agent = self._get_agent(request, timeout)
    ...
    d = agent.request(
        method, to_bytes(url, encoding="ascii"), headers, bodyproducer)
    ...
    return d

def _get_agent(self, request, timeout):
    bindaddress = request.meta.get('bindaddress') or self._bindAddress
    proxy = request.meta.get('proxy')
    if proxy:
        _, _, proxyHost, proxyPort, proxyParams = _parse(proxy)
        scheme = _parse(request.url)[0]
        proxyHost = to_unicode(proxyHost)
        omitConnectTunnel = b'noconnect' in proxyParams
        if scheme == b'https' and not omitConnectTunnel:
            proxyConf = (proxyHost, proxyPort,
                         request.headers.get(b'Proxy-Authorization', None))
            return self._TunnelAgent(reactor, proxyConf,
                contextFactory=self._contextFactory, connectTimeout=timeout,
                bindAddress=bindaddress, pool=self._pool)
        else:
            endpoint = TCP4ClientEndpoint(reactor, proxyHost, proxyPort,
                timeout=timeout, bindAddress=bindaddress)
            return self._ProxyAgent(endpoint)
    return self._Agent(reactor, contextFactory=self._contextFactory,
        connectTimeout=timeout, bindAddress=bindaddress, pool=self._pool)

从上述代码可以看出,对请求设置了代理服务后,Scrapy 会根据条件选择使用「代理」或「隧道」模式来处理该 请求。我们根据代码逻辑将选择条件总结如下:

  • 为 HTTPS 请求默认使用 「隧道」 模式;
  • 代理服务 URL 中出现 noconnect 参数时,为 HTTPS 请求使用 「代理」 模式;
  • 为 HTTP 请求全部使用「代理」模式;

几点结论

根据上面的分析,我们得出如下结论:

  • Scrapy 0.8+ 默认启用了 scrapy.downloadmiddlewares.httpproxy.HttpProxyMiddleware 中间件。用户可 以通过环境变量或者 Request.meta['proxy'] 字段为请求指定代理服务。

  • 未显式设置 Request.meta['proxy'] 的请求,Scrapy 根据环境变量 http_proxyhttps_proxy 的 值选择对请求是否启用代理服务和启用哪个代理服务。如果两个环境变量都被设置的情况下:为 HTTP 请求选用 http_proxy 中设置的代理;为 HTTPS 请求选用 https_proxy 中设置的代理。

  • Scrapy 默认为 HTTP 请求使用 「代理」 模式代理服务。

  • Scrapy 默认为 HTTPS 请求使用「隧道」模式代理服务。用户可以通过在代理服务 URL 中添加 noconnect 参数关闭此默认行为。

  • Scrapy 并不要求 https_proxy 指定的代理服务支持 HTTPS 协议,但要求其支持「隧道」模式(即支持 HTTP CONNECT 方法)。

Comments

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

Devel

Tags

Contact