使用python爬虫抓取网站的一些技巧总结我写过一个自动发帖的脚本,一个自动接收邮件的脚本,一个简单的验证码识别脚本。本来想写一个抓取GoogleMusic的脚本,但是gmbox强大,就不用写了。这些脚本有一个共同点。它们都与网络有关。总是使用一些获取链接的方法。除了simplecd这个半爬虫半网站的项目,积累了很多爬网站的经验。这里总结一下,以后做事就不用重复了。1.最基本的抓站importurllib2content=urllib2.urlopen('http://XXXX').read()2.使用代理服务器在某些情况下是有用的,比如IP被封了,或者比如IP访问次数被限制等等。importurllib2proxy_support=urllib2.ProxyHandler({'http':'http://XX.XX.XX.XX:XXXX'})opener=urllib2.build_opener(proxy_support,urllib2.HTTPHandler)urllib2.install_opener(opener)content=urllib2。urlopen('http://XXXX').read()3.如果需要登录,登录比较麻烦,我把问题拆分一下:3.1cookie处理importurllib2,cookielibcookie_support=urllib2.HTTPCookieProcessor(cookielib.CookieJar())opener=urllib2.build_opener(cookie_support,urllib2.HTTPHandler)urllib2.install_opener(opener)content=urllib2.urlopen('http://XXXX').read()可以,如果要使用proxy和cookie同时,那么只需添加proxy_support,并将operner改为opener=urllib2.build_opener(proxy_support,cookie_support,urllib2.HTTPHandler)3.2表单处理和登录必须填写表单,如何填写表单?先用工具截取要填写的表格内容。比如我平时用firefox+httpfox插件,看看发了哪些包。让我举一个例子。以verycd为例。首先找到你发送的POST请求和POST表单项:可以看到verycd的字样需要填写username,password,continueURI,fk,login_submit,其中fk是随机生成的(其实不是很随机的,貌似是纪元时间简单编码生成的),需要从网页中获取,也就是说你得先访问网页一次,然后用正则表达式等工具拦截fk返回数据中的项目。continueURI,顾名思义,随便写,login_submit是固定的,从源码可以看出。还有用户名和密码,很明显。好了,数据填好了,我们就生成postdataimporturllibpostdata=urllib.urlencode({'username':'XXXXX','password':'XXXXX','continueURI':'http://www.verycd.com/','fk':fk,'login_submit':'login'})然后生成http请求,然后发送请求:req=urllib2.Request(url='http://secure.verycd.com/signin/*/http://www.verycd.com/',data=postdata)result=urllib2.urlopen(req).read()3.3假装浏览器访问一些网站,反感爬虫访问,所以都爬虫请求被拒绝。这时候我们需要伪装成浏览器,可以通过修改http包中的headers来实现:headers={'User-Agent':'Mozilla/5.0(Windows;U;WindowsNT6.1;en-US;rv:1.9.1.6)Gecko/20091201Firefox/3.5.6'}req=urllib2.Request(url='http://secure.verycd.com/signin/*/http://www.verycd.com/',data=postdata,headers=headers)3.4防“防盗链”有些网站有所谓的防盗链设置。其实在你发送请求的header中查看referringsite是不是本身很简单,所以我们只需要像3.3一样,将headers的referer改成这个网站即可。以黑幕cnbeta为例:headers={'Referer':'http://www.cnbeta.com/articles'}headers是一个dict数据结构,你可以放任何你想要的header,做一些伪装。例如,一些智能网站总是喜欢窥探人们的隐私。当其他人通过代理访问时,他们只是想读取header中的X-Forwarded-For以查看其真实IP。没啥好说的,就把X-Forwarded-For改一下,改成什么好玩的好欺负欺负他,呵呵。3.5终极绝招有时候即使做了3.1-3.4,访问还是会被阻塞,没办法,老老实实把httpfox看到的header都写上去,一般就可以了。如果实在不行,那就只能使出绝招了,selenium直接控制浏览器访问,只要浏览器能做,那它也能做。同样还有pamie、watir等。4、多线程并发抓取如果单线程太慢,就需要多线程。这是一个简单的线程池模板。这个程序只是简单的打印了1-10,但是可以看出是并发的。fromthreadingimportThreadfromQueueimportQueuefromtimeimportsleep#q是任务队列#NUM是并发线程总数#JOBS是有多少个任务q=Queue()NUM=2JOBS=10#具体的处理函数负责处理单个任务defdo_somthing_using(arguments):printarguments#这是工作进程,负责不断从队列中取数据并进行处理defworking():whileTrue:arguments=q.get()do_somthing_using(arguments)sleep(1)q.task_done()#forkNUM个线程在等待thequeueforiinrange(NUM):t=Thread(target=working)t.setDaemon(True)t.start()#将JOBS放入队列foriinrange(JOBS):q.put(i)#等待所有JOBS完成q.加入()5。验证码处理遇到验证怎么办?这里要处理两种情况:谷歌的验证码,冷酷简单的验证码:字符数有限,只使用简单的平移或者旋转加噪声不失真。这种验证码可能还是可以处理的。大体思路是旋转回头,去除噪声,然后分割出单个字符。划分后采用特征提取方法(如PCA)降维生成特征库,然后将验证码与特征库进行比对。这个比较复杂,一篇博文说不完,这里就不展开了。具体方法请拿相关教材学习。其实有些验证码还是很弱的,这里就不点名了。反正我已经通过2的方法提取了非常高精度的验证码,所以2其实是可行的。6gzip/deflatesupport目前的网页普遍支持gzip压缩,往往可以解决大量的传输时间问题。以VeryCD主页为例,未压缩版为247K,压缩版为45K,是原来的1/5。这意味着爬行速度快了5倍。但是python的urllib/urllib2默认是不支持压缩的。要返回压缩格式,您必须在请求的标头中指定'accept-encoding',然后在读取响应后检查标头以查看是否有'content-encoding'项。判断是否需要解码非常麻烦。如何让urllib2自动支持gzip,defalte呢?其实可以继承BaseHanlder类,然后build_opener的方式来处理:importurllib2fromgzipimportGzipFilefromStringIOimportStringIOclassContentEncodingProcessor(urllib2.BaseHandler):"""Ahandlertoaddgzipcapabilitiestourllib2requests"""#addheaderstorequestsdefhttp_request(self,req):req.add_header("接受编码","gzip,deflate")returnreq#decodedefhttp_response(self,req,resp):old_resp=resp#gzipifresp.headers.get("content-encoding")=="gzip":gz=GzipFile(fileobj=StringIO(resp.read()),mode="r")resp=urllib2.addinfol(gz,old_resp.headers,old_resp.url,old_resp.code)resp.msg=old_resp.msg#deflateifresp.headers.get("内容编码”)==”deflate”:gz=StringIO(deflate(resp.read()))resp=urllib2.addinfol(gz,old_resp.headers,old_resp.url,old_resp.code)#'classtoaddinfo()andresp.msg=old_resp.msgreturnresp#deflatesupportimportzlibdefdeflate(data):#zlibonlyprovidesthezlibcompressformat,不是deflateformat;试试:#soontopofall有这个解决方法:returnzlib.decompress(data,-zlib.MAX_WBITS)exceptzlib.error:returnzlib.decompress(data)那么就简单了,encoding_support=ContentHTTPEncodingProcessoropener=urllib2.build_opener(encoding_support.Hurller2)#打开网页直接用opener,如果服务器支持gzip/default,会自动解压content=opener.open(url).read()7.更方便的多线程总结文章确实提到了一个简单的多线程模板,但是当那玩意真正应用到程序中时,只会让程序变得支离破碎,不美观。如何更方便的进行多线程,我也绞尽脑汁。先想想怎么让多线程调用最方便?1.使用twisted进行异步I/O爬取其实更高效的爬取并不一定需要多线程,也可以使用异步I/O的方式:直接使用twisted的getPage方法,然后加上异步I/O结束callback和errback方法就足够了。例如,您可以这样做:fromtwisted.web.clientimportgetPagefromtwisted.internetimportreactorlinks=['http://www.verycd.com/topics/%d/'%iforiinrange(5420,5430)]defparse_page(data,url):printlen(data),urldeffetch_error(error,url):printerror.getErrorMessage(),url#批量抓取链接forurlinlinks:getPage(url,timeout=5)\.addCallback(parse_page,url)\#成功调用parse_page方法.addErrback(fetch_error,url)#如果失败,调用fetch_error方法reactor.callLater(5,reactor.stop)#5秒后通知reactor结束程序reactor.run()twisted顾名思义,代码写的太扭曲了,不是正常人可以接受的,虽然这个简单的例子看起来不错;每次写出一个扭曲的程序,整个人都扭曲了,疲惫不堪。没有文档,我必须阅读源代码才能知道如何修复它。我就不提了。如果要支持gzip/deflate,甚至做一些登录扩展,就得为twisted等写一个新的HTTPClientFactory类。我真的皱了皱眉头,所以我放弃了。有毅力的人,请自己尝试一下。这篇关于如何使用twisted批量处理url的文章是一篇不错的文章,由浅入深,通俗易懂,可以看看。2.设计一个简单的多线程爬取类还是感觉在urllib之类的python“原生”东西里折腾更舒服。试想一下,如果有一个Fetcher类,可以调用f=Fetcher(threads=10)#设置下载线程数为10forurlinurls:f.push(url)#将所有url推入下载队列whilef.taskleft():#如果还有线程还没有完成下载content=f.pop()#从下载完成队列中获取结果do_with(content)#处理内容content这样的多线程调用简单明了,下面这么设计,首先有两个队列,用Queue处理。多线程的基本结构类似于《技巧总结》一文。push方法和pop方法都比较好办,都是直接用Queue方法。taskleft是如果有“正在运行的任务”或者“队列中的任务”是有的,也好办,所以代码如下:.build_opener(urllib2.HTTPHandler)self.lock=Lock()#线程锁self.q_req=Queue()#任务队列self.q_ans=Queue()#完成队列self.threads=threadsforiinrange(threads):t=Thread(target=self.threadget)t.setDaemon(True)t.start()self.running=0def__del__(self):#解构时等待两个队列完成time.sleep(0.5)self.q_req.join()self.q_ans.join()deftaskleft(self):returnsself.q_req.qsize()+self.q_ans.qsize()+self.runningdefpush(self,req):self.q_req.put(req)defpop(self):returnself。q_ans.get()defthreadget(self):whileTrue:req=self.q_req.get()withself.lock:#保证操作的原子性,进入criticalareaself.running+=1try:ans=self.opener.open(req).read()exceptException,what:ans=''printwhatself.q_ans.put((req,ans))withself.lock:self.running-=1self.q_req.task_done()time.sleep(0.1)#don'tspamif__name__=="__main__":links=['http://www.verycd.com/topics/%d/'%iforiinrange(5420,5430)]f=Fetcher(threads=10)forurlinlinks:f.push(url)whilef.taskleft():url,content=f.pop()printurl,len(content)8.一些琐碎的经验1.连接池:opener.open和urllib2.urlopen一样,都会新建一个http请求。通常这不是问题,因为在线性环境中,一秒钟可能还会产生一个新的请求;但是,在多线程环境中,每秒可能有数十个甚至数百个请求。这样做只需几分钟,正常合理的服务器肯定会禁止你。但是在正常的html请求中,同时维护几十个与服务器的连接是很正常的,所以完全可以手动维护一个HttpConnection池,然后每次都从连接池中选择一个连接进行连接抓住它。能。这里有一个技巧,就是用squid作为代理服务器进行爬取,然后squid会自动帮你维护连接池,而且它还有数据缓存的功能,而且squid永远是每个服务器都必须安装的东西我的,何必呢那就自找麻烦写连接池。2.设置线程堆栈大小堆栈大小的设置会显着影响python的内存使用。如果python多线程不设置这个值,程序会占用大量内存,这对openvzvps来说是非常致命的。stack_size必须大于32768,实际上应该总是大于32768*2fromthreadingimportstack_sizestack_size(32768*16)3.设置失败后自动重试defget(self,req,retries=3):try:response=self.opener。open(req)data=response.read()exceptException,what:printwhat,reqifretries>0:returnsself.get(req,retries-1)else:print'GETFailed',reqreturn''returndata4,设置超时importsocketsocket.setdefaulttimeout(10)#setting10秒后连接超时。5.登录更简单。首先要在build_opener中加入对cookie的支持,参考“总结”一文;如果要登录VeryCD,在Fetcher中添加一个空方法login,在init()中调用,然后继承Fetcher类,重写登录方法:deflogin(self,username,password):importurllibdata=urllib。urlencode({'username':用户名,'password':密码,'continue':'http://www.verycd.com/','login_submit':u'login'.encode('utf-8'),'save_cookie':1,})url='http://www.verycd.com/signin'self.opener.open(url,data).read()会在Fetcher初始化时自动登录VeryCD网站。9.综上所述,结合以上所有技巧,离我目前的Fetcher类私有最终版本不远了。支持多线程、gzip/deflate压缩、超时设置、自动重试、设置堆栈大小、自动登录等功能;代码简单,使用方便,性能还不错,堪称居家旅行杀人放火必备利器,咳咳。之所以离最终版不远,是因为最终版还有一个保留功能“马甲术”:多智能体自动选择。看似只是random.choice的区别,其实包含代理获取、代理验证、代理测速等诸多环节,这又是另外一回事了。参考http://obmem.info/?p=476http://obmem.info/?p=753
