爬蟲

  • 數據抓包

    • 爬蟲其實就是利用開發的程序模擬發起正常的請求行爲
    • 正常的請求行爲:
      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
        
        
      1. 普通內存版本 - 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()
        
        
      2. 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
        
        
      3. 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模塊進行反序列化操作(將二進制轉化爲對象),取數進行序列化操作(將對象轉化爲二進制)

    • 布隆過濾器
      原理:
      在這裏插入圖片描述
      減少誤判率:

    1. 增加hash函數
    2. 增加hash表長度
    3. hash算法中加salt

    Python實現的內存版布隆過濾器pybloom

    實戰:手動實現的redis版布隆過濾器
    muti_hash.py

    import 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']}
    


  • Redis 共享資源競爭
    Redis 共享資源競爭
  • Redis共享資源競爭解決方案:上鎖
    鎖機制
  1. 使用了鎖機制後,能確保同一份數據只會被某一個線程獲取到,而不會被多個線程同時獲取,從而保證了數據不會被處理多次;
  2. 此處相當於實現了同一個線程內部zrange與zrem是一個原子性操作。
  • 注意:
  1. 一般的內存中的鎖只能解決單進程中多個線程間的資源共享問題;
  2. 如果是不同進程間甚至不同服務器上線程間資源共享問題,則需要考慮使用如redis分佈式鎖來實現;
  3. redis雖有事務機制,但仍不足以保證前面理想的執行結果百分百出現。
  • Redis分佈式鎖 實現原理
    在這裏插入圖片描述
  • 說明:
  1. 獲取鎖:利用redis的setnx命令特徵
    1. 如果key不存在則執行操作,返回值將是1,此時表明鎖獲取成功,即上鎖;
    2. 如果key存在則不執行任何操作,返回值將是0,此時表明鎖獲取失敗,因爲已經被上鎖了
  2. 釋放鎖:獲取設置的值,判斷是否是當前線程設置的值
    1. get命令獲取對應key的值
    2. 判斷值是否和預先設置的一樣(thread_id),保證不是其他線程解開的鎖
    3. 如果一致,就把該key刪除,表示釋放鎖,此時其他線程便可以獲取到鎖
  • 注意:
    1. 同一把鎖,注意lock_name一致
    2. 使用同一把鎖的各個線程,必須維護好各自的thread_id,不能重複。否則可能出現,如a線程上的鎖卻被b線程解開了,這樣的bug
    3. 爲防止死鎖問題(如a線程上了鎖,但在解開鎖前a線程掛了),應當給lock_name這個數據設置一定過期時間,具體時間,依實際情況定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章