深入分析 Scrapy 下載器原理
今天我們來完整分析下 Scrapy 中下載器模塊的代碼,深入理解下載器的實(shí)現(xiàn)原理以及用到的 Twisted 相關(guān)模塊。本節(jié)的內(nèi)容會(huì)有些枯燥,請(qǐng)耐心閱讀下去。
1. Twisted 中的 Web Client 模塊
本小節(jié)內(nèi)容主要參考官方文檔對(duì)于 Web Client 模塊的介紹,也就是文獻(xiàn)1。這部分內(nèi)容正是 Scrapy 下載器的核心,為了能更好的理解下載器,我們需要先學(xué)習(xí)下 Twisted 中的這塊內(nèi)容。
1.1 發(fā)出請(qǐng)求
注意 twisted.web.client.Agent
這個(gè)類,它是客戶端 API 的入口點(diǎn),請(qǐng)求是使用 request()
方法發(fā)出的,該方法以請(qǐng)求方法、請(qǐng)求URI、請(qǐng)求頭和可以生成請(qǐng)求體的對(duì)象作為參數(shù)。代理負(fù)責(zé)連接設(shè)置。因此,它需要一個(gè) reactor 作為初始值設(shè)定項(xiàng)的參數(shù)。來看官方給的第一個(gè)簡單例子:
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://idcbgp.cn/wiki/',
Headers({'User-Agent': ['Twisted Web Client Example']}),
None)
def cbResponse(ignored):
print('Response received')
d.addCallback(cbResponse)
# 關(guān)閉reactor()
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
上述代碼簡單實(shí)例化一個(gè) agent,然后調(diào)用 request()
方法請(qǐng)求 http://idcbgp.cn/wiki/
這個(gè)地址,這個(gè)動(dòng)作也是一個(gè)延遲加載的方式;接下來的回調(diào)鏈中還會(huì)有請(qǐng)求完成后打印收到響應(yīng)的方法以及最后關(guān)閉 reactor 的方法;執(zhí)行的結(jié)果如下:
[root@server2 scrapy-test]# python3 request.py
Response received
如果想要給請(qǐng)求帶上參數(shù),就需要傳遞一個(gè) twisted.web.iweb.IBodyProducer 類型的對(duì)象到 Agent.request。我們繼續(xù)來學(xué)習(xí)官方給出的第二個(gè)例子:
下面的代碼給出了一個(gè)簡單的 IBodyProducer 實(shí)現(xiàn),它向使用者寫入內(nèi)存中的字符串
# 代碼文件命名為: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
下面的代碼則在請(qǐng)求中帶上了 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)
# 構(gòu)造請(qǐng)求體
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)
# 回調(diào)鏈,收到上個(gè)request的請(qǐng)求響應(yīng)
def cbResponse(ignored):
print('Response received')
d.addCallback(cbResponse)
# 關(guān)閉reactor
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
1.2 接收響應(yīng)
接下來一個(gè)內(nèi)容就是關(guān)于數(shù)據(jù)的接收。前面的代碼都只有請(qǐng)求,沒有接收響應(yīng)數(shù)據(jù)。如果 Agent.request 請(qǐng)求成功,則 Deferred 將觸發(fā)一個(gè)響應(yīng)。一旦收到所有響應(yīng)頭,就會(huì)發(fā)生這種情況。它發(fā)生在處理任何響應(yīng)體 (如果有)之前。Response 對(duì)象有一個(gè)使響應(yīng)體可用的方法:deliverBody,接下來我們給出一個(gè)使用實(shí)例:
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):
"""
響應(yīng)的數(shù)據(jù)從該方法獲取,最終獲取的數(shù)據(jù)大小不超過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實(shí)例,傳入reactor
agent = Agent(reactor)
# 請(qǐng)求慕課網(wǎng)wiki
d = agent.request(
b'GET',
b'http://idcbgp.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()
# 獲取響應(yīng)數(shù)據(jù)
response.deliverBody(BeginningPrinter(finished))
return finished
# 加入回調(diào)鏈
d.addCallback(cbRequest)
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
我們可以直接來看看這個(gè):
其實(shí)仔細(xì)分析 Scrapy 下載器源碼我們可以在 scrapy/core/downloader/handlers/http11.py
的代碼中看到如下非常類似的代碼:
2. Scrapy 中下載器模塊源碼分析
2.1 下載器的__init__() 方法分析
我們直接看源碼目錄,關(guān)于下載器模塊的代碼全在 scrapy/core/downloader
目錄下:
其中主要的核心代碼就是這個(gè) __init__.py
文件,其中定義了 Downloader
這個(gè)類,它就是我們的下載器。首先來看這個(gè)類的初始化過程:
# ...
class Downloader:
DOWNLOAD_SLOT = 'download_slot'
def __init__(self, crawler):
# 獲取配置
self.settings = crawler.settings
self.signals = crawler.signals
self.slots = {}
self.active = set()
# 獲取handlers
self.handlers = DownloadHandlers(crawler)
# 獲取配置中的請(qǐng)求并發(fā)數(shù)
self.total_concurrency = self.settings.getint('CONCURRENT_REQUESTS')
# 每個(gè)域的請(qǐng)求并發(fā)數(shù)
self.domain_concurrency = self.settings.getint('CONCURRENT_REQUESTS_PER_DOMAIN')
# ip并發(fā)請(qǐng)求數(shù)
self.ip_concurrency = self.settings.getint('CONCURRENT_REQUESTS_PER_IP')
# 是否設(shè)置隨機(jī)延遲
self.randomize_delay = self.settings.getbool('RANDOMIZE_DOWNLOAD_DELAY')
# 中間件管理器
self.middleware = DownloaderMiddlewareManager.from_crawler(crawler)
self._slot_gc_loop = task.LoopingCall(self._slot_gc)
self._slot_gc_loop.start(60)
從這個(gè)初始化的過程中,我們要重點(diǎn)關(guān)注幾個(gè)屬性值:
-
settings:對(duì)應(yīng)的 scrapy 項(xiàng)目配置;
-
handlers:實(shí)例化 DownloadHandlers, 而這個(gè)類則是網(wǎng)頁下載的最終處理類;
-
各種配置屬性:從 settings.py 中獲取控制并發(fā)的參數(shù);
-
middleware:下載中間件的管理器,在下一節(jié)中我們會(huì)詳細(xì)分析這個(gè)屬性;
2.2 下載器實(shí)例的 handlers 屬性值分析
重點(diǎn)先看看這個(gè) handlers
屬性值,它是 DownloadHandlers
類的一個(gè)實(shí)例,對(duì)應(yīng)的代碼位置為:scrapy/core/downloader/handlers/__init__.py
。其類定義如下:
# ...
class DownloadHandlers:
def __init__(self, crawler):
self._crawler = crawler
self._schemes = {} # stores acceptable schemes on instancing
self._handlers = {} # stores instanced handlers for schemes
self._notconfigured = {} # remembers failed handlers
handlers = without_none_values(
crawler.settings.getwithbase('DOWNLOAD_HANDLERS'))
for scheme, clspath in handlers.items():
self._schemes[scheme] = clspath
self._load_handler(scheme, skip_lazy=True)
crawler.signals.connect(self._close, signals.engine_stopped)
def _get_handler(self, scheme):
"""Lazy-load the downloadhandler for a scheme
only on the first request for that scheme.
"""
if scheme in self._handlers:
return self._handlers[scheme]
if scheme in self._notconfigured:
return None
if scheme not in self._schemes:
self._notconfigured[scheme] = 'no handler available for that scheme'
return None
return self._load_handler(scheme)
def _load_handler(self, scheme, skip_lazy=False):
path = self._schemes[scheme]
try:
dhcls = load_object(path)
if skip_lazy and getattr(dhcls, 'lazy', True):
return None
dh = create_instance(
objcls=dhcls,
settings=self._crawler.settings,
crawler=self._crawler,
)
except NotConfigured as ex:
self._notconfigured[scheme] = str(ex)
return None
except Exception as ex:
# ...
else:
self._handlers[scheme] = dh
return dh
def download_request(self, request, spider):
scheme = urlparse_cached(request).scheme
handler = self._get_handler(scheme)
if not handler:
raise NotSupported("Unsupported URL scheme '%s': %s" %
(scheme, self._notconfigured[scheme]))
return handler.download_request(request, spider)
@defer.inlineCallbacks
def _close(self, *_a, **_kw):
# ...
我們分別來解析這個(gè)類中定義的方法,都是非常重要的。此外,每個(gè)方法含義明確,而且代碼精煉,我們對(duì)此一一進(jìn)行說明。
首先是 __init__()
方法,我們可以看到一個(gè)核心的語句:handlers = without_none_values(crawler.settings.getwithbase('DOWNLOAD_HANDLERS'))
。其中 getwithbase()
方法的定義位于 scapy/settings/__init__.py
文件中,其內(nèi)容如下:
# 源碼位置:scrapy/settings/__init__.py
# ...
class BaseSettings(MutableMapping):
# ...
def getwithbase(self, name):
"""Get a composition of a dictionary-like setting and its `_BASE`
counterpart.
:param name: name of the dictionary-like setting
:type name: string
"""
compbs = BaseSettings()
compbs.update(self[name + '_BASE'])
compbs.update(self[name])
return compbs
# ...
從上面的代碼中,可以知道 crawler.settings.getwithbase('DOWNLOAD_HANDLERS')
語句其實(shí)是獲取了 settings.py 配置中的 DOWNLOAD_HANDLERS
和 DOWNLOAD_HANDLERS_BASE
值,并返回包含這兩個(gè)屬性值的 BaseSettings 實(shí)例。我們從默認(rèn)的 scrapy/settings/default_settings.py
中可以看到這兩個(gè)屬性值如下:
# 源碼位置:scrapy/settings/default_settings.py
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',
}
通常我們項(xiàng)目中的下載請(qǐng)求一般是 http 或者 https,對(duì)應(yīng)的都是 scrapy.core.downloader.handlers.http.HTTPDownloadHandler
這個(gè)位置的 handler,從這里也可以看到 scrapy 框架其實(shí)是支持非常多協(xié)議下載的,比如 s3 下載、文件下載以及 ftp下載等。緊接著的兩句就是在設(shè)置 self._schemes
和 self._handlers
的值。其中 self._handlers
會(huì)加載對(duì)應(yīng) Handler 類:
def _load_handler(self, scheme, skip_lazy=False):
# 獲取協(xié)議對(duì)應(yīng)的handler類路徑,比如 ftp協(xié)議對(duì)應(yīng)著的handler類為
# scrapy.core.downloader.handlers.ftp.FTPDownloadHandler
path = self._schemes[scheme]
try:
# 獲取相應(yīng)的handler類,非字符串形式
dhcls = load_object(path)
# 默認(rèn)__init__()方法調(diào)用時(shí)參數(shù)skip_lazy為True,我們需要檢查對(duì)應(yīng)handler類中的lazy屬性值,沒有設(shè)置時(shí)默認(rèn)為True;如果handler類中l(wèi)azy屬性設(shè)置為True或者不設(shè)置,則該handler不會(huì)加入到self._handlers中
if skip_lazy and getattr(dhcls, 'lazy', True):
return None
# 創(chuàng)建相應(yīng)的handler類實(shí)例,需要的參數(shù)為:
dh = create_instance(
objcls=dhcls,
settings=self._crawler.settings,
crawler=self._crawler,
)
except NotConfigured as ex:
# ...
except Exception as ex:
# ...
else:
# 設(shè)置self._handlers
self._handlers[scheme] = dh
return dh
上面的注釋已經(jīng)非常清楚,在眾多 handlers 中,只有 S3DownloadHandler
類中沒有設(shè)置 lazy。所以在 __init__() 方法執(zhí)行完后,self._handlers
中不會(huì)有鍵 “s3” 及其對(duì)應(yīng)的實(shí)例。
_get_handler()
就非常明顯了,由于我們?cè)诔跏蓟椒?__init__()
中已經(jīng)得到了 self._handlers
,此時(shí)該方法就是根據(jù)傳入的協(xié)議獲取相應(yīng) Handler 類的實(shí)例。 例如傳入的 scheme="http"
,則返回就是 HTTPDownloadHandler
類的一個(gè)實(shí)例。
最后一個(gè) download_request()
方法可以說是下載網(wǎng)頁的核心調(diào)用方法。我們來逐步分析該方法中的語句:
scheme = urlparse_cached(request).scheme
handler = self._get_handler(scheme)
上面兩句是獲取對(duì)應(yīng)的下載請(qǐng)求的 Handler 實(shí)例,比較容易理解。接下來的 return 就是調(diào)用 handler 實(shí)例中的 download_request()
方法按照對(duì)應(yīng)協(xié)議方式下載請(qǐng)求數(shù)據(jù):
return handler.download_request(request, spider)
每個(gè) handler 類都會(huì)有對(duì)應(yīng)的 download_request()
方法。我們重點(diǎn)看看 http
和 https
協(xié)議對(duì)應(yīng)的 handler 類:
# 源碼位置:scrapy/core/downloader/handlers/http.py
from scrapy.core.downloader.handlers.http10 import HTTP10DownloadHandler
from scrapy.core.downloader.handlers.http11 import (
HTTP11DownloadHandler as HTTPDownloadHandler,
)
可以看到,這里的 HTTPDownloadHandler
類實(shí)際上是 http11.py
中的 HTTP11DownloadHandler
類。這里面內(nèi)容有點(diǎn)多,我們簡要地分析下:
# 源碼位置:scrapy/core/downloader/handlers/http11.py
# ...
class HTTP11DownloadHandler:
# ...
def download_request(self, request, spider):
"""Return a deferred for the HTTP download"""
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,
crawler=self._crawler,
)
return agent.download_request(request)
# ...
是不是挺簡單的兩條語句:得到 agent,然后調(diào)用 agent.download_request()
方法得到請(qǐng)求的結(jié)果?我們繼續(xù)追蹤這個(gè) ScrapyAgent 類:
# 源碼位置:scrapy/core/downloader/handlers/http11.py
# ...
from twisted.web.client import Agent, HTTPConnectionPool, ResponseDone, ResponseFailed, URI
# ...
class ScrapyAgent:
_Agent = Agent
# ...
def _get_agent(self, request, timeout):
from twisted.internet import reactor
bindaddress = request.meta.get('bindaddress') or self._bindAddress
# 從請(qǐng)求的meta參數(shù)中獲取proxy
proxy = request.meta.get('proxy')
if proxy:
# 有代理的情況
# ...
# 沒有代理返回Agent的一個(gè)實(shí)例
return self._Agent(
reactor=reactor,
contextFactory=self._contextFactory,
connectTimeout=timeout,
bindAddress=bindaddress,
pool=self._pool,
)
def download_request(self, request):
from twisted.internet import reactor
# 從meta中獲取下載超時(shí)時(shí)間
timeout = request.meta.get('download_timeout') or self._connectTimeout
# 獲取agent
agent = self._get_agent(request, timeout)
# request details
url = urldefrag(request.url)[0]
method = to_bytes(request.method)
headers = TxHeaders(request.headers)
if isinstance(agent, self._TunnelingAgent):
headers.removeHeader(b'Proxy-Authorization')
if request.body:
bodyproducer = _RequestBodyProducer(request.body)
else:
bodyproducer = None
start_time = time()
# 使用agent發(fā)起請(qǐng)求
d = agent.request(method, to_bytes(url, encoding='ascii'), headers, bodyproducer)
# set download latency
d.addCallback(self._cb_latency, request, start_time)
# response body is ready to be consumed
d.addCallback(self._cb_bodyready, request)
d.addCallback(self._cb_bodydone, request, url)
# check download timeout
self._timeout_cl = reactor.callLater(timeout, d.cancel)
d.addBoth(self._cb_timeout, request, url, timeout)
return d
看完上面的代碼就應(yīng)該比較清楚了,我們先不考慮代理的相關(guān)代碼。直接看 _get_agent()
方法就是獲取 twisted 模塊中 Agent 的一個(gè)實(shí)例,然后通過這個(gè) agent 去請(qǐng)求網(wǎng)頁 (在 download_request()
方法中完成 ),最后返回的仍然是一個(gè) Deferred 對(duì)象。在 download_request()
中的代碼就和我們?cè)诘谝恍」?jié)中介紹的類似,這里便是 Scrapy 最后完成網(wǎng)頁抓取的地方,就是基于 Twisted 的 web client 部分的方法。其中,最核心的一行語句就是:
d = agent.request(method, to_bytes(url, encoding='ascii'), headers, bodyproducer)
這個(gè)分析已經(jīng)走到了 Scrapy 下載器的最后一層,在往下就是研究 Twisted 框架的源碼了。同學(xué)們有興趣的話,可以繼續(xù)追蹤下去,對(duì)本節(jié)課而言,追蹤到此已經(jīng)結(jié)束了。我們跳出 handlers,繼續(xù)研究 Downloader
類。
2.3 下載器的 _download() 分析
我們回到 Downloader
類上繼續(xù)學(xué)習(xí),該下載器類中最核心的有如下三個(gè)方法:
_enqueue_request()
:請(qǐng)求入隊(duì);_process_queue()
:處理隊(duì)列中的請(qǐng)求;_download()
:下載網(wǎng)頁;
從代碼中很明顯可以看到,三個(gè)函數(shù)的關(guān)系如下:
Downloader中三個(gè)核心函數(shù)關(guān)系
我們來重點(diǎn)看看這個(gè) _download()
方法:
_download()方法分析
看圖中注釋部分,_download()
方法先創(chuàng)建一個(gè)下載的 deferred,注意這里的方法正是 self.handlers
的 download_request()
方法,這是網(wǎng)頁下載的主要語句。 接下來又使用 deferred 的 addCallback()
方法添加一個(gè)回調(diào)函數(shù):_downloaded()
。很明顯,_downloaded()
就是下載完成后調(diào)用的方法,其中 response 就是下載的結(jié)果,也就是后續(xù)會(huì)返回給 spider 中的 parse()
方法的那個(gè)。我們可以簡單做個(gè)實(shí)驗(yàn),看看是不是真的會(huì)在這里打印出響應(yīng)的結(jié)果:
創(chuàng)建一個(gè)名為 test_downloader 的 scrapy 的項(xiàng)目:
[root@server2 scrapy-test]# scrapy startproject test_downloader
生成一個(gè)名為 downloader 的 spider:
# 進(jìn)入到spider目錄
[root@server2 scrapy-test]# cd test_downloader/test_downloader/spiders/
# 新建一個(gè)spider文件
[root@server2 spiders]# scrapy genspider downloader idcbgp.cn/wiki/
[root@server2 spiders]# cat downloader.py
import scrapy
class DownloaderSpider(scrapy.Spider):
name = 'downloader'
allowed_domains = ['idcbgp.cn/wiki/']
start_urls = ['http://idcbgp.cn/wiki/']
def parse(self, response):
pass
我們添加幾個(gè)配置,將 scrapy 的日志打到文件中,避免影響我們打印一些結(jié)果:
# test_download/settings.py
# ...
#是否啟動(dòng)日志記錄,默認(rèn)True
LOG_ENABLED = True
LOG_ENCODING = 'UTF-8'
#日志輸出文件,如果為NONE,就打印到控制臺(tái)
LOG_FILE = 'downloader.log'
#日志級(jí)別,默認(rèn)DEBUG
LOG_LEVEL = 'INFO'
# 日志日期格式
LOG_DATEFORMAT = "%Y-%m-%d %H:%M:%S"
#日志標(biāo)準(zhǔn)輸出,默認(rèn)False,如果True所有標(biāo)準(zhǔn)輸出都將寫入日志中,比如代碼中的print輸出也會(huì)被寫入到
LOG_STDOUT = False
最重要的步驟來啦,我們?cè)?scrapy 的源碼 scrapy/core/downloader/__init__.py
的中添加一些代碼,用于查看下載器獲取的結(jié)果:
我們來對(duì)添加的這部分代碼進(jìn)行下說明:
# ...
class Downloader:
# ...
def _download(self, slot, request, spider):
print('下載請(qǐng)求:{}, {}'.format(request.url, spider.name))
# The order is very important for the following deferreds. Do not change!
# 1. Create the download deferred
dfd = mustbe_deferred(self.handlers.download_request, request, spider)
# 2. Notify response_downloaded listeners about the recent download
# before querying queue for next request
def _downloaded(response):
self.signals.send_catch_log(signal=signals.response_downloaded,
response=response,
request=request,
spider=spider)
###############################新增代碼########################################
print('__downloaded()中 response 結(jié)果類型:{}'.format(type(response)))
import gzip
from io import BytesIO
from scrapy.http.response.text import TextResponse
if isinstance(response, TextResponse):
text = response.text
else:
# 解壓縮文本,這部分會(huì)在后續(xù)的下載中間件中被處理,傳給parse()方法時(shí)會(huì)變成解壓后的數(shù)據(jù)
f = gzip.GzipFile(fileobj = BytesIO(response.body))
text = f.read().decode('utf-8')
print('得到結(jié)果:{}'.format(text[:3000]))
############################################################################
return response
但就我們新建的項(xiàng)目而言,只是簡單的爬取慕課網(wǎng)的 wiki 頁面,獲取相應(yīng)的頁面數(shù)據(jù)。由于我們沒有禁止 robot 協(xié)議,所以項(xiàng)目第一次會(huì)爬取 /robots.txt
地址,檢查 wiki 頁面是否允許爬?。唤酉聛聿艜?huì)爬取 wiki 頁面。測(cè)試發(fā)現(xiàn),第一次請(qǐng)求 /robots.txt
地址時(shí),在 _downloaded()
中得到的結(jié)果直接就是 TextResponse 實(shí)例,我們可以用 response.text
方式直接拿到結(jié)果;但是第二次請(qǐng)求 http://idcbgp.cn/wiki/
時(shí),返回的結(jié)果是經(jīng)過壓縮的,其結(jié)果的前三個(gè)字節(jié)碼為:b'\x1f\x8b\x08'
開頭的 ,說明它是 gzip 壓縮過的數(shù)據(jù)。為了能查看相應(yīng)的數(shù)據(jù),我們可以在這里解碼查看,對(duì)應(yīng)的就是上面的 else
部分代碼。我們現(xiàn)在來進(jìn)行演示:
3. 小結(jié)
本小節(jié)中我們?cè)敱M的剖析了 Scrapy 的下載器模塊,找出了最終請(qǐng)求網(wǎng)頁數(shù)據(jù)的方式。整個(gè)下載器的代碼不算特別復(fù)雜,主要是對(duì) Twisted 的 web client 模塊中的類和方法的進(jìn)一步封裝,我們可以通過參考文獻(xiàn)1來掌握該模塊的學(xué)習(xí)。當(dāng)掌握了 Twisted 的這些基礎(chǔ)后,在來看 Scrapy 的代碼就會(huì)一目了然。下一小節(jié)我們將繼續(xù)剖析 Scrapy 的中間件模塊,進(jìn)一步學(xué)習(xí) Scrapy 框架源碼。