開源代碼閱讀——proxy_pool★

項(xiàng)目鏈接: https://github.com/jhao104/proxy_pool

爬蟲代理IP池項(xiàng)目,主要功能為定時(shí)采集網(wǎng)上發(fā)布的免費(fèi)代理驗(yàn)證入庫(kù)赖瞒,定時(shí)驗(yàn)證入庫(kù)的代理保證代理的可用性,提供API和CLI兩種使用方式堆巧。同時(shí)你也可以擴(kuò)展代理源以增加代理池IP的質(zhì)量和數(shù)量盐类。

模塊組成:獲取功能俗批、存儲(chǔ)功能痕貌、校驗(yàn)功能溜徙、接口管理

:star:程序主要是啟動(dòng)了startServer的API接口服務(wù)湃缎、startScheduler定時(shí)服務(wù)

  • startServer->runFlask:是向外提供了通過(guò)proxyHandler來(lái)獲得Redis中的proxy數(shù)據(jù)
  • startScheduler->sche.add_task(__runProxyFetch)、sche.add_task(__runProxyCheck)蠢壹,
    • __runProxyFetch:proxy_fetcher.run()->proxy_queue->Checker("raw", proxy_queue)獲得各個(gè)代理網(wǎng)站的代理信息后嗓违,進(jìn)行校驗(yàn),校驗(yàn)成功則入庫(kù)
    • __runProxyCheck:proxy in proxy_handler.getAll()->proxy_queue->Checker("use", proxy_queue):通過(guò)proxy_handler拿到庫(kù)里所有現(xiàn)存的數(shù)據(jù)后图贸,進(jìn)行有效性校驗(yàn)蹂季,無(wú)效的則刪除,有效的則更新信息

