为爬虫搭建一个ProxyPool

前言


在日常写数据采集脚本的时候,我们总会遇到各种反爬手段,比如:headers反爬,ip反爬等。上一篇我们知道了Cookies池的搭建过程,通过Cookies池可以登录站点抓取设置登录限制的页面数据。但是仅仅有Cookies池是不够的,因为你Cookies的数量再多,也只是单节点,有一些网站如58会设置IP反爬,你的频率快了之后,ip可能会有被封的风险。那我限制下速度不行吗? 限制了速度,其实也就降低了效率,还有这并不一定能有效绕过IP反爬。所以,我们就有必要去构建和维护一个代理池


Step 1 ---- 实现思路


  • 获取代理从各个代理网站抓取免费代理
  • 存储代理选定一种数据结构(这里采用redis数据库中的有序集合,原因稍后说)用来存储抓取下来的代理
  • 检测代理设置一个测试URL,然后使用代理去请求,根据响应的状态码判断代理是否可用。
  • 接口提供给爬虫一个接口,返回一个随机代理(这里使用flask)




Step2 ---- 模块构成及其功能


代理池有5个模块构成:存储模块,获取模块,检测模块,接口模块,调度模块。下图展示的是代理池的结构

proxy.jpg
  • 获取模块


    从多个站点中抓取代理存放到数据库中,我们要测试是否有用,就必须设计一个筛选可用代理的规则(这里我使用Python3网络爬虫开发实战一书作者的方法)。

    筛选规则:在获取代理后,在将它写入数据库前给它设置一个初始score,如果检测到该代理可用,就讲它设置为100,不可用减一;当减到0时,从数据库中删除。然后设置一个随机函数,用于返回随机代理


  • 存储模块


    获取到一个代理之后,我们需要对其设置score(设置score的目的是用于筛选出可用代理)。这里我们选用redis中的有序集合,是因为这个数据结构里面带有score字段,我们可以根据score字段进行排序,而且集合内所有元素不重复。


  • 检测模块


    拿出每一个代理进行请求测试,根据代理筛选规则设置代理分数


  • 接口模块


    在检测模块运行完之后,代理池中会留下两种代理。一种是可用代理(score=100),另一种是不可用代理(score<100,失败次数太多或是代理不稳定),通过flask构建一个爬虫接口返回score=100的代理。


  • 调度模块


    如果构建一个代理池,你是先采集代理 --> 存储代理 --> 检测代理这样的顺序执行的话,那效率就有点低了,因为需要等待上一个任务完成之后才能进行下一个任务。因此,这里我们采用多进程和异步来实现对各个模块的调度(异步的语法要Python3.5以上才支持。)


Step3 ---- 各个模块实现


-- 存储模块 --




class RedisClient(object):
    def __init__(self):
        '''
        decode_responses参数设置为true,写入的value值为str,否则为字节型
        '''
        self.db = StrictRedis(host=HOST,port=PORT,password=PASSWD,decode_responses=True)
        pass


    def random(self):
        result = self.db.zrangebyscore(REDIS_KEY,MAX_SCORE,MAX_SCORE)
        if len(result):
            return choice(result)
            
        else:
            result = self.db.zrevrange(REDIS_KEY,MIN_SCORE,MAX_SCORE)
            if len(result):
                return choice(result)
            else:
                raise PoolEmptyError

    

    def add(self,proxy,score=INITIAL_SCORE):
        if not self.db.zscore(REDIS_KEY,proxy):
            return self.db.zadd(REDIS_KEY,score,proxy)
        
    

    def decrease(self,proxy):
        # 获取proxy对应的分数
        score = self.db.zscore(REDIS_KEY,proxy)
        if score and score > MIN_SCORE:
            print("代理 {} , 分数 {} , 减1".format(proxy,score))
            return self.db.zincrby(REDIS_KEY,proxy,-1)
            
        else:
            print("代理 {} , 当前分数 {} , 移除".format(proxy,score))
            self.db.zrem(REDIS_KEY,proxy)
            
        
    

    def exists(self,proxy):
        return not self.db.zscore(REDIS_KEY,proxy) == None
        
    

    def max(self,proxy):
        print("代理 {} , 可用 , 设置为{}".format(proxy,MAX_SCORE))
        return self.db.zadd(REDIS_KEY,MAX_SCORE,proxy)v


-- 获取模块 --



