妙用Hook來(lái)研究Python的Import機(jī)制
這兩天周末在家學(xué)習(xí)Python,我發(fā)現(xiàn)我們平常接觸最多的也就是import這條語(yǔ)句,這兩天在編寫(xiě)一些程序的時(shí)候恰恰需要import hook去完成一些操作,借著這個(gè)周末在家閑著沒(méi)事兒通過(guò)import hook這個(gè)命令,把Python的import機(jī)制了解了一下。
0x00 Import機(jī)制概述
從名字上可以推斷出,import hook這個(gè)命令是和Python的導(dǎo)入機(jī)制有所關(guān)聯(lián)。再具體一點(diǎn)的話,import hook的作用是把我們自己寫(xiě)的腳本直接注入到Python導(dǎo)入的例行操作里去。如果還要繼續(xù)往下說(shuō)的話,那我們首先應(yīng)該來(lái)了解一下import默認(rèn)的時(shí)候是如何處理的。
對(duì)于我們來(lái)說(shuō)的話,其實(shí)這個(gè)過(guò)程比較簡(jiǎn)單:當(dāng)Python的解釋器遇到import語(yǔ)句的時(shí)候,它回去查閱sys.path里面所有已經(jīng)儲(chǔ)存的目錄。這個(gè)列表初始化的時(shí)候,通常包含一些來(lái)自外部的庫(kù)(external libraries)或者是來(lái)自操作系統(tǒng)的一些庫(kù),當(dāng)然也會(huì)有一些類似于dist-package的標(biāo)準(zhǔn)庫(kù)在里面。這些目錄通常是被按照順序或者是直接去搜索想要的:如果說(shuō)他們當(dāng)中的一個(gè)包含有期望的package或者是module,這個(gè)package或者是module將會(huì)在整個(gè)過(guò)程結(jié)束的時(shí)候被直接提取出來(lái)。
我們可以寫(xiě)一段代碼來(lái)演示一下ImportError,運(yùn)行下面的代碼的時(shí)候,我們會(huì)catch一個(gè)exception,在程序結(jié)束之前,它可能會(huì)嘗試多個(gè)imports。
- #!/usr/bin/env python
 - #coding=utf8
 - try:
 - # Python 2.7-3.x
 - import json
 - except ImportError:
 - try:
 - # Python 2.6
 - import simplejson as json
 - except ImportError:
 - try:
 - from django.utils import simplejson as json
 - except ImportError:
 - raise Exception("Requires a JSON package!")
 
雖然說(shuō)這段sample寫(xiě)的很不beautiful,但是他可以在一定程度上增加我們寫(xiě)的程序或者package的可以執(zhí)行。慶幸的是我們僅僅需要用這種方式去處理極少數(shù)有價(jià)值的庫(kù),比如說(shuō)代碼中的Json庫(kù)。
0x01 關(guān)于__path__的更多細(xì)節(jié)
上文中提到的Python的Import流在大多數(shù)情況下是想描述一樣有用的,但是事實(shí)上遠(yuǎn)不止這些。他省略了一些我們可以根據(jù)需要調(diào)節(jié)的地方。
首先,__path__這個(gè)屬性是我們可以在__init__.py里面去定義的。你可以認(rèn)為他像一個(gè)sys.path的本地?cái)U(kuò)展并且只服務(wù)于我們導(dǎo)入的package的子模塊。換句話說(shuō),它包含目錄時(shí)應(yīng)該尋找一個(gè)package的子模塊被導(dǎo)入。默認(rèn)的情況下只有__init__.py的目錄,但是他可以擴(kuò)展到包含任何其他任何的路徑。
舉一個(gè)典型的例子就是把一些邏輯上的package分割成多個(gè)實(shí)際上的package,其實(shí)就是分割成多個(gè)distribution,一般情況下是不同的pypi包。舉個(gè)例子,讓我們假設(shè)構(gòu)造一個(gè)test.package,里面包含有test.client和test.server,他們?cè)趐ypi注冊(cè)的時(shí)候是按照兩個(gè)不同的distribution去注冊(cè)的,這樣的話用戶可以選擇其中的一個(gè)或多個(gè)distribution去安裝。我們需要設(shè)置test.__path__讓他們?nèi)ブ赶騮est.server和test.client的目錄(如果你只安裝了一個(gè)distribution的話只需要設(shè)置一個(gè))。聽(tīng)上去好像有點(diǎn)復(fù)雜,實(shí)際上Python有一個(gè)模塊叫做pkgutil,這個(gè)模塊的作用就是讓我們很輕松的去實(shí)現(xiàn)上述的功能,你只需要在test/__init__.py下面添加一下兩行就可以了。
- import pkgutil
 - __path__ = pkgutil.extend_path(__path__, __name__)
 
其實(shí)還有比這個(gè)還簡(jiǎn)單的方法,這里推薦一個(gè)文章給大家:http://doughellmann.com/PyMOTW/
0x02 真·鉤子:sys.meta_path和sys.path_hooks
讓我們繼續(xù),接著我們就會(huì)去分析import的過(guò)程,其實(shí)這部分正是這篇文章的重點(diǎn)。截下來(lái)說(shuō)的比如說(shuō)從zip文件或者是repo里面字節(jié)獲取模塊,或者是動(dòng)態(tài)的去用各種方法建立它們,比如說(shuō)是web服務(wù)、dll或者是RESTful API等等幾乎你可以想到的任何的方法。我也會(huì)提到一些各個(gè)獨(dú)立模塊之間拿坑爹的交互性,比如說(shuō)一個(gè)package檢測(cè)到自己被導(dǎo)入的時(shí)候,它能夠適應(yīng)和擴(kuò)展自己的接口。接著我們將會(huì)討論一下Python的安全增強(qiáng)沙箱,這個(gè)沙箱的作用是用來(lái)拒絕訪問(wèn)某些模塊或者是改變其某些功能。
這些功能其實(shí)都可以通過(guò)import hooks來(lái)實(shí)現(xiàn)。有兩種不同的hook,一種叫做meta hook(sys.meta_path),另一種叫做path hook(sys.path_hooks)。盡管他們?cè)趦蓚€(gè)差不多的導(dǎo)入流的階段被調(diào)用,但是他們被創(chuàng)建的時(shí)候還是會(huì)取決于兩個(gè)東西,一個(gè)叫做模塊查找器(Module Finder),一個(gè)叫做模塊加載器(Module Loader)。
模塊查找器其實(shí)是一種簡(jiǎn)單的用來(lái)查找模塊的對(duì)象,他(find_module)的使用方法如下面所示:
- finder.find_module(fullname, path=None)
 
