跳到主要内容

深入分析 Scrapy 下载器原理

今天我们来完整分析下 Scrapy 中下载器模块的代码,深入理解下载器的实现原理以及用到的 Twisted 相关模块。本节的内容会有些枯燥,请耐心阅读下去。

1. Twisted 中的 Web Client 模块

本小节内容主要参考官方文档对于 Web Client 模块的介绍,也就是文献1。这部分内容正是 Scrapy 下载器的核心,为了能更好的理解下载器,我们需要先学习下 Twisted 中的这块内容。

1.1 发出请求

注意 twisted.web.client.Agent 这个类,它是客户端 API 的入口点,请求是使用 request() 方法发出的,该方法以请求方法、请求URI、请求头和可以生成请求体的对象作为参数。代理负责连接设置。因此,它需要一个 reactor 作为初始值设定项的参数。来看官方给的第一个简单例子:

from __future__ import print_function

from twisted.internet import reactor
from twisted.web.client import Agent
from twisted.web.http_headers import Headers

agent = Agent(reactor)

d = agent.request(
b'GET',
b'http://www.linkdao.cn/wiki/',
Headers({'User-Agent': ['Twisted Web Client Example']}),
None)

def cbResponse(ignored):
print('Response received')
d.addCallback(cbResponse)

# 关闭reactor()
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)

reactor.run()

上述代码简单实例化一个 agent,然后调用 request() 方法请求 http://www.linkdao.cn/wiki/ 这个地址,这个动作也是一个延迟加载的方式;接下来的回调链中还会有请求完成后打印收到响应的方法以及最后关闭 reactor 的方法;执行的结果如下:

[root@server2 scrapy-test]# python3 request.py 
Response received

如果想要给请求带上参数,就需要传递一个 twisted.web.iweb.IBodyProducer 类型的对象到 Agent.request。我们继续来学习官方给出的第二个例子:

下面的代码给出了一个简单的 IBodyProducer 实现,它向使用者写入内存中的字符串

# 代码文件命名为:bytesprod.py 

from zope.interface import implementer

from twisted.internet.defer import succeed
from twisted.web.iweb import IBodyProducer

@implementer(IBodyProducer)
class BytesProducer(object):
def \_\_init\_\_(self, body):
self.body = body
self.length = len(body)

def startProducing(self, consumer):
consumer.write(self.body)
return succeed(None)

def pauseProducing(self):
pass

def stopProducing(self):
pass

下面的代码则在请求中带上了 body 体:

# 代码文件:sendbody.py

from twisted.internet import reactor
from twisted.web.client import Agent
from twisted.web.http_headers import Headers

from bytesprod import BytesProducer

agent = Agent(reactor)
# 构造请求体
body = BytesProducer(b"hello, world")
d = agent.request(
b'POST',
b'http://httpbin.org/post',
Headers({'User-Agent': ['Twisted Web Client Example'],
'Content-Type': ['text/x-greeting']}),
# 带上body
body)

# 回调链,收到上个request的请求响应
def cbResponse(ignored):
print('Response received')
d.addCallback(cbResponse)

# 关闭reactor
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)

reactor.run()

1.2 接收响应

接下来一个内容就是关于数据的接收。前面的代码都只有请求,没有接收响应数据。如果 Agent.request 请求成功,则 Deferred 将触发一个响应。一旦收到所有响应头,就会发生这种情况。它发生在处理任何响应体 (如果有)之前。Response 对象有一个使响应体可用的方法:deliverBody,接下来我们给出一个使用实例:

from __future__ import print_function

from pprint import pformat

from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.internet.protocol import Protocol
from twisted.web.client import Agent
from twisted.web.http_headers import Headers

# 继承Protocol
class BeginningPrinter(Protocol):
def \_\_init\_\_(self, finished):
self.finished = finished
self.remaining = 1024 \* 10

def dataReceived(self, bytes):
"""
响应的数据从该方法获取,最终获取的数据大小不超过self.remaining
"""
if self.remaining:
display = bytes[:self.remaining]
print('Some data received:')
print(str(display, encoding='utf8'))
self.remaining -= len(display)

def connectionLost(self, reason):
print('Finished receiving body:', reason.getErrorMessage())
self.finished.callback(None)

# 获取agent实例,传入reactor
agent = Agent(reactor)
# 请求慕课网wiki
d = agent.request(
b'GET',
b'http://www.linkdao.cn/wiki/',
Headers({'User-Agent': ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36']}),
None)

def cbRequest(response):
finished = Deferred()
# 获取响应数据
response.deliverBody(BeginningPrinter(finished))
return finished
# 加入回调链
d.addCallback(cbRequest)

def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)

reactor.run()

我们可以直接来看看这个: