Python 编程语言于 1994 年问世,自新千年以来,这种语言获得了极大的成功。衡量一种语言成功与否的标准之一就是其实现的数量。最知名也是最常用的 Python 实现称为 CPython。此外还有其他一些成功的项目,例如 Jython(在 Java ? 运行时中工作的 Python 语言)和 IronPython(在 .NET 平台上工作的 Python 语言)。所有这些项目都是开放源码的,而 Python 在开放源码软件世界中始终有着极大的影响力。
Python 实现的一个由来已久的目标就是支持纯语言设计,通过以自己的方式指定相关语言来 “引导” Python 的定义,而不是按照 C 和 Java 等其他语言的方式做出规定。PyPy 项目正是应此需求而出现的一种 Python 实现。PyPy 表示 “用 Python 实现的 Python”,但实际上它是使用一个称为 RPython 的 Python 子集实现的。更准确地来说,PyPy 自身就是一种运行时,您可以在其中插入任何语言。
PyPy 整洁的语言设计使之非常适合嵌入低级优化器,提供诸多优化优势。具体来说,PyPy 集成了一种即时 (JIT) 编译器。这与能够以革命性的方式改变 Java 性能的知名技术 HotSpot 属于同一种技术的不同形式,Sun Microsystems 于 21 世纪初期从 Animorphic 手中收购了 HotSpot,并整合到了自己的 Java 实现之中,使这种语言适用于大多数用途。Python 原本已经适用于多种用途,但性能是最常被人们抱怨的问题。PyPy 的跟踪 JIT 编译器已经展现了它革新 Python 程序性能的能力,尽管我认为这个项目仍然处于后续测试阶段,但它已经是 Python 程序员的一种重要工具,是开发人员工具箱的有用补充。
在这篇文章中,我将介绍 PyPy,而且假设读者并不具备丰富的 Python 背景知识。
首先,请不要将 PyPy 与 PyPI 混淆。这是两个截然不同的项目。PyPI 即 Python Package Index,是获得第三方 Python 软件包以补充标准库的一个站点及系统。在您进入正确的 PyPy 站点之后(请参见 参考资料 部分),您会看到开发人员已经使大多数用户能够轻松开始尝试使用 PyPy。如果您在最新的硬件上使用 Linux?、Mac 或 Windows?(不含 Windows 64,目前尚不支持 Windows 64),那么就应该能够直接下载并执行一个二进制软件包。
PyPY 的最新版本是 1.8,它充分实现了 Python 2.7.2,也就是说能够兼容这个版本的 CPython 的语言特性和行为。然而,在许多基准使用当中,它的速度已经远远超过了 CPython 2.7.2,这是它引起我们注意的真正因素。下面的会话展示了我在 Ubuntu 11.04 机器上安装 PyPy 的过程。这段会话来自旧版本的 PyPy,但 PyPy 1.8 也会提供类似的结果。
$ cd Downloads/$ wget https://bitbucket.org/pypy/pypy/downloads/pypy-1.6-linux.tar.bz2$ cd ../.local$ tar jxvf ~/Downloads/pypy-1.6-linux.tar.bz2$ ln -s ~/.local/pypy-1.6/bin/pypy ~/.local/bin/
现在,您需要更新 $PATH
,以包含 ~/.local/bin/。安装 PyPy 之后,建议您同样安装 Distribute 和 Pip,以便简化额外软件包的安装。(尽管本文中未提及,但您也可能需要使用 Virtualenv,这是保持独立、整洁的 Python 环境的一种方法。)以下会话展示了 Distribute 和 Pip 的设置。
$ wget http://python-distribute.org/distribute_setup.py$ wget https://raw.github.com/pypa/pip/master/contrib/get-pip.py$ pypy distribute_setup.py$ pypy get-pip.py
您应发现,库文件安装在 ~/.local/pypy-1.8/site-packages/ 目录之中,可执行文件位于 ~/.local/pypy-1.8/bin 目录之中,因此您可能希望将后者添加到 $PATH
。此外,务必确保使用了之前安装的 pip,而不是系统级的 pip。随后,您就可以安装本文稍后要用到的第三方软件包。
$ pip install html5lib $ pip install pyparsing
清单 1 展示了调用 Python 的 “彩蛋” import this
之后,PyPy 解释器的输出结果。
uche@malatesta:~$ pypyPython 2.7.1 (d8ac7d23d3ec, Aug 17 2011, 11:51:18)[PyPy 1.6.0 with GCC 4.4.3] on linux2Type "help", "copyright", "credits" or "license" for more information.And now for something completely different: ``__xxx__ and __rxxx__ vs operationslots: particle quantum superposition kind of fun''>>>> import thisThe Zen of Python, by Tim PetersBeautiful is better than ugly.Explicit is better than implicit.Simple is better than complex.Complex is better than complicated.Flat is better than nested.Sparse is better than dense.Readability counts.Special cases aren't special enough to break the rules.Although practicality beats purity.Errors should never pass silently.Unless explicitly silenced.In the face of ambiguity, refuse the temptation to guess.There should be one-- and preferably only one --obvious way to do it.Although that way may not be obvious at first unless you're Dutch.Now is better than never.Although never is often better than *right* now.If the implementation is hard to explain, it's a bad idea.If the implementation is easy to explain, it may be a good idea.Namespaces are one honking great idea -- let's do more of those!>>>>
作为 PyPy 的一个简单实例,我将展示一个解析网页并打印出网页中表示的链接列表的程序。这正是网络蜘蛛 (spidering) 软件的基本理念,即出于某些目的跟踪页面间的链接网络。
在解析方面,我选择了 html5lib,这是一种纯 Python 解析库,设计用于实现定义了 HTML5 规范的 WHAT-WG 组织的解析算法。HTML5 针对向后兼容性而设计,即便可以兼容损坏的网页。因此 html5lib 同时也是一种出色的通用 HTML 解析工具包。这种工具在 CPython 和 PyPy 上执行了基准测试,在 PyPy 上的速度明显要更快。
清单 2 解析了一个特定的网页,逐行打印了该页面中的链接。您在命令行中指定目标页面 URL,例如: pypy listing1.py http://www.ibm.com/developerworks/opensource/
。
#!/usr/bin/env pypy#Import the needed libraries for useimport sysimport urllib2import html5lib#List of tuples, each an element/attribute pair to check for linkslink_attrs = [ ('a', 'href'), ('link', 'href'),]#This function is a generator, a Python construct that can be used as a sequence.def list_links(url): ''' Given a URL parse the HTML and yield a sequence of link strings as they are found on the page. ''' #Open the URL and get back a stream of the content stream = urllib2.urlopen(url) #Parse the HTML content according to html5lib conventions tree_builder = html5lib.treebuilders.getTreeBuilder('dom') parser = html5lib.html5parser.HTMLParser(tree=tree_builder) doc = parser.parse(stream) #In the outer loop, go over each element/attribute set for elemname, attr in link_attrs: #In the inner loop, go over the matches of the current element name for elem in doc.getElementsByTagName(elemname): #If the corresponding attribute is found, yield it in sequence attrvalue = elem.getAttribute(attr) if attrvalue: yield attrvalue return#Read the URL to parse from the first command line argument#Note: Python lists start at index 0, but as in UNIX convention the 0th#Command line argument is the program name itselfinput_url = sys.argv[1]#Set up the generator by calling it with the URL argument, then iterate#Over the yielded link strings, printing eachfor link in list_links(input_url): print link
我在代码中作了非常详尽的注释,因为我并未假设读者拥有深厚的 Python 知识,但您应该了解一些基本知识,例如,如何使用缩排格式来表示控制流。相关的 Python 教程请参见 参考资料 部分。
为简单起见,我避免了此类程序的一些惯例,但确实使用了一项我认为即便对入门级的程序员也非常有用的高级特性。函数 list_links
称为生成器。它是一个像序列一样操作的函数,可依次计算并提供项目。yield
语句是这里的关键,它提供了值序列。
大多数网页解析任务都不仅仅是发现和显示链接那样简单,有一些库可以帮助处理典型的 “网络抓取” 任务。Pyparsing 是一种通用的纯 Python 解析工具包,包含一些支持 HTML 解析的工具。
在下一个示例中,我将展示如何从 IBM developerWorks 索引页面中抓取文章列表。图 1 展示了目标页面的屏幕快照。清单 3 是 HTML 形式的示例记录。
<tbody> <tr> <td> <a href="http://www.ibm.com/developerworks/opensource/library/os-wc3jam/index.html"> <strong>Join the social business revolution</strong></a> <div> Social media has become social business and everyone from business leadership to software developers need to understand the tools and techniques that will be required. The World Wide Web Consortium (W3C) will be conducting a social media event to discuss relevant standards and requirement for the near and far future. </div> </td> <td>Articles</td> <td class="dw-nowrap">03 Nov 2011</td> </tr></tbody>
清单 4 给出了解析这个页面的代码。同样,我仍然在这里给出了大量注释,但在给出清单后,我还要说明几个重要新概念。
#!/usr/bin/env pypy#Import the needed built-in libraries for useimport sysimport urllib2from greenlet import greenlet#Import what we need from pyparsingfrom pyparsing import makeHTMLTags, SkipTodef collapse_space(s): ''' Strip leading and trailing space from a string, and replace any run of whitespace within with a single space ''' #Split the string according to whitespace and then join back with single spaces #Then strip leadig and trailing spaces. These are all standard Python library tools return ' '.join(s.split()).strip()def handler(): ''' Simple coroutine to print the result of a matched portion from the page ''' #This will be run the first time the code switches to this greenlet function print 'A list of recent IBM developerWorks Open Source Zone articles:' #Then we get into the main loop while True: next_tok = green_handler.parent.switch() print ' *', collapse_space(data.title), '(', data.date, ')', data.link.href#Turn a regular function into a greenlet by wrapping itgreen_handler = greenlet(handler)#Switch to the handler greenlet the first time to prime itgreen_handler.switch()#Read the search starting pageSTART_URL = "http://www.ibm.com/developerworks/opensource/library/"stream = urllib2.urlopen(START_URL)html = stream.read()stream.close()#Set up some tokens for HTML start and end tagsdiv_start, div_end = makeHTMLTags("div")tbody_start, tbody_end = makeHTMLTags("tbody")strong_start, strong_end = makeHTMLTags("strong")article_tr, tr_end = makeHTMLTags("tr")td_start, td_end = makeHTMLTags("td")a_start, a_end = makeHTMLTags("a")#Put together enough tokens to narrow down the data desired from the pagearticle_row = ( div_start + SkipTo(tbody_start) + SkipTo(a_start) + a_start('link') + SkipTo(strong_start) + strong_start + SkipTo(strong_end)("title") + SkipTo(div_start) + div_start + SkipTo(div_end)("summary") + div_end + SkipTo(td_start) + td_start + SkipTo(td_end)("type") + td_end + SkipTo(td_start) + td_start + SkipTo(td_end)("date") + td_end + SkipTo(tbody_end) )#Run the parser over the page. scanString is a generator of matched snippetsfor data, startloc, endloc in article_row.scanString(html): #For each match, hand it over to the greenlet for processing green_handler.switch(data)
我刻意地设置了 清单 4,以便引入 PyPy 的 Stackless Python 特性。简而言之,这是一种存在已久、替代性的 Python 实现,旨在尝试高级的控制流特性。大多数 Stackless 特性都因其他运行时中的限制而未能进入其他 Python 实现,但 PyPy 中完全没有这方面的担忧。Greenlets 就是一个例子。Greenlets 是超轻量级的线程,可通过协作的方式进行多重任务处理,通过显式调用来将内容从一个 greenlet 切换到另一个 greenlet。Greenlets 使您能够执行生成器允许的一些精密的任务,还提供了更多能力。
在 清单 4 中,我使用 greenlets 定义了一个协作例程,这个函数的操作通过一种支持轻松构造和遵循流的方式精密地彼此交错。greenlets 通常用于较为主流的编程方式使用回调的场合,例如事件驱动的系统。这里您不是调用回调,而是将上下文切换到协作例程。此类工具的关键优势在于允许您以高效率为目的构造应用程序,而完全不必担心艰难的状态管理。
清单 4 简单展示了协作例程和 greenlets 的大体情况,但这是轻松融入 PyPy 环境的一种有用概念,PyPy 现已打包提供 greenlet 和其他 Stackless 特性。在 清单 4 中,每一次 pyparsing 匹配一条记录时,都会调用 greenlet 来处理该记录。
下面是 清单 4 的输出示例。
A list of recent IBM developerWorks Open Source Zone articles: * Join the social business revolution ( 03 Nov 2011 ) http://www.ibm.com/developerworks/opensource/library/os-wc3jam/index.html * Spark, an alternative for fast data analytics ( 01 Nov 2011 ) http://www.ibm.com/developerworks/opensource/library/os-spark/index.html * Automate development and management of cloud virtual machines ( 29 Oct 2011 ) http://www.ibm.com/developerworks/cloud/library/cl-automatecloud/index.html
我刻意采用了 清单 4 中的方法,但是我要提出有关这种方法的一个警告:在没有极为专业的工具时,尝试处理 HTML 总是非常危险的行为。尽管不像 XML 那样如果不使用符合规定的解析器就会造成反模式,但 HTML 也是非常复杂难处理的,即便在符合标准的页面中(实际上大多数页面都不是符合标准的)也是如此。如果您需要通用的 HTML 解析,那么 html5lib 是一种较好的选择。也就是说,网络抓取通常是一种专业化的操作,您只要根据特定环境提取信息。对于此类极具局限性的用途,pyparsing 尚可,也提供了一些精巧的工具能给您带来帮助。
在 清单 4 中,我引入了并非必不可少的 greenlets,如果您将此类代码扩展到更多的实际场景,那么我这种做法的原因将显而易见。如果您正在多路解析和处理其他操作,那么 greenlets 方法将使您可以构造处理,这与 UNIX 命令行管道完全不同。如果问题进一步复杂化,您要处理多个源页面,那么还存在一个问题,urllib2 的操作并非始终异步,只要访问某个网页,整个程序就会被阻塞。这个问题的解决方法已经超出了本文的讨论范围,但 清单 4 中对高级控制流的使用应该能够使您养成一种谨慎思考如何在保证性能的前提下拼合此类复杂应用程序的习惯。
PyPy 是一个拥有积极的维护支持的项目,也是一个仍然在不断发展的目标,然而,如今这个项目已经取得了很大的成功,高水平的 CPython 兼容性也意味着:如果您开始尝试,那么就很有可能会获得能帮助您完成工作的一种成熟的备用平台。在本文中,充分了解了如何开始动手尝试,还简单了解了 PyPy 极具吸引力的 Stackless 特性。PyPy 的性能一定会令您感到惊喜,更重要的是,它能带来在不牺牲速度的前提下优雅编程的全新思路。
联系客服