作為存儲(chǔ)功能的接口proxyHandler疏日,也是兩個(gè)API服務(wù)與定時(shí)服務(wù)的中介偿洁。程序也是通過(guò)存儲(chǔ)功能,將核心的兩個(gè)功能:<u>定時(shí)抓取的proxy數(shù)據(jù)</u>與<u>提供proxy數(shù)據(jù)給用戶使用</u>成功聯(lián)系在了一起

  • 通過(guò)元類實(shí)現(xiàn)單例模式:ConfigHandler沟优,其可以在任意模塊中以c = ConfigHandler()的形式獲得父能,而不是ConfigHandler.getInstance()

  • @LazyProperty懶加載屬性的裝飾器: 只有用到時(shí)才會(huì)加載并將值注入到__dict__、加載一次后值就不再變化净神、何吝;講解可見:http://www.reibang.com/p/708dc26f9b92——描述符or修飾符實(shí)現(xiàn)

    class LazyProperty(object):
        # 在被注解類方法被解釋器運(yùn)行的時(shí)候就會(huì)創(chuàng)建LazyProperty實(shí)例并返回
        def __init__(self, func):
            self.func = func
        """通過(guò)python描述符來(lái)實(shí)現(xiàn)"""
        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                # 會(huì)將結(jié)果值通過(guò)setattr方法存入到instance對(duì)象實(shí)例的__dict__中
                value = self.func(instance)
                setattr(instance, self.func.__name__, value)
                return value
    class ConfigHandler(withMetaclass(Singleton)):
        # 返回一個(gè)LazyProperty實(shí)例 
        @LazyProperty
        def serverHost(self):
            return os.environ.get("HOST", setting.HOST)
    c = ConfigHandler()
    # 會(huì)觸發(fā)ConfigHandler.__dict__["serverHost"], 然后接而觸發(fā)LazyProperty的__get__,value = self.func(instance)會(huì)得到真正serverHost函數(shù)的值后將其設(shè)置在ConfigHandler instance對(duì)象的__dict__中鹃唯,由于對(duì)象的__dict__["serverHost"]=value優(yōu)先級(jí)高于類的__dict__["serverHost"]=LazyProperty()對(duì)象爱榕,因此之后調(diào)用得到的是value結(jié)果
    print(c.serverHost)
    

    __get__只有訪問(wèn)類屬性的時(shí)候才會(huì)生效,這邊是通過(guò)setattr將serverHost設(shè)置成了ConfigHandler的類屬性

  • 封裝了一個(gè)請(qǐng)求工具類WebRequest:

    • 增加了異常處理的功能
    • 增加了日志功能
    • 請(qǐng)求頭會(huì)得到隨機(jī)UA
    • 設(shè)置重試
  • 使用click創(chuàng)建子命令:

    1. 得到一個(gè)click_group
    CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
    
    @click.group(context_settings=CONTEXT_SETTINGS)
    @click.version_option(version=VERSION)
    def cli():
        """ProxyPool cli工具"""
    
    1. 指定group下的子命令
    @cli.command(name="server")
    # 還可以設(shè)置參數(shù): @click.option('--count', default=1, help='Number of greetings.') --> def server(count)
    def server():
        """ 啟動(dòng)api服務(wù) """
        click.echo(BANNER)
        startServer()
    
    if __name__ == '__main__':
        cli()
    

    然后通過(guò)bash腳本同時(shí)開啟兩個(gè)進(jìn)程

    #!/usr/bin/env bash
    python proxyPool.py server &
    python proxyPool.py schedule
    
  • DbClient DB工廠類

    class DbClient(withMetaclass(Singleton)):
       def __init__(self, db_conn):
            self.parseDbConn(db_conn)
            self.__initDbClient()
    
        @classmethod
        def parseDbConn(cls, db_conn):
            db_conf = urlparse(db_conn)
            cls.db_type = db_conf.scheme.upper().strip()
            cls.db_host = db_conf.hostname
            cls.db_port = db_conf.port
            cls.db_user = db_conf.username
            cls.db_pwd = db_conf.password
            cls.db_name = db_conf.path[1:]
            return cls
    
        def __initDbClient(self):
            """
            init DB Client
            :return:
            """
            __type = None
            if "SSDB" == self.db_type:
                __type = "ssdbClient"
            elif "REDIS" == self.db_type:
                __type = "redisClient"
            else:
                pass
            assert __type, 'type error, Not support DB type: {}'.format(self.db_type)
            self.client = getattr(__import__(__type), "%sClient" % self.db_type.title())(host=self.db_host,
    port=self.db_port,
    username=self.db_user,
    password=self.db_pwd,
    db=self.db_name)
    
  • 繼承重寫logging.logger坡慌,可選參數(shù)為name, level=DEBUG, stream=True, file=True黔酥,讓每個(gè)功能函數(shù)都能生成單獨(dú)的日志文件,并進(jìn)行了可選控制。

    相比單例跪者,日志精度更細(xì)棵帽,但也使用起來(lái)也更麻煩,需要考慮什么地方需要渣玲。

  • 提供"擴(kuò)展代理"接口

    1. ProxyFetcher類中添加自定義的獲取代理的靜態(tài)方法逗概, 該方法需要以生成器(yield)形式返回host:ip格式的代理

    2. 添加好方法后,修改setting.py文件中的PROXY_FETCHER項(xiàng)下添加自定義方法的名字:

      PROXY_FETCHER = [
          "freeProxy01",    
          "freeProxy02",
          # ....
          "freeProxyCustom1"  #  # 確保名字和你添加方法名字一致
      ]
      

      schedule 進(jìn)程會(huì)每隔一段時(shí)間抓取一次代理忘衍,下次抓取時(shí)會(huì)自動(dòng)識(shí)別調(diào)用你定義的方法逾苫。

    實(shí)現(xiàn)方式

self.log.info("ProxyFetch : start")

# 從配置中拿執(zhí)行函數(shù)
for fetch_source in self.conf.fetchers:
    # 判斷ProxyFetcher中是否有定義、是否可調(diào)用
    fetcher = getattr(ProxyFetcher, fetch_source, None)
    if not fetcher:
        self.log.error("ProxyFetch - {func}: class method not exists!".format(func=fetch_source))
        continue
    if not callable(fetcher):
        self.log.error("ProxyFetch - {func}: must be class method".format(func=fetch_source))
        continue
    thread_list.append(_ThreadFetcher(fetch_source, proxy_dict))
    
for thread in thread_list:
    thread.setDaemon(True)
    thread.start()
