妙用Hook來研究Python的Import機制

分類:技術 時間:2017-01-13

這兩天周末在家學習Python,我發現我們平常接觸最多的也就是import這條語句,這兩天在編寫一些程序的時候恰恰需要import hook去完成一些操作,借著這個周末在家閑著沒事兒通過import hook這個命令,把Python的import機制了解了一下。

0x00 Import機制概述

從名字上可以推斷出,import hook這個命令是和Python的導入機制有所關聯。再具體一點的話,import hook的作用是把我們自己寫的腳本直接注入到Python導入的例行操作里去。如果還要繼續往下說的話,那我們首先應該來了解一下import默認的時候是如何處理的。

對于我們來說的話,其實這個過程比較簡單:當Python的解釋器遇到import語句的時候,它回去查閱sys.path里面所有已經儲存的目錄。這個列表初始化的時候,通常包含一些來自外部的庫(external libraries)或者是來自操作系統的一些庫,當然也會有一些類似于dist-package的標準庫在里面。這些目錄通常是被按照順序或者是直接去搜索想要的:如果說他們當中的一個包含有期望的package或者是module,這個package或者是module將會在整個過程結束的時候被直接提取出來。

我們可以寫一段代碼來演示一下ImportError,運行下面的代碼的時候,我們會catch一個exception,在程序結束之前,它可能會嘗試多個imports。

#!/usr/bin/envpython
#coding=utf8
try:
#Python2.7-3.x
importjson
exceptImportError:
try:
#Python2.6
importsimplejsonasjson
exceptImportError:
try:
fromdjango.utilsimportsimplejsonasjson
exceptImportError:
raiseException(quot;RequiresaJSONpackage!quot;)

雖然說這段sample寫的很不beautiful,但是他可以在一定程度上增加我們寫的程序或者package的可以執行。慶幸的是我們僅僅需要用這種方式去處理極少數有價值的庫,比如說代碼中的Json庫。

0x01 關于__path__的更多細節

上文中提到的Python的Import流在大多數情況下是想描述一樣有用的,但是事實上遠不止這些。他省略了一些我們可以根據需要調節的地方。

首先,__path__這個屬性是我們可以在__init__.py里面去定義的。你可以認為他像一個sys.path的本地擴展并且只服務于我們導入的package的子模塊。換句話說,它包含目錄時應該尋找一個package的子模塊被導入。默認的情況下只有__init__.py的目錄,但是他可以擴展到包含任何其他任何的路徑。

舉一個典型的例子就是把一些邏輯上的package分割成多個實際上的package,其實就是分割成多個distribution,一般情況下是不同的pypi包。舉個例子,讓我們假設構造一個test.package,里面包含有test.client和test.server,他們在pypi注冊的時候是按照兩個不同的distribution去注冊的,這樣的話用戶可以選擇其中的一個或多個distribution去安裝。我們需要設置test.__path__讓他們去指向test.server和test.client的目錄(如果你只安裝了一個distribution的話只需要設置一個)。聽上去好像有點復雜,實際上Python有一個模塊叫做pkgutil,這個模塊的作用就是讓我們很輕松的去實現上述的功能,你只需要在test/__init__.py下面添加一下兩行就可以了。

importpkgutil
__path__=pkgutil.extend_path(__path__,__name__)

其實還有比這個還簡單的方法,這里推薦一個文章給大家: http://doughellmann.com/PyMOTW/

0x02 真middot;鉤子:sys.meta_path和sys.path_hooks

讓我們繼續,接著我們就會去分析import的過程,其實這部分正是這篇文章的重點。截下來說的比如說從zip文件或者是repo里面字節獲取模塊,或者是動態的去用各種方法建立它們,比如說是web服務、dll或者是RESTful API等等幾乎你可以想到的任何的方法。我也會提到一些各個獨立模塊之間拿坑爹的交互性,比如說一個package檢測到自己被導入的時候,它能夠適應和擴展自己的接口。接著我們將會討論一下Python的安全增強沙箱,這個沙箱的作用是用來拒絕訪問某些模塊或者是改變其某些功能。