class ProxyMetaClass(type):
    def __new__(cls,name,bases,attrs):
        attrs['__CrawleFunc__'] = []
        count = 0
        for k,v in attrs.items():
            if 'crawle' in k:
                attrs['__CrawleFunc__'].append(k)
                count += 1
        attrs['__CrawleCount__'] = count
        return type.__new__(cls,name,bases,attrs)



class Crawler(object,metaclass=ProxyMetaClass):
    def get_proxies(self,callack):
        proxies = []
        if callack == 'crawle_daili66':
            return self.process_daili66(callack)
        else:
            for proxy in eval("self.{}()".format(callack)):
                print("成功获取代理 {}".format(proxy))
                proxies.append(proxy)
            return proxies
    

    def process_daili66(self,callback):
        proxies = []
        for page_count in range(1,11):
            for proxy in eval("self.{}(page_count={})".format(callback,page_count)):
                print("成功获取代理 {}".format(proxy))
                proxies.append(proxy)
        return proxies


    def crawle_xiciproxy(self):
        start_url = "http://www.xicidaili.com/"
        html = get_pages(start_url,"xici")
        if html:
            return parse_xiciproxy(html)
        
    

    def crawle_daili66(self,page_count=1):
        start_url = "http://www.66ip.cn/{}.html"
        sleep(5)
        html = get_pages(start_url.format(page_count),website="66")
        if html:
            print("第{}页代理".format(page_count))
            return parse_66(html)
    

    def crawle_guobanjia(self):
        start_url = "http://www.goubanjia.com/"
        html = get_pages(start_url,'goubanjia')
        if html:
            return parse_guobanjia(html)
        pass



-- 检测模块 --



class CheckUp(object):
    def __init__(self):
        self.db = RedisClient()
    


    def run(self):
        # get all proxies
        # get event loop
        # split proxies to serval part
        # pack a task list
        # call run to exec asynic

        print("Start test")
        try:
            proxies = self.db.all()
            loop = asyncio.get_event_loop()
            for i in range(0,len(proxies),BATCH_SIZE):
                tasks_proxies = proxies[i:i + BATCH_SIZE]
                tasks = [self.check_single_proxy(proxy) for proxy in tasks_proxies]
                loop.run_until_complete(asyncio.wait(tasks))
                sleep(3)
        except Exception as e:
            _ = e
            print(e.args)
            print("CheckUp occurs Error")

    async def check_single_proxy(self,proxy):
        connection = aiohttp.TCPConnector(verify_ssl=False)
        async with aiohttp.ClientSession(connector=connection) as session:
            # 测试代理可用性




-- 接口模块 --




@app.route("/")
def index():
    return "<h2>Welcome to Proxy Pool System</h2>"
    



def get_conn():
    if not hasattr(g,"redis"):
        g.redis = RedisClient()
    return g.redis

@app.route('/count')
def get_count():
    conn = get_conn()
    return conn.count()
    


@app.route('/random')
def random():
    conn = get_conn()
    return conn.random()




-- 调度模块 --




class Scheduler(object):
    def getter(self,cylcle=CYCLE_GETTER):
        getter = Getter()
        while True:
            print("Start to get proxy")
            getter.run()
            sleep(cylcle)
            
    

    def checkup(self,cycle=CYCLE_CHECKUP):
        check = CheckUp()
        while True:
            print("Start to checkup proxy")
            check.run()
            sleep(cycle)
    

    def api(self):
        app.run(API_HOST,API_PORT)
        
    
    def run(self):
        print("Proxy Pool start run")
        if API_PROCSS:
            api_process = multiprocessing.Process(target=self.api)
            api_process.start()

        
        if GETTER_PROCESS:
            getter_process = multiprocessing.Process(target=self.getter)
            getter_process.start()

        
        if CHECKUP_PROCESS:
            checkup_process = multiprocessing.Process(target=self.checkup)
            checkup_process.start()
        



最后


使用代理池我们可以随机更换IP(不一定都有用)来对抗IP反爬,这对于爬取大规模数据是必要的,也是对一个爬虫工程师最基本的条件

最后运行一下,可以看到如下结果

运行.png
代理池.png

具体代码已发布在github上:点我传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,015评论 11 349
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,754评论 0 5
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,507评论 6 13
  • 一直记得几年前看的一本书,林志颖写的《我对时间有耐心》。 在书里,他说从小做什么事情就用心去做好了,不...
    爱吃核桃不吐皮阅读 261评论 0 0