关于Scrapy-Redis源码的小小研究, 并在此基础上修改其使用的serializer和fingerprint
前提
熟练使用Scrapy框架, 已经安装了Redis服务器, Scrapy框架, 并且已经将Scrapy-Redis插件源码整合到Scrapy项目中
如果不熟悉Scrapy-Redis的使用, 参考前面一篇文章, Scrapy框架之Scrapy-Redis的两种使用方式
一. 修改serializer
使用Scrapy-Redis后, 往Scrapy-Redis调度器中推送的种子, 会被Scrapy-Redis使用pickle序列化工具将其Request对象以二进制形式存储到Redis中, 例如:
\x80\x04\x95E\x01\x00\x00\x00\x00\x00\x00}\x94(\x8C\x03url\x94\x8C\x1
3http://www.sina.com\x94\x8C\x08callback\x94\x8C\x05parse\x94\x8C\x0
7errback\x94N\x8C\x06method\x94\x8C\x03GET\x94\x8C\x07headers\x9
4}\x94C\x0AUser-Agent\x94]\x94CiMozilla/5.0 (X11; Linux x86_64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109
Safari/537.36\x94as\x8C\x04body\x94C\x00\x94\x8C\x07cookies\x94}\x9
4\x8C\x04meta\x94}\x94\x8C\x04item\x94}\x94\x8C\x02xx\x94\x8C\x02o
o\x94ss\x8C\x09_encoding\x94\x8C\x05utf8\x94\x8C\x08
priority\x94K\x00\x8C\x0Bdont_filter\x94\x88\x8C\x05flags\x94]\x94u.
这种格式是极难看的, 不方便我们进行调试
我们现在通过修改这个serializer, 使它采用json序列化工具来保存Request.
查看一下Scrapy-Redis源码, 先看一下queue.py文件
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)
可以看到如果没有设置serializer则使用默认的serializer(即picklecompat),所以这里我们再看一下picklecompat.py文件:
"""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)
它提供了两个方法, loads和dumps, 毫无疑问,一个是序列化,一个是反序列化, 也就是说如果我们希望使用json来代替pickle, 只需要遵循这个协议,提供loads和dumps方式即可.
到这里, 我们在Scrapy-Redis目录下新建一个文件为jsoncompat.py文件, 然后编写以下内容:
import json
from scrapy import Item
from tutorial.scrapy_redis.utils import convert_str
def loads(s):
if type(s) is bytes:
s = s.decode()
t = json.loads(s)
if 'body' in t.keys():
t['body'] = bytes(t['body'], encoding='utf-8')
return t
def dumps(obj):
meta = obj['meta']
for k in meta.keys():
if issubclass(type(meta[k]), Item):
meta[k] = dict(meta[k])
if 'body' in obj.keys():
obj['body'] = obj['body'].decode()
if 'headers' in obj.keys():
obj['headers'] = convert_str(obj['headers'])
return json.dumps(obj)
其中scrapy_redis.utils中的convert_str方法为:
def convert_str(data):
if isinstance(data, bytes):
return data.decode()
if isinstance(data, dict):
return dict(map(convert_str, data.items()))
if isinstance(data, tuple):
return map(convert_str, data)
if isinstance(data, list):
return data[0].decode()
return data
然后修改之前的那个queue.py文件,
if serializer is None:
# Backward compatibility.
# TODO: deprecate pickle.
serializer = jsoncompat
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)
一切准备就绪, 然后往调度器中重新push一条任务, 可以看到, 最终效果:
{
"url":"[http://www.weixin.com](http://www.weixin.com/)",
"callback":"parse",
"errback":null,
"method":"GET",
"headers":{
"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"
},
"body":"",
"cookies":{
},
"meta":{
"item":{
"xx":"oo"
}
},
"_encoding":"utf-8",
"priority":0,
"dont_filter":true,
"flags":[
]
}
另外, 上述修改serializer的方式还可以通过修改settings文件来做到, 具体方法是编写完jsoncompat文件后, 不改变queue文件而是在settings增加这行配置:
SCHEDULER_SERIALIZER = jsoncompat
之所以这样做也可以生效, 是因为在Scrapy-Redis的scheduler文件中有留有说明:
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',
# We use the default setting name to keep compatibility.
'dupefilter_cls': 'DUPEFILTER_CLASS',
'serializer': 'SCHEDULER_SERIALIZER',
}
这五个参数可以通过在settings中设置指定的参数来指定(比如想修改serializer, 则可以在settings中设置参数SCHEDULER_SERIALIZER来指定). 如果settings文件没有指定,则使用默认值;
二. 修改fingerprint
Scrapy-Redis在dupefilter实现了根据指纹去重(实际上简单的调用了一下Scrapy的指纹去重), 代码如下:
def request_fingerprint(self, request):
"""Returns a fingerprint for a given request.
Parameters
----------
request : scrapy.http.Request
Returns
-------
str
"""
return request_fingerprint(request)
我们可以看到,去重指纹是sha1(method + url + body + header); 所以,实际能够去掉重复的比例并不大。如果我们需要自己提取去重的finger,需要自己实现Filter,并配置上它。
下面这个Filter只根据url的md5码去重:
def request_fingerprint(self, request):
"""Returns a fingerprint for a given request.
Parameters
----------
request : scrapy.http.Request
Returns
-------
str
"""
return md5(request.url)
住: 另外一种方式类似于上边修改serializer, 直接新建一个文件, 包含新的指纹去重算法, 然后在settings中指定即可.
网友评论