聚合国内IT技术精华文章,分享IT技术精华,帮助IT从业人士成长

使用PyV8解析HTML文档

2013-04-07 07:46 浏览: 2481281 次 我要评论(0 条) 字号:

什么是PyV8?

PyV8是一个Python封装V8引擎的壳。它提供了简单可用的API,能够利用python来构建出JavaScript的运行时环境。

PyV8能用来干什么?

在nodejs火热流行的时代,或许很少人关注这个基于python简单封装的v8引擎。在某些方面,它比nodejs简洁,而它们拥有同样的本质基础,使得它具有和nodejs相似的潜力。

既然是基于v8的,那么利用它来解析dom和执行javascript是理所当然的。试想一下,如果我们能够建立一个系统把html文档结构解析成w3c dom树,那么我们就可以在上面运行任何javascript,使得这个系统事实上成为一个浏览器(尽管缺少渲染和显示的部分)。

不过,pyv8提供的是纯js运行环境,因此,要解析html文档,还需要为它构建出w3c dom以及浏览器的环境。实现dom和浏览器接口毫无疑问是件力气活,幸运的是,pyv8的官方demo中为我们做了80%以上的工作,提供了w3c.py和browser.py的支持!我们所要做的是在这基础上做一些基本的修改。

好吧,让我们来实践一下:

首先我们构建一个基本的js运行环境,参考commonjs的api实现js基础的模块加载和执行。这一部分还不涉及dom和浏览器,基本上是js原生环境。

  1. import os, re, platform
  2. from PyV8 import JSContext, JSError
  3.  
  4. from logger import logger
  5.  
  6. class CommonJS():
  7.     _js_path = [os.path.dirname(__file__), os.path.join(os.path.dirname(__file__),'core')]
  8.     _js_logger = logger().instance()
  9.  
  10.     def __init__(self):
  11.         self._js_threadlock = False
  12.         self._js_ctx = JSContext(self)
  13.         self._js_modules = {}
  14.         self._loaded_modules = {}
  15.  
  16.         for jsroot in CommonJS._js_path:
  17.             for (root, dirs, files) in os.walk(jsroot):
  18.                 for _file in files:
  19.                     m = re.compile('(.*).js$').match(_file)
  20.                     relpath = os.path.relpath(root, jsroot)
  21.                     namespace = re.sub(r'^.', '', relpath)
  22.                     namespace = re.sub(r'^\', '/', namespace)
  23.                     if(namespace):
  24.                         namespace = namespace + '/'
  25.                     if(m):
  26.                         self._js_modules.update({namespace + m.group(1) : os.path.join(root,_file)})
  27.  
  28.         self.execute("var exports;");               
  29.    
  30.     @classmethod
  31.     def append(path):
  32.         if(path not in CommonJS._js_path):
  33.             CommonJS._js_path.append(path)
  34.  
  35.     def require(self, module):
  36.         if(not self._js_modules.has_key(module)):
  37.             raise Exception, "unknown module `" + module + "`"
  38.         path = self._js_modules[module]
  39.  
  40.         if(not self._loaded_modules.has_key(path)):
  41.             self._js_logger.info("loading module <%s>...", module)
  42.             code = file(path).read()
  43.             try:
  44.                 code = code.decode('utf-8')
  45.                 if(platform.system() == 'Windows'):
  46.                     code = code.encode('utf-8')
  47.                 self._js_ctx.eval(code)
  48.             except JSError, ex:
  49.                 self._js_logger.error(ex)
  50.                 self._js_logger.debug(ex.stackTrace)
  51.                 raise Exception, ex
  52.             self._loaded_modules[path] = self._js_ctx.locals.exports
  53.             return self._loaded_modules[path]
  54.         else:
  55.             return self._loaded_modules[path]
  56.  
  57.     def execute(self, code, args = []):
  58.         self._js_ctx.enter()
  59.        
  60.         # use lock while jscode executing to make mutil-thread work
  61.         while self._js_threadlock:
  62.             pass
  63.         self._js_threadlock = True
  64.         try:
  65.             if(isinstance(code, basestring)):
  66.                 code = code.decode('utf-8')
  67.                 if(platform.system() == 'Windows'):
  68.                     code = code.encode('utf-8')
  69.                 r = self._js_ctx.eval(code)
  70.             else:
  71.                 r = apply(code, args)
  72.             return r
  73.         except JSError, ex:
  74.             self._js_logger.error(ex)
  75.             self._js_logger.debug(ex.stackTrace)
  76.             raise Exception, ex
  77.         finally:
  78.             self._js_threadlock = False
  79.             self._js_ctx.leave()

好了,我们可以测试一下:

  1. from commonjs import CommonJS
  2. ctx = CommonJS()
  3. ctx.append('/my/js/path')
  4. print ctx.execute('require("base"); QW.provide("Test",{}); exports = QW.Test');

结果是
[JSObject]

在这基础上,我们进一步实现整个运行时环境和一些内置方法:

  1. #JavaScript HTML Context for PyV8
  2.  
  3. from PyV8 import JSClass
  4. from logger import logger
  5. import re, threading, hashlib
  6.  
  7. import urllib,urllib2
  8.  
  9. from w3c import parseString, Document, HTMLElement
  10. from commonjs import CommonJS
  11.  
  12. import browser
  13.  
  14. from StringIO import StringIO
  15. import gzip
  16.  
  17. class JSR(CommonJS, browser.HtmlWindow):
  18.     def __init__(self, url_or_dom, charset=None, headers={}, body={}, timeout=2):
  19.         urllib2.socket.setdefaulttimeout(timeout)
  20.         jsonp = False
  21.  
  22.         if(isinstance(url_or_dom, Document)):
  23.             url = "localhost:document"
  24.             dom = url_or_dom
  25.  
  26.         elif(url_or_dom.startswith('<')):
  27.             url = "localhost:string"
  28.             dom = parseString(url_or_dom)
  29.  
  30.         else: #url
  31.             url = url_or_dom
  32.             if(not re.match(r'w+://', url)):
  33.                 url = "http://" + url
  34.  
  35.             request = urllib2.Request(url, urllib.urlencode(body), headers=headers) 
  36.             response = urllib2.urlopen(url)
  37.            
  38.             contentType = response.headers.get('Content-Type')
  39.  
  40.             if(contentType):
  41.                 #print contentType
  42.                 t = re.search(r'x-javascript|json', contentType)
  43.                 if(t):
  44.                     jsonp = True
  45.                 m = re.match(r'^.*;s*charset=(.*)$', contentType)
  46.                 if(m):
  47.                     charset = m.group(1) 
  48.                 #print charset
  49.  
  50.             if(not charset):
  51.                 charset = 'utf-8' #default charset
  52.                 # guess charset from httpheader
  53.  
  54.             html = response.read()
  55.             encoding = response.headers.get('Content-Encoding')
  56.  
  57.             if(encoding and encoding == 'gzip'):
  58.                 buf = StringIO(html)
  59.                 f = gzip.GzipFile(fileobj=buf)
  60.                 html = f.read()   
  61.                            
  62.             self.__html__ = html
  63.             html = unicode(html, encoding=charset, errors='ignore')
  64.             dom = parseString(html)   
  65.  
  66.         navigator = browser.matchNavigator(headers.get('User-Agent') or '')
  67.            
  68.         browser.HtmlWindow.__init__(self, url, dom, navigator)
  69.         CommonJS.__init__(self)
  70.        
  71.         self.console = JSConsole(self._js_logger)
  72.        
  73.         for module in "base, array.h, function.h, helper.h, object.h, string.h, date.h, custevent, selector, dom_retouch".split(","):
  74.             self.execute(self.require, [module.strip()])
  75.        
  76.         if(jsonp):
  77.             code = "window.data=" + html.encode('utf-8')
  78.             self.execute(code)
  79.             #print code
  80.  
  81.         self._js_logger.info('JavaScript runtime ready.')
  82.  
  83.     _js_timer_map = {}
  84.  
  85.     def _js_execTimer(self, id, callback, delay, repeat = False):
  86.         code = '(function f(){ _js_timers[' + str(id) + '][1].code();'
  87.         if(repeat):
  88.             code = code + '_js_execTimer(' + str(id) + ', f, ' + str(delay) + ', true);'
  89.         code = code + '})();'
  90.  
  91.         #thread locking
  92.         self._js_timer_map[id] = threading.Timer(delay / 1000.0, lambda: self.execute(code))
  93.         self._js_timer_map[id].start()
  94.  
  95.     def setTimeout(self, callback, delay):
  96.         timerId = super(JSR, self).setTimeout(callback, delay)
  97.         self._js_execTimer(timerId, callback, delay, False)
  98.         return timerId
  99.  
  100.     def clearTimeout(self, timerId):
  101.         if(timerId in self._js_timer_map):
  102.             self._js_timer_map[timerId].cancel()
  103.             self._js_timer_map[timerId] = None
  104.             super(JSR, self).clearTimeout(timerId)
  105.  
  106.     def setInterval(self, callback, delay):
  107.         timerId = super(JSR, self).setInterval(callback, delay)
  108.         self._js_execTimer(timerId, callback, delay, True)
  109.         return timerId       
  110.    
  111.     def clearInterval(self, timerId):
  112.         if(timerId in self._js_timer_map):
  113.             self._js_timer_map[timerId].cancel()
  114.             self._js_timer_map[timerId] = None
  115.             super(JSR, self).clearTimeout(timerId)
  116.  
  117.     def md5(self, str):
  118.         return hashlib.md5(str).hexdigest()
  119.  
  120. class JSConsole(JSClass):
  121.     def __init__(self, logger):
  122.         self._js_logger = logger
  123.     def log(self, msg):
  124.         self._js_logger.info(str(msg).decode('utf-8'))

到此为止,一个简单的运行时环境就实现了。

使用方法:

  1. from jsruntime import JSR
  2.  
  3. rt = JSR('www.baidu.com')
  4. rt.execute('alert(document.title)') #结果是“百度一下,你就知道”
  5. rt.execute('document.querySelector("body div")') #获得body下匹配的第一个div
  6. ...

利用它,你就可以用js来处理抓取的页面元素,甚至通过运行页面上的js来获取动态加载的内容,只要继续扩展,可以用它来实现一个相当不错的页面内容采集服务,而采集规则,不再只能是简单的正则,而是可以用js像处理任何动态网页那样来处理抓取的页面。

有兴趣滴童鞋可以研究下,pyv8的潜力还是很大的,大家一起期待一下~



网友评论已有0条评论, 我也要评论

发表评论

*

* (保密)

Ctrl+Enter 快捷回复