for thread in thread_list:
    thread.join()
self.log.info("ProxyFetch - all complete!")
    
  • Cpython(默認(rèn)安裝的都是Cpython)中Dict和list枚钓、tuple都是線程安全

    • 以裝飾器的形式將過(guò)濾器將入到容器中

      
      class ProxyValidator(withMetaclass(Singleton)):
          pre_validator = []
          http_validator = []
          https_validator = []
      
          @classmethod
          def addPreValidator(cls, func):
              cls.pre_validator.append(func)
              return func
      
      # 實(shí)際上執(zhí)行了 formatValidator=ProxyValidator.addPreValidator(formatValidator)
      # 由于addPreValidator返回了func, 所以formatValidator還是原來(lái)的addPreValidator, 但在類定義的時(shí)候ProxyValidator.pre_validator添加了formatValidator方法
      @ProxyValidator.addPreValidator
      def formatValidator(proxy):
          """檢查代理格式"""
          verify_regex = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}"
          _proxy = findall(verify_regex, proxy)
          return True if len(_proxy) == 1 and _proxy[0] == proxy else False
      
      
      class DoValidator(object):
          """ 校驗(yàn)執(zhí)行器 """
          @classmethod
          def validator(cls, proxy):
              """
              校驗(yàn)入口
              Args:
                  proxy: Proxy Object
              Returns:
                  Proxy Object
              """
              http_r = cls.httpValidator(proxy)
              https_r = False if not http_r else cls.httpsValidator(proxy)
      
              proxy.check_count += 1
              proxy.last_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
              proxy.last_status = True if http_r else False
              if http_r:
                  if proxy.fail_count > 0:
                      proxy.fail_count -= 1
                  proxy.https = True if https_r else False
              else:
                  proxy.fail_count += 1
              return proxy
          
          @classmethod
          def preValidator(cls, proxy):
              for func in ProxyValidator.pre_validator:
                  if not func(proxy):
                      return False
              return True
      
  • starter-banner:?jiǎn)?dòng)橫幅

    • reflection: No铅搓、adjustment: cewnter、Stretch: Yes搀捷、width: 80
    • 還不錯(cuò)的font:
      • 5lineoblique——好看
      • banner3——清楚
      • bell——抽象
      • big——清晰
      • bigchief——等高線版本星掰、藝術(shù)
      • block——塊狀
      • bulbhead——可愛
      • larry3d——立體3d
      • ogre——清晰
      • puffy——清晰+一點(diǎn)可愛
      • slant——清晰+斜體
  • 定時(shí)器框架apschedule配置:

    scheduler = BlockingScheduler(logger=scheduler_log, timezone=timezone)
    
    scheduler.add_job(__runProxyCheck, 'interval', minutes=2, id="proxy_check", name="proxy檢查")
    
    executors = {
        # job_defaults中的max_instances也受限于max_workers, 所以要大于max_instances;此外max_workers也決定了同時(shí)能處理幾個(gè)同時(shí)發(fā)生的task
        'default': {'type': 'threadpool', 'max_workers': 20},
        'processpool': ProcessPoolExecutor(max_workers=5)
    }
    job_defaults = {
        # 合并將所有這些錯(cuò)過(guò)的執(zhí)行合并為一個(gè), 默認(rèn)為True嫩舟。 如果是定時(shí)的存儲(chǔ)任務(wù)的話氢烘,參數(shù)肯定不同,不能合并所以得手動(dòng)設(shè)置False
        # 像本項(xiàng)目每隔一段時(shí)間抓取到的數(shù)據(jù)也不太一樣至壤,所以無(wú)法直接當(dāng)作一次錯(cuò)誤任務(wù)合并
        'coalesce': False,
        # 默認(rèn)情況下,每個(gè)作業(yè)只允許同時(shí)運(yùn)行一個(gè)實(shí)例枢纠。這意味著像街,如果作業(yè)即將運(yùn)行,但前一次運(yùn)行尚未完成晋渺,則認(rèn)為最近一次運(yùn)行失敗镰绎。通過(guò)在添加作業(yè)時(shí)使用關(guān)鍵字參數(shù),可以設(shè)置調(diào)度程序允許同時(shí)運(yùn)行的特定作業(yè)的最大實(shí)例數(shù)木西。默認(rèn)為1
        'max_instances': 10,
        # 框架會(huì)檢查每個(gè)錯(cuò)過(guò)的執(zhí)行時(shí)間畴栖,如果當(dāng)前還在misfire_grace_time時(shí)間內(nèi),則會(huì)重新嘗試執(zhí)行任務(wù)八千,設(shè)高點(diǎn)就可以避免任務(wù)被漏掉執(zhí)行吗讶。默認(rèn)為1
        # "misfire_grace_time": 5  該項(xiàng)目未使用,而是采用了多任務(wù)實(shí)例來(lái)規(guī)避任務(wù)錯(cuò)過(guò)執(zhí)行==>即官方給出兩種方案中的另一種恋捆。任務(wù)錯(cuò)過(guò)信息:Run time of job "say (trigger: interval[0:00:02])" was missed by 0:00:03.010383
    }
    
    scheduler.configure(executors=executors, job_defaults=job_defaults, timezone=timezone)
    scheduler.start()
    
    

    job_defaults參數(shù)含義見官方文檔

    注: 經(jīng)過(guò)測(cè)試照皆,在add_task中的func如果起了多個(gè)線程,其執(zhí)行不受限于sche的配置

  • Python中如果只是使用全局變量則不需要用global聲明(因?yàn)樽兞克褜?huì)由內(nèi)往外)沸停,但是如果需要修改則需要用global聲明膜毁,否則無(wú)法找到相應(yīng)變量

  • 生成器:使用了yield關(guān)鍵字的函數(shù)就是生成器,生成器是一類特殊的迭代器。

    作用:

    • 處理大量數(shù)據(jù):生成器一次返回一個(gè)結(jié)果瘟滨,而不是一次返回所有結(jié)果候醒。比如sum([i for i in range(10000000000000)])會(huì)卡機(jī);sum(i for i in range(10000000000000))則不會(huì)
    • 代碼更加簡(jiǎn)潔:可以減少變量杂瘸、空間
    • 迭代器本身的作用

    yield關(guān)鍵字有兩點(diǎn)作用:

    保存當(dāng)前運(yùn)行狀態(tài)(斷點(diǎn))倒淫,然后暫停執(zhí)行,即將生成器(函數(shù))掛起胧沫;可以使用next()函數(shù)讓生成器從斷點(diǎn)處繼續(xù)執(zhí)行昌简,即喚醒生成器(函數(shù))
    將yield關(guān)鍵字后面表達(dá)式的值作為返回值返回,此時(shí)可以理解為起到了return的作用

    def __runProxyFetch():     
        for proxy in proxy_fetcher.run():
            proxy_queue.put(proxy)
            
    class Fetcher(object):
        name = "fetcher"
        def run(self):
            # ...
            # 相比使用生成推導(dǎo)式 return [p for p in proxy_dict.values() if DoValidator.preValidator(p.proxy)]绒怨, 使用yield生成器可以節(jié)省空間
            for _ in proxy_dict.values():
                    if DoValidator.preValidator(_.proxy):
                        yield _
    
  • 應(yīng)用部署:

    ①對(duì)apk換源纯赎;②設(shè)置時(shí)區(qū)

    FROM python:3.6-alpine
    # ..
    # apk repository
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
    # timezone
    RUN apk add -U tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && apk del tzdata
    # ...
    ENTRYPOINT [ "sh", "start.sh" ]
    
    

    docker-compose.yml: 鏡像還沒(méi)編譯好的情況。(如果自己改了功能并啟用的話南蹂,需要用這種犬金;或者自己發(fā)布鏡像后用后一種)

    version: '2'
    services:
      proxy_pool:
        build: .
        container_name: proxy_pool
        ports:
          - "5010:5010"
        links:
          - proxy_redis
        environment:
          DB_CONN: "redis://@proxy_redis:6379/0"
      proxy_redis:
        image: "redis"
        container_name: proxy_redis
    

    docker-compose.yml:別人鏡像已經(jīng)編譯好并上傳

    version: "3"
    services:
      redis:
        image: redis
        expose:
          - 6379
      web:
        restart: always
        image: jhao104/proxy_pool
        environment:
          - DB_CONN=redis://redis:6379/0
        ports:
          - "5010:5010"
        depends_on:
          - redis
    