他需要把一個(gè)完整的模塊的名字當(dāng)做參數(shù)傳進(jìn)去,path則為這個(gè)模塊的路徑。這個(gè)對(duì)象的可以完成以下三件事中的任意一件:
- 拋出一個(gè)異常,然后完全取消所有的導(dǎo)入流程
 - 返回一個(gè)None,意思是被導(dǎo)入的這個(gè)模塊不能夠被這個(gè)查找器所找到。但是他仍然可以被導(dǎo)入流的下一個(gè)階段所找到,比如說(shuō)一些自定義的查找器或者是Python的標(biāo)準(zhǔn)導(dǎo)入機(jī)制。
 - 返回一個(gè)加載器對(duì)象用來(lái)加載實(shí)際的模塊。
 
下一個(gè)就是模塊加載器,模塊加載器其實(shí)就是一個(gè)用來(lái)加載制定模塊的對(duì)象,它(load_module)的使用方法如下面的代碼所示:
- loader.load_module(fullname)
 
這里需要在強(qiáng)調(diào)一次,fullname參數(shù)需要傳進(jìn)去一個(gè)我們想要加載的模塊的全名。返回值應(yīng)當(dāng)是一個(gè)模塊的對(duì)象,***的結(jié)果當(dāng)然就是完成導(dǎo)入對(duì)象的操作。需要注意的是,這些模塊可能已經(jīng)被導(dǎo)入了,或者是復(fù)制這些模塊的功能用來(lái)返回這些已經(jīng)存在的模塊。下面是這個(gè)函數(shù)的原型:
- def load_module(self, fullname):
 - if fullname in sys.modules: return sys.modules[fullname]
 
如果在這一階段出現(xiàn)了任何錯(cuò)誤,模塊加載器應(yīng)該拋出一個(gè)ImportError的異常
0x03 自己構(gòu)造一個(gè)加載器:
上面這些僅僅是一些理論,其實(shí)吧PEP302標(biāo)準(zhǔn)里面都描述了這些。在實(shí)際當(dāng)中,其實(shí)模塊加載器和模塊查找器可以是同一個(gè)對(duì)象,也就是說(shuō)find_module可以去return self。舉個(gè)例子,其實(shí)這個(gè)簡(jiǎn)單的hook可以去阻止任何特定的模塊被導(dǎo)入:
- #!/usr/bin/env python
 - #coding=utf8
 - import sys
 - class ImportBlocker(object):
 - def __init__(self, *args):
 - self.module_names = args
 - def find_module(self, fullname, path=None):
 - if fullname in self.module_names:
 - return self
 - return None
 - def load_module(self, name):
 - raise ImportError("%s is blocked and cannot be imported" % name)
 - sys.meta_path = [ImportBlocker('httplib')]
 
一旦我們?cè)趕ys.meta_path中加載了這個(gè)hook,他就會(huì)去阻止任何導(dǎo)入的新模塊并且檢查他是否存在于我們的列表里。如果我們?nèi)ナ褂肦equest庫(kù)的時(shí)候,這個(gè)hook也會(huì)同樣起作用。
Import Request
執(zhí)行這條語(yǔ)句會(huì)失敗,因?yàn)閞equest是在urllib3內(nèi)部使用的,進(jìn)而去限制httplib的使用。但是一個(gè)hook要是沒(méi)事兒干總?cè)r截調(diào)用別的模塊似乎沒(méi)啥太大的意思,咱們換個(gè)別的玩法。如果說(shuō)總是拒絕調(diào)用特定的模塊,我們?yōu)樯恫挥靡粋€(gè)warning去代替呢?這樣的話,這個(gè)hook就可以幫我們檢測(cè)被導(dǎo)入到項(xiàng)目當(dāng)中又被棄用的模塊。代碼如下:
- # !/usr/bin/env python
 - # coding=utf-8
 - import logging
 - import imp
 - import sys
 - class WarnOnImport(object):
 - def __init__(self, *args):
 - self.module_names = args
 - def find_module(self, fullname, path=None):
 - if fullname in self.module_names:
 - self.path = path
 - return self
 - return None
 - def load_module(self, name):
 - if name in sys.modules:
 - return sys.modules[name]
 - module_info = imp.find_module(name, self.path)
 - module = imp.load_module(name, *module_info)
 - sys.modules[name] = module
 - logging.warning("Imported deprecated module %s", name)
 - return module
 - sys.meta_path = [WarnOnImport('getopt', 'optparse')]
 
為了去訪問(wèn)一個(gè)正常的導(dǎo)入機(jī)制,我們可以嘗試使用imp。它的find_module和load_module函數(shù)和我們要導(dǎo)入的hook具有相同的名字。但是imp提供的功能更強(qiáng)大,比如說(shuō)還包括了load_source和load_compile這些功能甚至可以從頭來(lái)初始化一個(gè)模塊(new_module)。















 
 
 













 
 
 
 