使用基本的Python模块,可以编写脚本来与Web站点交互,但是如果没有必要的话,那么您就不希望这样做。Python2.x中的模块urllib和urllib2,以及Python3.0中的统一的urllib.*子包,可以在URL的末尾获取资源。然而,当您希望与Web页面中找到的内容进行某种比较复杂的交互时,您需要使用mechanize库。
51CTO推荐专题: Python实用开发指南
在自动化Webscrap或用户与Web站点的交互模拟中,最大的困难之一就是服务器使用cookies跟踪会话进度。显然,cookies是HTTP头部的一部分,在urllib打开资源时会自然显示出来。
即使如此,在这个层次上执行处理也非常的繁琐。mechanize库将这种处理提升到一个更高程度的抽象并使您的脚本—或交互性Pythonshell—表现出非常类似实际Web浏览器的行为。
Python的mechanize受到Perl的WWW:Mechanize的启发,后者具有类似的一组功能。当然,作为长期的Python支持者,我认为mechanize更健壮,它看上去似乎继承了两种语言的通用模式。
mechanize的一个亲密伙伴是同样出色的BeautifulSoup库。这是一个非常神奇的“粗糙的解析器”,用于解析实际Web页面中包含的有效HTML。您不需要将BeautifulSoup用于mechanize,反之亦然,但是多半情况下,当您与“实际存在的Web”交互时,您将希望同时使用这两种工具。
一个实际示例
我曾在多个编程项目中使用过mechanize。最近一个项目是从一个流行的Web站点中收集匹配某种条件的名称的列表。该站点提供了一些搜索工具,但是没有提供任何正式的API来执行此类搜索。虽然访问者可能能够更明确地猜出我过去在做什么,但我将修改给出的代码的细节,以避免暴露有关被scrap的站点或我的客户机的过多信息。一般情况下,我给出的代码对于类似任务是通用的。
入门工具
在实际开发Webscrap/分析代码的过程中,我发现以交互式方式查看、处理和分析Web页面的内容以了解相关Web页面实际发生的操作是非常重要的功能。通常,站点中的一些页面是由查询动态生成(但是具有一致的模式),或是根据非常严格的模板预先生成。
完成这种交互式体验的一种重要方法就是在Pythonshell内使用mechanize本身,特别是在一个增强的shell内,比如IPython。通过这种方式,您可以在编写执行希望用于生产中的交互的最终脚本之前,请求各种已链接的资源、提交表单、维护或操作站点cookies,等等。
然而,我发现我与Web站点的许多实验性质的交互在实际的现代Web浏览器中得到了更好的执行。方便地呈现页面可以使您更加快速地了解给定页面或表单中正在发生的事情。问题在于,呈现页面仅仅完成了事情的一半,可能还不到一半。获得“页面源代码”会让您更进一步。要真正理解给定Web页面或与Web服务器的一系列交互的背后的原理,需要了解更多。
要了解这些内容,我常常使用Firebug或面向Firefox的WebDeveloper插件。所有这些工具都可以执行诸如显示表单字段、显示密码、检查页面的DOM、查看或运行Javascript、观察Ajax通信等操作。比较这些工具的优劣需要另外撰写一篇文章,但是如果您要进行面向Web的编程的话,那么必须熟悉这些工具。
不管使用哪一种工具来对准备实现自动化交互的Web站点做实验,您都需要花比编写简洁的mechanize代码(用于执行您的任务)更多的时间来了解站点实际发生的行为。
搜索结果scraper
考虑到上面提到的项目的意图,我将把包含100行代码的脚本分为两个功能:
◆检索所有感兴趣的结果
◆从被检索的页面中拉取我感兴趣的信息
使用这种方式组织脚本是为了便于开发;当我开始任务时,我需要知道如何完成这两个功能。我觉得我需要的信息位于一个普通的页面集合中,但是我还没有检查这些页面的具体布局。
首先我将检索一组页面并将它们保存到磁盘,然后执行第二个任务,从这些已保存的文件中拉取所需的信息。当然,如果任务涉及使用检索到的信息构成同一会话内的新交互,那么您将需要使用顺序稍微不同的开发步骤。因此,首先让我们查看我的fetch()函数:
- 清单1.获取页面内容
- importsys,time,os
- frommechanizeimportBrowser
- LOGIN_URL='http://www.example.com/login'
- USERNAME='DavidMertz'
- PASSWORD='TheSpanishInquisition'
- SEARCH_URL='http://www.example.com/search?'
- FIXED_QUERY='food=spam&''utensil=spork&''date=the_future&'
- VARIABLE_QUERY=['actor=%s'%actorforactorin
- ('GrahamChapman',
- 'JohnCleese',
- 'TerryGilliam',
- 'EricIdle',
- 'TerryJones',
- 'MichaelPalin')]
- deffetch():
- result_no=0#Numbertheoutputfiles
- br=Browser()#Createabrowser
- br.open(LOGIN_URL)#Opentheloginpage
- br.select_form(name="login")#Findtheloginform
- br['username']=USERNAME#Settheformvalues
- br['password']=PASSWORD
- resp=br.submit()#Submittheform
- #Automaticredirectsometimesfails,followmanuallywhenneeded
- if'Redirecting'inbr.title():
- resp=br.follow_link(text_regex='clickhere')
- #Loopthroughthesearches,keepingfixedqueryparameters
- foractorininVARIABLE_QUERY:
- #Iliketowatchwhat'shappeningintheconsole
- print>>sys.stderr,'***',actor
- #Letsdotheactualquerynow
- br.open(SEARCH_URL+FIXED_QUERY+actor)
- #Thequeryactuallygivesuslinkstothecontentpageswelike,
- #buttherearesomeotherlinksonthepagethatweignore
- nice_links=[lforlinbr.links()
- if'good_path'inl.url
- and'credential'inl.url]
- ifnotnice_links:#Maybetherelevantresultsareempty
- break
- forlinkinnice_links:
- try:
- response=br.follow_link(link)
- #Moreconsolereportingontitleoffollowedlinkpage
- print>>sys.stderr,br.title()
- #Incrementoutputfilenames,openandwritethefile
- result_no+=1
- out=open(result_%04d'%result_no,'w')
- print>>out,response.read()
- out.close()
- #Nothingevergoesperfectly,ignoreifwedonotgetpage
- exceptmechanize._response.httperror_seek_wrapper:
- print>>sys.stderr,"Responseerror(probably404)"
- #Let'snothammerthesitetoomuchbetweenfetches
- time.sleep(1)
对感兴趣的站点进行交互式研究后,我发现我希望执行的查询含有一些固定的元素和一些变化的元素。我仅仅是将这些元素连接成一个大的GET请求并查看“results”页面。而结果列表包含了我实际需要的资源的链接。
因此,我访问这些链接(当此过程出现某些错误时,会抛出try/except块)并保存在这些内容页面上找到的任何内容。很简单,是不是?Mechanize可以做的不止这些,但是这个简单的例子向您展示了Mechanize的大致功能。
#p#
处理结果
现在,我们已经完成了对mechanize的操作;剩下的工作是理解在fetch()循环期间保存的大量HTML文件。批量处理特性让我能够在一个不同的程序中将这些文件整齐、明显地分离开来,fetch()和process()可能交互得更密切。BeautifulSoup使得后期处理比初次获取更加简单。对于这个批处理任务,我们希望从获取的各种Web页面的零散内容中生成表式的以逗号分隔的值(CSV)数据。
- 清单2.使用BeautifulSoup从无序的内容中生成整齐的数据
- fromglobimportglob
- fromBeautifulSoupimportBeautifulSoup
- defprocess():
- print"!MOVIE,DIRECTOR,KEY_GRIP,THE_MOOSE"
- forfnameinglob('result_*'):
- #PutthatsloppyHTMLintothesoup
- soup=BeautifulSoup(open(fname))
- #Trytofindthefieldswewant,butdefaulttounknownvalues
- try:
- movie=soup.findAll('span',{'class':'movie_title'})[1].contents[0]
- exceptIndexError:
- fname="UNKNOWN"
- try:
- director=soup.findAll('div',{'class':'director'})[1].contents[0]
- exceptIndexError:
- lname="UNKNOWN"
- try:
- #Maybemultiplegripslisted,keyoneshouldbeinthere
- grips=soup.findAll('p',{'id':'grip'})[0]
- grips="".join(grips.split())#Normalizeextraspaces
- exceptIndexError:
- title="UNKNOWN"
- try:
- #HidesomestuffintheHTML<meta>tags
- moose=soup.findAll('meta',{'name':'shibboleth'})[0]['content']
- exceptIndexError:
- moose="UNKNOWN"
- print'"%s","%s","%s","%s"'%(movie,director,grips,moose)
第一次查看BeautifulSoup,process()中的代码令人印象深刻。读者应当阅读有关文档来获得关于这个模块的更多细节,但是这个代码片段很好地体现了它的整体风格。大多数soup代码包含一些对只含有格式良好的HTML的页面的.findAll()调用。这里是一些类似DOM的.parent、nextSibling和previousSibling属性。它们类似于Web浏览器的“quirks”模式。我们在soup中找到的内容并不完全是一个解析树。
结束语
诸如我之类的守旧者,甚至于一些更年轻的读者,都会记住使用TCLExpect(或使用用Python和其他许多语言编写的类似内容)编写脚本带来的愉悦。自动化与shell的交互,包括telnet、ftp、ssh等等远程shell,变得非常的直观,因为会话中的所有内容都被显示出来。
Web交互变得更加细致,因为信息被分为头部和内容体,并且各种相关的资源常常通过href链接、框架、Ajax等被绑定在一起。然而,总的来说,您可以使用wget之类的工具来检索Web服务器提供的所有字节,然后像使用其他连接协议一样运行与Expect风格完全相同的脚本。
在实践中,几乎没有编程人员过分执着于过去的老方法,比如我建议的wget+Expect方法。Mechanize保留了许多与出色的Expect脚本相同的东西,令人感觉熟悉和亲切,并且和Expect一样易于编写(如果不是更简单的话)。
Browser()对象命令,比如.select_form()、.submit()和.follow_link(),真的是实现“查找并发送”操作的最简单、最明显的方法,同时绑定了我们希望在Web自动化框架中具备的复杂状态和会话处理的所有优点。
【编辑推荐】