-
數據抓包
- 爬蟲其實就是利用開發的程序模擬發起正常的請求行爲
- 正常的請求行爲:
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這個數據設置一定過期時間
,具體時間,依實際情況定