scheduler的邏輯

proxy_pool時(shí)序圖.png

項(xiàng)目目錄結(jié)構(gòu)默寫:

  • settings: 配置文件
  • main:?jiǎn)?dòng)文件
  • api:提供獲取proxy數(shù)據(jù)接口
  • handler:
    • loggerHandler:日志類
    • configHandler:?jiǎn)卫呐渲媒涌陬?/li>
    • ProxyHandler: Proxy CRUD操作類
  • fetcher: 代理數(shù)據(jù)獲取類
  • db:
    • dbClinet: 存儲(chǔ)功能接口類
    • redisClient:存儲(chǔ)功能實(shí)現(xiàn)類
  • helper
    • scheduler: 定時(shí)任務(wù)的定義與啟動(dòng)類
    • validator: proxy有效性校驗(yàn)類
    • check: 具體執(zhí)行校驗(yàn)邏輯類
    • proxy: 獲取的proxy數(shù)據(jù)封裝類
  • utils:
    • lazyProperty: 懶加載描述器
    • singleton: 單例管理器類
    • six: python2與python3兼容類
    • webRequest: 網(wǎng)絡(luò)請(qǐng)求封裝類
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市六剥,隨后出現(xiàn)的幾起案子晚顷,更是在濱河造成了極大的恐慌,老刑警劉巖疗疟,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邑贴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡送粱,警方通過(guò)查閱死者的電腦和手機(jī)讲逛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)店诗,“玉大人裹刮,你說(shuō)我怎么就攤上這事∨尤常” “怎么了捧弃?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)擦囊。 經(jīng)常有香客問(wèn)我违霞,道長(zhǎng),這世上最難降的妖魔是什么瞬场? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任葛家,我火速辦了婚禮,結(jié)果婚禮上泌类,老公的妹妹穿的比我還像新娘癞谒。我一直安慰自己底燎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布弹砚。 她就那樣靜靜地躺著双仍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桌吃。 梳的紋絲不亂的頭發(fā)上朱沃,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音茅诱,去河邊找鬼逗物。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瑟俭,可吹牛的內(nèi)容都是我干的翎卓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼摆寄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼失暴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起微饥,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤逗扒,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后欠橘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矩肩,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年肃续,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了黍檩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痹升,死狀恐怖建炫,靈堂內(nèi)的尸體忽然破棺而出畦韭,到底是詐尸還是另有隱情疼蛾,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布艺配,位于F島的核電站察郁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏转唉。R本人自食惡果不足惜皮钠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赠法。 院中可真熱鬧麦轰,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至新锈,卻和暖如春甲脏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妹笆。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工块请, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拳缠。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓墩新,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親脊凰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抖棘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第一部分 Python基礎(chǔ)篇(80題) 1、為什么學(xué)習(xí)Python狸涌? Python相對(duì)于其他編程語(yǔ)言有很多優(yōu)點(diǎn): ...
    清清子衿木子水心閱讀 1,697評(píng)論 0 1
  • 1.列出 5 個(gè)常用 Python 標(biāo)準(zhǔn)庫(kù)切省? python標(biāo)準(zhǔn)庫(kù)就是安裝python時(shí)默認(rèn)自帶的庫(kù),常用的標(biāo)準(zhǔn)庫(kù)有...
    千里尋花閱讀 243評(píng)論 0 0
  • 包(lib)帕胆、模塊(module) 在Python中朝捆,存在包和模塊兩個(gè)常見概念。 模塊:編寫Python代碼的py...
    清清子衿木子水心閱讀 3,801評(píng)論 0 27
  • 用于python面試整理懒豹,主要來(lái)源于http://www.cnblogs.com/skiler/p/6952707...
    十里江城閱讀 2,328評(píng)論 0 13
  • Astronomygonova - A wrapper for libnova -- Celestial Mech...
    JumboWu閱讀 8,613評(píng)論 0 41