爬虫初体验

  • 数据抓包

    • 爬虫其实就是利用开发的程序模拟发起正常的请求行为
    • 正常的请求行为:
      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这个数据设置一定过期时间,具体时间,依实际情况定
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值