解析scrapy-redis中的request请求对象

scrapy-redis项目会在redis中存储请求数据,单个请求的内容如下:

1
\x80\x04\x95\xFE\x02\x00\x00\x00\x00\x00\x00}\x94(\x8C\x03url\x94\x8C+https://weibo.cn/6960161079/profile?page=50\x94\x8C\x08callback\x94\x8C\x0Bparse_tweet\x94\x8C\x07errback\x94N\x8C\x06method\x94\x8C\x03GET\x94\x8C\x07headers\x94}\x94C\x07Referer\x94]\x94C*https://weibo.cn/6960161079/profile?page=1\x94as\x8C\x04body\x94C\x00\x94\x8C\x07cookies\x94}\x94\x8C\x04meta\x94}\x94(\x8C\x05depth\x94K\x03\x8C\x07account\x94}\x94(\x8C\x03_id\x94\x8C!xgdmyoshbnczvmu-gi71536@yahoo.com\x94\x8C\x08password\x94\x8C\x0AMpynlcbob2\x94\x8C\x06cookie\x94X\x13\x01\x00\x00_T_WM=3e75c689253decfdeed2f36c228f25a8; SSOLoginState=1564821709; SCF=Am8KCIzBVmojgtH8Hi2oM2OAcQD6d5Ru97nYpPsX9rNGmlv4UTczI0CLthhTzqviK1d0bDT82zLWohjONUgc3hs.; SUHB=0xnSGM_AEwxnMa; SUB=_2A25wQTidDeRlGeFP41MT8ibOyT2IHXVTyljVrDV6PUJbkdAKLXjdkW1NQROqr0oU5FTEqUgkCToVQKrxapJnL4wB\x94\x8C\x06status\x94\x8C\x07success\x94u\x8C\x10download_timeout\x94G@$\x00\x00\x00\x00\x00\x00\x8C\x0Ddownload_slot\x94\x8C\x08weibo.cn\x94\x8C\x10download_latency\x94G?\xE5\xE1>\x80\x00\x00\x00u\x8C\x09_encoding\x94\x8C\x05utf-8\x94\x8C\x08priority\x94K\x00\x8C\x0Bdont_filter\x94\x88\x8C\x05flags\x94]\x94\x8C\x09cb_kwargs\x94}\x94u.

那么这个数据代表了什么,如何解析这部分数据呢。

尝试性decode

首先尝试的使用utf-8来decode这部分数据。

1
2
3
4
import redis 
r = redis.Redis()
request = r.zrange("weibo_spider:requests",0,0) # 得到一个列表
request[0].decode("utf-8") # 报错:

在尝试进行decode的时候就出现了报错内容 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte

尝试使用其他的decode方法依然没有正确的结果,那么这里的bytes可能并非是String直接编码得到,而是其他的对象进行序列化得到的。

编码和序列化的差异

对于编码和序列化得到的内容是相同的,都是bytes(可以网络或者系统中传输),但是源对象是有差异的。

  • 编码:源数据的类型是字符串类型
  • 序列化:源数据的类型是对象类型

在这个例子中得到的是一个request对象的bytes串,request对象很复杂,必然是使用序列化的技术来实现的,所以在这里尝试使用各种codec来进行解码都会报错。

在反序列化,将bytes转换成对象时需要使用对应的序列化方法。那么问题就来了,request对象是使用什么序列化技术的呢? 这个就需要查看源码了。

序列化request对象的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
optional = {
# TODO: Use custom prefixes for this settings to note that are
# specific to scrapy-redis.
'queue_key': 'SCHEDULER_QUEUE_KEY',
'queue_cls': 'SCHEDULER_QUEUE_CLASS', #
'dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',
}

def open(self, spider):
try:
self.queue = load_object(self.queue_cls)(
server=self.server,
spider=spider,
key=self.queue_key % {'spider': spider.name},
serializer=self.serializer,
)
except TypeError as e:
raise ValueError("Failed to instantiate queue class '%s': %s",
self.queue_cls, e)

def enqueue_request(self, request):
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False
if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
self.queue.push(request) # 入列
return True

def next_request(self):
block_pop_timeout = self.idle_before_close
request = self.queue.pop(block_pop_timeout) # 出列
if request and self.stats:
self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
return reques

在项目的settings.py文件中可以查看到 SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'使用的是这个模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from . import picklecompat    
class Base(object):
"""Per-spider base queue class"""

def __init__(self, server, spider, key, serializer=None):
if serializer is None:
# Backward compatibility.
# TODO: deprecate pickle.
serializer = picklecompat ## 定义序列化方法
if not hasattr(serializer, 'loads'):
raise TypeError("serializer does not implement 'loads' function: %r"
% serializer)
if not hasattr(serializer, 'dumps'):
raise TypeError("serializer '%s' does not implement 'dumps' function: %r"
% serializer)

self.serializer = serializer

def _encode_request(self, request):
"""Encode a request object"""
obj = request_to_dict(request, self.spider)
return self.serializer.dumps(obj)

class PriorityQueue(Base):
"""Per-spider priority queue abstraction using redis' sorted set"""

def push(self, request):
"""Push a request"""
data = self._encode_request(request) ## 这里就是将reques对象进行编码
score = -request.priority
self.server.execute_command('ZADD', self.key, score, data)

从上面可以看到request对象是使用序列化方法来进行的编码的,具体的方法见picklecompat文件。文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
"""A pickle wrapper module with protocol=-1 by default."""
try:
import cPickle as pickle # PY2
except ImportError:
import pickle

def loads(s):
return pickle.loads(s)

def dumps(obj):
return pickle.dumps(obj, protocol=-1)

这个文件就显示的非常清楚,使用的是pickle模块来完成序列化。

那么就同样使用该库来完成反序列化。

使用pickle加载序列化数据

首先需要安装pickle模块。

1
2
import pickle 
pickle.loads(r.zrange("weibo_spider:request",0,0)[0])

得到的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{'_encoding': 'utf-8',
'body': b'',
'callback': 'parse_comment',
'cb_kwargs': {},
'cookies': {},
'dont_filter': True,
'errback': None,
'flags': [],
'headers': {b'Referer': [b'https://weibo.cn/comment/H9PVWxxKs?page=1']},
'meta': {'account': {'_id': 'ulyubnmxkxqb-pgal@yahoo.com',
'cookie': '_T_WM=ded47978db16e5f1cbb9b620034c2e1e; SSOLoginState=1565150168; SCF=ApWx2a0jtPy2v8lJ4nIGxg382Qn7rp1ZJkJQkYbVkztomDCq2tlC8_szbjm0hn3UTKYLih66ZmgGB0LINvYokHc.; SUHB=0sqR45gDPXFJdM; SUB=_2A25wTjuIDeRhGeFP41AZ8CjMzT6IHXVTsUXArDV6PUJbkdAKLUf7kW1NQROrBI850wMbW4KDCxsMUTdXZIhHdjCC',
'password': 'WTwmlotqtnt58',
'status': 'success'},
'depth': 5,
'download_latency': 0.5067558288574219,
'download_slot': 'weibo.cn',
'download_timeout': 10.0,
'weibo_url': 'https://weibo.com/2060865431/H9PVWxxKs'},
'method': 'GET',
'priority': 0,
'url': 'https://weibo.cn/comment/H9PVWxxKs?page=105'}

如上就完成了读取过程。

参考文档

源码分析参考:Dupefilter 负责执行requst的去重,使用的Redis的设定数据结构

小白进阶之Scrapy第六篇Scrapy-Redis详解 介绍了面向分布式的调度器和单机上的调度器差异

Standard Encodings 文中共列举了n中codec的方法,不同的python版本所包含的codec数量是不同的,v3.7有98种

Python Serialization Benchmarks 列举了8种序列化方法的优缺点,已经进行的性能测试对比。

本文原创,请随意转载,但请添加文章链接,并将链接放置在文章开头,谢谢。

随手请吃块糖呗