這些功能其實都可以通過import hooks來實現。有兩種不同的hook,一種叫做meta hook(sys.meta_path),另一種叫做path hook(sys.path_hooks)。盡管他們在兩個差不多的導入流的階段被調用,但是他們被創建的時候還是會取決于兩個東西,一個叫做模塊查找器(Module Finder),一個叫做模塊加載器(Module Loader)。

模塊查找器其實是一種簡單的用來查找模塊的對象,他(find_module)的使用方法如下面所示:

finder.find_module(fullname,path=None)

他需要把一個完整的模塊的名字當做參數傳進去,path則為這個模塊的路徑。這個對象的可以完成以下三件事中的任意一件:

  • 拋出一個異常,然后完全取消所有的導入流程
  • 返回一個None,意思是被導入的這個模塊不能夠被這個查找器所找到。但是他仍然可以被導入流的下一個階段所找到,比如說一些自定義的查找器或者是Python的標準導入機制。
  • 返回一個加載器對象用來加載實際的模塊。

下一個就是模塊加載器,模塊加載器其實就是一個用來加載制定模塊的對象,它(load_module)的使用方法如下面的代碼所示:

loader.load_module(fullname)

這里需要在強調一次,fullname參數需要傳進去一個我們想要加載的模塊的全名。返回值應當是一個模塊的對象,最后的結果當然就是完成導入對象的操作。需要注意的是,這些模塊可能已經被導入了,或者是復制這些模塊的功能用來返回這些已經存在的模塊。下面是這個函數的原型:

defload_module(self,fullname):
iffullnameinsys.modules:returnsys.modules[fullname]

如果在這一階段出現了任何錯誤,模塊加載器應該拋出一個ImportError的異常

0x03 自己構造一個加載器:

上面這些僅僅是一些理論,其實吧PEP302標準里面都描述了這些。在實際當中,其實模塊加載器和模塊查找器可以是同一個對象,也就是說find_module可以去return self。舉個例子,其實這個簡單的hook可以去阻止任何特定的模塊被導入:

#!/usr/bin/envpython
#coding=utf8
importsys
classImportBlocker(object):
def__init__(self,*args):
self.module_names=args
deffind_module(self,fullname,path=None):
iffullnameinself.module_names:
returnself
returnNone
defload_module(self,name):
raiseImportError(quot;%sisblockedandcannotbeimportedquot;%name)
sys.meta_path=[ImportBlocker('httplib')]

一旦我們在sys.meta_path中加載了這個hook,他就會去阻止任何導入的新模塊并且檢查他是否存在于我們的列表里。如果我們去使用Request庫的時候,這個hook也會同樣起作用。

Import Request

執行這條語句會失敗,因為request是在urllib3內部使用的,進而去限制httplib的使用。但是一個hook要是沒事兒干總去攔截調用別的模塊似乎沒啥太大的意思,咱們換個別的玩法。如果說總是拒絕調用特定的模塊,我們為啥不用一個warning去代替呢?這樣的話,這個hook就可以幫我們檢測被導入到項目當中又被棄用的模塊。代碼如下:

#!/usr/bin/envpython
#coding=utf-8
importlogging
importimp
importsys
classWarnOnImport(object):
def__init__(self,*args):
self.module_names=args
deffind_module(self,fullname,path=None):
iffullnameinself.module_names:
self.path=path
returnself
returnNone
defload_module(self,name):
ifnameinsys.modules:
returnsys.modules[name]
module_info=imp.find_module(name,self.path)
module=imp.load_module(name,*module_info)
sys.modules[name]=module
logging.warning(quot;Importeddeprecatedmodule%squot;,name)
returnmodule
sys.meta_path=[WarnOnImport('getopt','optparse')]

為了去訪問一個正常的導入機制,我們可以嘗試使用imp。它的find_module和load_module函數和我們要導入的hook具有相同的名字。但是imp提供的功能更強大,比如說還包括了load_source和load_compile這些功能甚至可以從頭來初始化一個模塊(new_module)。


Tags: Python

文章來源:http://zhuanlan.51cto.com/art/201701/527713.htm


ads
ads

相關文章
ads

相關文章

ad