-
数据抓包
- 爬虫其实就是利用开发的程序模拟发起正常的请求行为
- 正常的请求行为:
PC端web浏览器的请求行为
移动端web浏览器的请求行为
移动APP端的请求行为
其他终端的请求行为 - 数据抓包的目的就是通过捕获PC端、移动端或其他终端的请求行为,并对其进行分析,从而实现在程序中进行模拟。
-
chrome抓包
一般情况下,PC端web页面可利用chrome进行抓包
优点:只要是浏览器支持的应用层协议都能进行捕获
缺点:只能捕获通过Chrome浏览器发起的请求 -
抓包工具原理
- 抓包工具本质:一个本地代理软件,本地的所有请求都是通过抓包工具进行转发。
- 常见抓包工具:
Wireshark:几乎TCP/IP协议栈任何协议都能捕获
Fiddler:http/https抓包工具,跨平台性差
Charles
(推荐): http/https抓包工具,跨平台性强,官方文档支持较好
-
Charles安装
利用Charles进行PC端http/https抓包
利用Charles进行移动端http/https抓包
注意:HTTPS的抓包必须安装好对应的证书
-
HTTPS抓包原理
- WebSocket协议与HTTP对比
- 安装并使用 websocket-client
在docker-dir/Dockerfile中追加一句RUN pip install websocket_client
安装websocket_client,重新执行Dockerfile构建spider-dev镜像
测试代码:import websocket try: import thread except ImportError: import _thread as thread import time def on_message(ws, message): print(message) def on_error(ws, error): print(error) def on_close(ws): print("### closed ###") def on_open(ws): def run(*args): for i in range(10): time.sleep(1) ws.send("Hello %d" % i) time.sleep(1) ws.close() print("thread terminating...") thread.start_new_thread(run, ()) if __name__ == "__main__": websocket.enableTrace(True) ws = websocket.WebSocketApp("ws://123.207.167.163:9010/ajaxchattest", on_message=on_message, on_error=on_error, on_close=on_close) ws.on_open = on_open ws.run_forever()
-
去重
依据原始数据去重
根据原始数据特征值去重
-
信息摘要hash算法
信息摘要hash算法:指可以将任意长度的文本、字节数据,通过一个算法得到一个固定长度的文本。 如MD5(128位)、SHA1(160位)等。
特征:只要源文本不同,计算得到的结果,必然不同
(摘要)
摘要:摘要算法主要用于比对信息源是否一致,因为只要源发生变化,得到的摘要必然不同;而且通常结果要比源短很多,所以称为“摘要”。
正因此,利用信息摘要算法能大大降低去重容器的存储空间使用率,并提高判断速度,且由于其强唯一性的特征,几乎不存在误判。
注意:hash算法得出的结果其实本质上就是一串数值,如md5的128位指的是二进制的长度,十六进制的长度是32位。一个十六进制等于四个二进制。
- 基类
__init__.py
# 信息摘要hash算法去重方案实现 # 1. 普通内存版本 # 2. Redis持久化版本 # 3. MySQL持久化版本 import six import hashlib class BaseFilter(object): """基于信息摘要算法进行数据去重判断和存储""" def __init__(self, hash_func_name="md5", redis_host='localhost', redis_port=6379, redis_db=0, redis_key='filter', mysql_url=None, mysql_table_name="filter"): # redis配置 self.redis_host = redis_host self.redis_port = redis_port self.redis_db = redis_db self.redis_key = redis_key self.mysql_url = mysql_url self.mysql_table_name = mysql_table_name self.hash_func = getattr(hashlib, hash_func_name) self.storage = self._get_storage() def _safe_data(self, data): """ python2 str == python bytes python2 unicode == python3 str :param data: 给定的原始数据 :return: 二进制类型的字符串数据 """ if six.PY3: if isinstance(data, bytes): return data elif isinstance(data, str): return data.encode() else: raise Exception("Please input string") elif six.PY2: if isinstance(data, str): return data elif isinstance(data, unicode): return data.encode() else: raise Exception("Please input string") def _get_hash_value(self, data): """ 根据给定数据,返回对应信息摘要hash值 :param data: 给定的原始数据 :return: hash值 """ hash_obj = self.hash_func() hash_obj.update(self._safe_data(data)) hash_value = hash_obj.hexdigest() return hash_value def save(self, data): """ 根据data算出指纹进行存储 :param data: 给定的原始数据 :return: 存储的结果 """ hash_value = self._get_hash_value(data) return self._save(hash_value) def _save(self, hash_value): """ 存储对应的hash值(方法由子类重写) :param data: 通过摘要算法算出的hash值 :return: 存储结果 """ pass def is_exists(self, data): """ 判断给定的指纹是否存在 :param data: 给定的指纹信息 :return: True or False """ hash_value = self._get_hash_value(data) return self._is_exists(hash_value) def _is_exists(self, hash_value): """ 判断指纹是否存在(方法由子类重写) :param data: 通过摘要算法算出的hash值 :return: True or False """ pass def _get_storage(self): """ 存储(方法由子类重写) :return: """ pass
- 普通内存版本 - momery_fitler.py
from . import BaseFilter class MemoryFilter(BaseFilter): """基于python中集合数据机构进行去重判断依据的存储""" def _is_exists(self, hash_value): if hash_value in self.storage: return True return False def _save(self, hash_value): """ 利用set进行存储 :param hash_value: :return: """ return self.storage.add(hash_value) def _get_storage(self): return set()
- Redis持久化版本 - redis_filter.py
import redis from . import BaseFilter class RedisFilter(BaseFilter): """基于redis的持久化存储的去重判断依据的实现""" def _is_exists(self, hash_value): """判断redis对应的无序集合中是否有对应的数据""" return self.storage.sismember(self.redis_key, hash_value) def _save(self, hash_value): """ 利用redis无序集合进行存储 :param hash_value: :return: """ return self.storage.sadd(self.redis_key, hash_value) def _get_storage(self): """返回一个redis链接对象""" pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, db=self.redis_db) client = redis.StrictRedis(connection_pool=pool) return client
- MySQL持久化版本 - mysql_filter.py
from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base from . import BaseFilter Base = declarative_base() class MysqlFilter(BaseFilter): """基于mysql去重判断依据存储""" def __init__(self, *args, **kwargs): self.table = type( kwargs['mysql_table_name'], (Base,), dict( __tablename__=kwargs['mysql_table_name'], id=Column(Integer, primary_key=True), hash_value=Column(String(36), index=True, unique=True) ) ) BaseFilter.__init__(self, *args, **kwargs) def _is_exists(self, hash_value): """判断mysql中是否有对应的数据""" session = self.storage() ret = session.query(self.table).filter_by(hash_value=hash_value).first() session.close() if ret is None: return False return True def _save(self, hash_value): """ 利用mysql进行存储 :param hash_value: :return: """ session = self.storage() filter = self.table(hash_value=hash_value) session.add(filter) session.commit() session.close() def _get_storage(self): """返回一个mysql连接对象(sqlalchemy数据库连接对象)""" engine = create_engine(self.mysql_url) Base.metadata.create_all(engine) # 创建表,如果已存在就忽略 Session = sessionmaker(engine) return Session
- 基类
-
基于simhash算法的去重
局部敏感的哈希算法,能实现相似
文本(如下图文章)的去重。
-
与信息摘要算法的区别:
信息摘要算法:如果原始内容只相差一个字节,所产生的签名也很可能差别很大。 ==
Simhash算法:如果原始内容只相差一个字节,所产生的签名差别非常小。
Simhash值比对:通过两者的simhash值的二进制位的差异来表示原始文本内容的差异。差异个数又被称为海明距离
。 -
注意:
Simhash对长文本500字+比较适用,短文本可能偏差较大;
在google的论文给出的数据中,64位simhash值,在海明距离为3的情况下,可认为两篇文档是相似的或者是重复的。当然这个值只是参考值,针对自己的应用可能有不同的测试取值。 -
simhash开源模块
由于simhash算法是二进制位的比对,只能在内存中进行。如果要持久化存储需要使用pickle模块进行反序列化操作(将二进制转化为对象),取数进行序列化操作(将对象转化为二进制)
-
-
布隆过滤器
原理:
减少误判率:
- 增加hash函数
- 增加hash表长度
- hash算法中加salt
Python实现的内存版布隆过滤器pybloom
实战:手动实现的redis版布隆过滤器
muti_hash.pyimport hashlib import six # 1. 多个hash函数的实现和求值 # 2. hash表实现和实现对应的映射和判断 class MultiHash(object): """根据提供的原始数据和的预定义的多个salt,生成多个hash函数值""" def __init__(self, salts, hash_func_name="md5"): self.salts = salts if len(salts) < 3: raise Exception("Please provide at least 3 values...") self.hash_func = getattr(hashlib, hash_func_name) def get_hash_value(self, data): """根据提供的原始数据,返回多个hash值""" hash_value = [] for i in self.salts: hash_obj = self.hash_func() hash_obj.update(self._safe_data(data)) hash_obj.update(self._safe_data(i)) ret = hash_obj.hexdigest() hash_value.append(int(ret, 16)) return hash_value def _safe_data(self, data): """ python2 str == python bytes python2 unicode == python3 str :param data: 给定的原始数据 :return: 二进制类型的字符串数据 """ if six.PY3: if isinstance(data, bytes): return data elif isinstance(data, str): return data.encode() else: raise Exception("Please input string") elif six.PY2: if isinstance(data, str): return data elif isinstance(data, unicode): return data.encode() else: raise Exception("Please input string") if __name__ == '__main__': mh = MultiHash(['1', '2', '3']) print(mh.get_hash_value('fone'))
bloom_redis.py
# 布隆过滤器 redis存储hash值 import redis from multi_hash import MultiHash class BloomFilter(object): """ 布隆过滤器 redis存储hash值 salts在同一个项目中不能更改 """ def __init__(self, salts, redis_host='localhost', redis_port=6379, redis_db=0, redis_key='filter'): self.redis_host = redis_host self.redis_port = redis_port self.redis_db = redis_db self.redis_key = redis_key self.client = self._get_storage() self.multi_hash = MultiHash(salts) def _get_storage(self): """返回一个redis链接对象""" pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, db=self.redis_db) client = redis.StrictRedis(connection_pool=pool) return client def save(self, data): """存储""" hash_values = self.multi_hash.get_hash_value(data) for hash_value in hash_values: offset = self._get_offset(hash_value) self.client.setbit(self.redis_key, offset, 1) def is_exists(self, data): """判断是否存在""" hash_values = self.multi_hash.get_hash_value(data) for hash_value in hash_values: offset = self._get_offset(hash_value) res = self.client.getbit(self.redis_key, offset) if res == 0: return False return True def _get_offset(self, hash_value): """ 求余 2**8 = 256 2**20 = 1024 * 1024 (2**8 * 2**20 * 2**3) 代表hash表的长度,在同一个项目中不能更改 """ return hash_value % (2**8 * 2**20 * 2**3) if __name__ == '__main__': data = ['1', '2', '3', '4', '1', 'a', '中文', 'a', '中'] bf = BloomFilter(['1', '22', '333'], redis_host='172.17.0.2') for d in data: if not bf.is_exists(d): bf.save(d) print("映射数据成功:", d) else: print("重复数据:", d)
-
-
请求去重
- 请求去重判断依据
- 请求方法
- 请求地址(URL)
- URL查询参数
- 请求体
- 去重方案
- 基于信息摘要算法求指纹的去重
- 基于布隆过滤器的去重
- 请求数据处理
- 统一大小写(method、URL)
- URL查询参数排序(query)
- 请求体排序(data)
使用python库自带的模块urllib.parse
解析后的_包含协议scheme、经统一小写的域名hostname(未经统一大小写的域名netloc)、路由path(保留大小写)、路径中的查询参数query(保留大小写)In [1]: import urllib.parse In [2]: url = 'Http://WWw.baidu.com/s?q=101' In [3]: _ = urllib.parse.urlparse(url) In [4]: _ Out[4]: ParseResult(scheme='http', netloc='WWw.baidu.com', path='/s', params='', query='q=101', fragment='') In [5]: _.scheme + "://" + _.hostname + _.path Out[5]: 'http://www.baidu.com/s' In [6]: url Out[6]: 'Http://www.baidu.com/s?q=101'
urllib.parse.parse_qsl:
不对
相同key进行合并
urllib.parse.parse_qs:相同key进行合并In [13]: urllib.parse.parse_qsl(_.query+"&q=102") Out[13]: [('q', '101'), ('q', '102')] In [14]: urllib.parse.parse_qs(_.query+"&q=102") Out[14]: {'q': ['101', '102']}
- 队列
- 临时队列
内置队列模块
asyncio中的队列模块
gevent中的队列模块
tornado中的队列模块 - 持久化队列
- 现成:
queuelib中的disk queue
基于redis实现的queue(如pyspider中的redis_queue) - 利用Python实现基于redis的FIFO、LIFO、Priority队列
RedisFifoQueue
RedisLifoQueue
RedisPriorityQueue
- 临时队列
- Redis 共享资源竞争
- Redis共享资源竞争解决方案:上锁
- 使用了锁机制后,能确保同一份数据只会被某一个线程获取到,而不会被多个线程同时获取,从而保证了数据不会被处理多次;
- 此处相当于实现了同一个线程内部zrange与zrem是一个原子性操作。
- 注意:
- 一般的内存中的锁只能解决单进程中多个线程间的资源共享问题;
- 如果是不同进程间甚至不同服务器上线程间资源共享问题,则需要考虑使用如redis分布式锁来实现;
- redis虽有事务机制,但仍不足以保证前面理想的执行结果百分百出现。
- Redis分布式锁 实现原理
- 说明:
- 获取锁:利用redis的setnx命令特征
1. 如果key不存在则执行操作,返回值将是1,此时表明锁获取成功,即上锁;
2. 如果key存在则不执行任何操作,返回值将是0,此时表明锁获取失败,因为已经被上锁了- 释放锁:获取设置的值,判断是否是当前线程设置的值
1. get命令获取对应key的值
2. 判断值是否和预先设置的一样(thread_id),保证不是其他线程解开的锁
3. 如果一致,就把该key删除,表示释放锁,此时其他线程便可以获取到锁
- 注意:
1. 同一把锁,注意lock_name一致
2. 使用同一把锁的各个线程,必须维护好各自的thread_id,不能重复。否则可能出现,如a线程上的锁却被b线程解开了,这样的bug
3. 为防止死锁问题(如a线程上了锁,但在解开锁前a线程挂了),应当给lock_name这个数据设置一定过期时间
,具体时间,依实际情况定