在使用python抓取網頁的過程中,有的時候需要執行某些簡單的javascript,以獲得自己需要的內容,例如執行js里面的document.write或者document.getElementById等。自己解析js代碼顯然有點吃力不討好,因此最好能找到一些可以解析執行js的python庫。
google之可以找到三個候選者,分別是微軟的ScriptControl,v8的python移植PyV8,還有SpiderMonkey的Python移植Python-Spidermonkey。其中ScriptControl只能在windows上運行,需要win32com庫;PyV8能在windows和*nix上運行,但是需要裝PyV8庫;而SpiderMonkey是mozilla的js引擎在python上的移植,感覺已經不太活躍,因此沒用。
微軟的ScriptControl中對執行js最重要的方法就是addObject與eval,通過addObject,我們可以向js執行環境注入一個我們自定義的document對象,通過eval方法,我們可以執行一段js代碼。注入自定義對象需要使用win32com.server.util.wrap方法,將一個python對象包裝為COM對象,例如假設我們想注入一個只實現了write方法的document對象,代碼是這樣的:
import win32com.server.util, win32com.client
class win32Doc:
_public_methods_ = ['write']
def write(self, s):
print s
doc = win32Doc()
jsengine = win32com.client.Dispatch('MSScriptControl.ScriptControl')
jsengine.language = 'JavaScript'
jsengine.allowUI = False
jsengine.addObject('document', win32com.server.util.wrap(doc))
jsengine.eval('document.write("hello, world")')
在windows里運行這段python代碼,最終就會打印出hello, world來。如果我們希望從python里讀取js通過document.write寫入的字符串並進行解析,只要給上面的win32Doc類添加對應的方法(例如read),就可以讀取並解析HTML代碼,並進行進一步處理了。
對PyV8來說,原理也是類似的,不過在具體機制上有所不同而已。在PyV8中需要在初始化的時候加入一個全局對象,其他的對象都是掛在全局對象之下的,例如document只是全局對象的一個屬性而已(實際上,document對象就是window對象的一個屬性么),當然,這個屬性對應的實際上是一個對象。需要注意的是,PyV8在處理字符串編碼的時候讓人很迷惑,在windows下它需要js的編碼為UTF8,而在Linux下只要求寬字符串,即python里的unicode,而在內部的字符串都是UTF8編碼的。至於為何如此,熊貓也騷擾過開發PyV8的flier,貌似是V8自己的feature。示例代碼是這樣的:
import PyV8
class v8Doc(PyV8.JSClass):
def write(self, s):
print s.decode('utf-8')
class Global(PyV8.JSClass):
def __init__(self):
self.document = v8Doc()
glob = Global()
ctxt = PyV8.JSContext(glob)
ctxt.enter()
#or ctxt.eval(u'document.write("你好,中國")') for Linux
ctxt.eval(u'document.write("你好,中國")'.encode('utf-8'))
上面只是在python里模擬執行js的document.write的大體思路,如果還需要執行其他的js代碼對DOM樹進行操縱,那就一個個添加對應的方法好了。當然,這個添加也要保持一個限度,不然添加的方法太多,代碼會非常復雜,相當於自己已經開始實現一個DOM樹處理和解析的完全封裝了,如果是這樣,還不如使用自動化接口操縱瀏覽器,例如通過js/vbs操縱IE,或者在后台進行自動化批處理的話,使用一些headless browser軟件,例如phantomjs,這就留待以后再說了。