前言

离上一篇更新的博文应该过了挺久的了( python爬虫(上)–请求——关于旅游网站的酒店评论爬取(传参方法)),因为中间考完试紧接着就去实习的缘故,然后到新环境各种熟悉什么的,所以后面有所学到的东西就来不及汇总,终于在某个礼拜天的下午,喝着我的雀巢速溶咖啡,一边写着这篇总结。

上一篇我自己也回去又看了一遍,其实上一篇的博文主要还是用的是 传参 的方法,什么叫传参的方法?就是着重点在分析交互中各种数据请求来源,然后找到我们需要的数据来源,再写个脚本把数据请求回来,最后做个提取就OK了,这种办法的优点在于执行得很快,对比于后面我要说的一种 模拟浏览器 的方法来说,在请求页面的效率上会快上很多,而传参方法的难点在于寻找数据来源的那个URL还有它尾巴的一堆参数的构建,当然简单的比如上一篇的艺龙的酒店评论的话,直接用chrome的开发者工具XHR项目里面就可以找到,但当遇到更为复杂的页面结构,或者采用更加复杂技术的页面(比如混淆代码啊,压缩代码之类的),传参这种办法的工作就变得十分繁琐,特别是在XHR里面没有找到数据来源的URL的时候,要在一堆js脚本里面找到你要的数据的请求来源的时候,你会彻底懵逼了,反正我是懵逼了好几次,而且爬虫有个真理——越是接近人的操作的爬虫越是好的爬虫。因为网站是为了人去访问而服务的,那么就算网站反爬很厉害,但是也不可能去因为爬虫而误伤了正常的用户,对于一个网站而言得不偿失,所以当我懵逼了几次以后,果断转用模拟浏览器的方法,我在这里要明确一点,传参的思路和模拟浏览器的思路是不一样的,所以要分开来认识他们


爬虫制作总览

经过实习这一段时间以来的学习和研究,包括对python这门语言更加深入的了解,还有对爬虫中遇到的问题的总结,我觉得大致上爬虫的制作思路如下:

这里写图片描述 P.s.更正上图lxml.etee为lxml.etree

上图中需要补充的是,有时候有登录的需求,登录里面会涉及验证码的问题,其实说白了就是OCR技术的应用,还有访问过频,封IP或者是弹验证码的问题,这些都是爬虫会遇到的独特问题,不过一般按照上图的思路去做爬虫,遇到上面这些问题再相对应的做应对策略就好.

其实爬虫好玩的地方在于与网站程序猿的之间的博弈,一般而言,没有哪个网站是希望自己的数据被爬虫爬去的,当然谷歌百度这种大的搜索引擎除外,每天都被成百上千的练手也好,娱乐也罢的大大小小的爬虫们爬网站内容,哪个服务器受得了,所以对于山那头的程序猿而言,爬虫生死之战在于把关好请求页面这一步,不让爬虫程序拿到完整静态页面;对于开发爬虫的程序猿而言,爬虫生死之战就在能否成功到达拿到完整静态文本那一步——请求页面(完整是针对动态页面而言需要完整加载出目的数据的文本),因为拿到静态文本以后,我要怎么做网站那头的程序猿就算使用洪荒之力也已经限制不了我了,这时候慢慢做提取都可以,请求页面就是前言里面说的两种办法:传参or模拟浏览器.

【P.s.传参和模拟浏览器最大的不同是获取数据的地方不一样。传参让人感觉更加底层,就是有点贴近http协议的那种底层,总给人一种要做非常细致推敲工作的过程的感觉,传参取得数据的地方就是你所请求的URL返回的文本,而难就难在这个URL怎么构造才能骗过山那头的各种脚本顺利请求回包含目的数据的源码; 而模拟浏览器的办法,就有点取巧,但是却是面对现在越来越复杂的网站架构技术的最佳办法,也是最贴合人操作浏览器这种想法的方法(最不容易被封),模拟浏览器操作的办法取得数据的地方是浏览器的内核,因为模拟浏览器方法的核心思想是:无论你中间技术怎么复杂,怎么变化,你最终还是要在浏览器内核中渲染好加载好数据来呈现给用户,而渲染好加载好后的必定是一个静态文本,那么我直接拿这个静态文本再提取数据就好了。模拟浏览器操作的方法虽然比传参的方法慢,但是胜在它稳,而且爬取思路非常清晰简单:D】


模拟浏览器动作

模拟浏览器的方法其实就是把一个人每天到目的网站上复制黏贴目的数据的过程用程序和机器实现,这过程为:用浏览器打开网站→输入信息和提交等动作→浏览器请求相关网页→浏览器渲染返回信息→人把渲染出来的信息复制黏贴保存系下来.

这样日复一日,每天都重复一样的机械性 的动作,这毫无疑问是很低效的,但是却是最保险稳定的,因为被封IP的几率很小,甚至可以忽略不计,但是估计这个人得累死,那么我们这些懒鬼当然希望能够让机器帮助我们实现日复一日的这样机械的过程,那么就得模拟这一个过程,这里我们要用到selenium来模拟人来操作浏览器。

(1)模拟登陆--Selenium

首先是模拟用户登录,以登录后的状态去请求接下来的页面,因为项目内容保密的缘故,一些信息不方便透露,不过单纯用到的技术还有思想还是可以分享一下的

Q:为什么最好用登录后状态去请求数据? A:(收集自网上的回答) 1.一方面可能是因为你要的数据在登录后才有,比如微博; 2.另外一方面也可能是因为你需要会员登录获得优惠的价格信息; 3.而从单纯的技术的角度的来说,用登录状态来爬是有好处的: ①携程网站对于爬虫的频率是有限制的,爬取频率过高,服务器会返回429错误,此时如果没有登录用户无法正确获取数据; ②建议:登录账号后利用Cookie进行数据的爬取,虽然登录后过于频繁请求也会导致429错误(python的话20线程10min就会出现),但只要等3min就可以继续快乐地爬取了,而且爬取的数据不会出错,而不登录会数据出错 ③而值得注意的是,用账号登录之后生成的Cookie不到几个小时就会失效

模拟用户登录在很久以前还没有验证码的时候还是很简单的,特别是配合selenium的情况下是非常容易做到的,但是自从山那头的程序猿们发明和发展了验证码后,模拟用户登录就没有那么容易了

补充:关于selenium安装,pip install也可以,apt-get install 也可以,通过pip 安装的都是比较新的版本,而且扯多一句,用pip的话还可以安装一些历史版本,而apt-get是系统安装过的方法,这样安装的永远只有一个版本,但是是比较稳定的版本而且是全局的,但是apt-get有时候会出现一些奇奇怪怪的问题,所以我的建议是如果原来系统里面已经安装的python-xxxxx的库你就可以不用管它,接下来你要新安装的库尽可能都用pip统一管理,还有,无论是pip安装的库还是apt-get安装的库,都会显示在pip list命令打印出来的已安装列表里面,所以有时候你pip uninstall了的库,还会在pip list里面打印出来的原因是因为你的系统自己本身也有一个这样的库,这个系统级的py库你用pip来卸载是卸载不了的. (库,模块都是指module,要特别注意,别以为python的库和模块是两码事就好)

①没有验证码情况

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from selenium import webdriver

#这里用有GUI界面的Firefox为例子,当然你喜欢Chome,这里换成Chrome也可以
#构造模拟浏览器
firefox_login=webdriver.Firefox()
#Chrome_login=webdriver.Chrome()

#打开网址
firefox_login.get('###你的URL地址###')
firefox_login.maximize_window()#窗口最大化,可有可无,看情况

#输入账户密码
#我请求的页面的账户输入框的'id'是username和密码输入框的'name'是password
firefox_login.find_element_by_id('username').clear()
firefox_login.find_element_by_id('username').send_keys(u'###你的账户###')
firefox_login.find_element_by_name('password').clear()
firefox_login.find_element_by_name('password').send_keys(u'###你的密码###')

#输入完用户密码当然就是提交了,通过'name'为login来找到提交按钮
firefox_login.find_element_by_name('login').click()

#浏览器退出
firefox_login.quit()

#具体的selenium的方法需要你详细去翻翻它的[文档]
#http://selenium-python.readthedocs.io/

P.s.要学会用谷歌浏览器的开发者工具(F12或者Ctrl+Shift+I)的Elements去看 你要输入的框框的信息(id/name/xpath….),或者直接右键你要定位的目标,选择【检查】.举个栗子:

这里写图片描述

②有验证码情况(验证码识别深似海,建议学好数字图像处理)

验证码这块实在太多了,特别是经过这么多年的发展,已经有各种不同种类的验证码的,我计划着好好研究一番以后另外写一个专门介绍验证码的博文,而下面只是简单聊聊而已。

对于有验证码(这里指旧式验证码)的登录,其实就是在上面只需要用户名和密码的界面多给你一张验证码的图片(验证码图片请求来源的URL的格式大都是前面相同的地址后面加一串随机数,要知道这个随机数是多少,你就得看看关于请求验证码的js脚本怎么写,不然你连验证码这张图都拿不到,又谈何识别与登录),然后用户看出图片内容,再输入,验证码刚出现是非常正常的字母数字,因为要反爬的缘故,山那边的猿又对验证码图片做了增加噪点,扭曲挤压等图像处理,后来中国的汉字引入,验证码似乎就成了爬虫的专业杀虫剂,现在我见过的最有魔性的验证码是一个卖火车票的网站:D.

可是还是那句话,如果爬虫很难识别到验证码,用户体验也不会好,为了不伤了用户的心情,程序猿们还是小心翼翼地设计验证码,关于验证码发展史以及山那头的程序猿和爬虫程序猿之间的大战,你可以到知乎这个帖看看为什么有些验证码看起来很容易但是没人做自动识别的

上面的知乎帖子里面有一个名词:OCR。OCR技术是爬虫们应对验证码图片的神器,OCR全称Optical Character Recognition(光学字符识别),如果你把ocr当成一个黑匣子,那么如果你往这个黑匣子里面丢图片,他就会给你识别出图片内的字符,并输出文本给你,关于ocr的历史自己可以wiki一下,我先来说说ocr的实现思路:输入图片→中值滤波去噪点、二值化图片、分割等图片预处理→紧缩重排→字库特征匹配→字符

上面实现思路中,关键是:第一,前面对图片的预处理,如果预处理越好,得出的二值化图片越清晰正规,那么后面字库特征匹配准确率就越高;第二,字库的搭建要丰富,特征值要选好。

当然因为我不是做图像处理的,而且数字图像处理的课我也没有上好,所以上面基本都是班门弄斧,我就直接用现在已经有的轮子——摆在我前面的一堆图像处理和ocr库了,比如:

1.PIL(Python Image Library,python专属图像处理库,现在较新版本是原来PIL的一个fork叫Pillow,原PIL停止更新) 2.OpenCV(Open Source Computer Vision Library,是一个在图像操作和处理上比PIL更加先进的库,在python中OpenCV进行图像处理是通过cv2和numpy库实现的,换言之就是依赖于cv2和numpy这两个库) 3.Tesseract-ocr(前面转手很多次,现在属于google开发的ocr技术项目) 4.pyocr(依赖于Tesseract-ocr)等等,网上有人推荐先用OpenCV做图片预处理,再用Tesseract-ocr做识别,但是我只是用了PIL和pyocr/Tesseract-ocr,没有用OpenCV,PIL+pyocr识别差强人意,几乎没有几张能出东西,也难怪,因为pyocr已经没有更新很久了,而PIL+Teserract-ocr也只是好一点,可能是我没有训练够的原因,两种识别结果我就不方便给出来了,有机会你可以自己拿一些验证码试试,我这里只是捋顺一下思路,推荐可以直接上OpenCV+Tesseract-ocr试试.

P.s.扯一下打码平台,打码平台有做好了并且封装好了的ocr平台识别的,还有人工识别打码的,可笑的是,我一开始还以为只有人工打码。这种打码平台一般都是要收费的,而且识别率见仁见智,因为针对性不一定高,我个人的话还是推荐你,在你项目允许的情况下,请尽量自己折腾这一块验证码的识别工作,方便后期排错或者目标网站的验证码有变化的时候,也可以相对应地迅速及时作出调整。详细看“打码平台”那点事儿

上面谈论的旧式验证码都是“基于知识进行人机判断”(有点像看图说话),而新式验证码是“基于人类固有的生物特征以及操作的环境信息综合决策,来判断是人类还是机器”(看图做动作?),比方说谷歌老大发明的reCaptcha和阿里巴巴的NoCaptcha等(相信不少人都看过拖动块识别的那种验证吧,有兴趣的可以直接搜搜极验~).但是就算是新式的也有人做了破解,还是那句验证码要做还是能识别的,只是成本值不值得而已.

更多关于验证码的,后面单独一篇慢慢聊,具体地聊,但是这里我先挖个坑:-) ========>填坑

传参能做么? 其实上面说的用户登录这一步(假设是旧式验证码而且已经识别到了),用传参的方法也可以做到的,一般的登录过程都是POST的方式提交账号密码(和验证码),然后服务器会给你写cookie(而会话状态一般都存在服务器的session中),而cookie又和验证码的生成息息相关,讲真,目前为止我是还没有实际折腾过传参的方式,如果你要折腾实验一下就抓住POST方式和cookie/session这几个点入手】

附: Selelnium支持的真·浏览器驱动:

  1. FireFox Driver
  2. Safari Driver
  3. IE Driver
  4. Chrome Driver
  5. Opera Driver

Selelnium支持的伪·浏览器驱动:

  1. PhantomJS
  2. HTMLUnit

Selelnium支持的移动端·浏览器驱动:

  1. Windows Phone Driver
  2. Selendroid
  3. IOS Driver
  4. Appium[支持iPhone,iPad,Android和FirefoxOS]

(2)无头(无GUI)浏览器--PhantomJS

Synopsis:PhantomJS is a headless Webkit scriptable with a js API.It has fast & native support for various Web standards: DOM handling,CSS selector,JSON,Canvas,and SVG.

To Be Brief:PhantomJS is web browser without a graphical user inferface.

好吧,前面的简单介绍其实是我想打下英文,你完全可以自己上它的官网自己慢慢看,都有。

再一个就是phantomjs的使用场景: 1.无需浏览器的Web测试; Testing 2.页面自动化测试; Page Automation 3.屏幕捕获; Screen Capture 4.网络监控; Network Monitoring (LinkedIn,Twitter等都在用PhantomJS做测试)

其实phantomjs这个东西一开始本意是用来做自动测试的(使用场景并没有提到爬虫),结果因为效果很好,很多人就拿来做爬虫.

而这里我想说说自己对于PhantomJS在爬虫中应用的看法:

首先,在一般人的认识里浏览器都是像chrome、firefox之类的有图形界面的(当然你要是说你不是一般人…- -b),但是这种GUI浏览器其实执行效率还是算慢的,因为要渲染出图形来,那么无GUI的浏览器的优势就有了,无GUI意味着占用资源少了,执行效率快了,而且适合linux纯CLI界面服务器来跑(纯CLI的linux服务器上面我还真没有试过FireFox和Chrome这种GUI的浏览器是不是能跑~),而爬虫中如果是选择了模拟浏览器这条路子来做页面请求,那么把GUI的浏览器换成无GUI的浏览器是必要的。

然后,phantomjs在爬虫中的应用,我认为你把它当成一个纯粹的浏览器就够了,当成Chrome一样的浏览器,并不怎么需要操作到phantomjs里面的方法,我们写爬虫的时候写的更多的反而是selenium下面的方法,通过selenium去操作浏览器,无论是phantomjs、HTMLUnit等无GUI的浏览器,还是Chrom、FireFox这种有GUI的浏览器,至于能不能用HTMLUnit代替PhantomJS这个问题,我只能说python和PhantomJS配合比较好,而且js解析十分稳健,相对而言HTMLUnit稍显逊色,但并不是不能用HTMLUnit来作为爬虫用的无头浏览器。

Tips: PhantomJS的渲染引擎是QtWebkit,而它的js解析引擎是Chrome的V8. PhantomJS在哪层构建就在哪层quit(); PhantomJS每个版本都是以花命名,有点浪漫 :D

P.s. ①PhantomJS推荐去官网下载编译好的二进制,因为debian系的distros的源里面那个PhantonJS是不完整的——“It seems that it’s not full-function.”,不完整会导致什么问题么?其他没有遇到过,就遇到过下面这个错误:

"selenium.common.exception.WebDriverException:
Message:ErrorUnable to load Atom 'find_elements' from file':
/ghostdriver/third_party/webdriver-atoms/find_elements.js'"

看到开头的 selenium我以为是selenium的问题,google了半天没有结果,回来再详细看看错误信息,最后直接把错误贴上去才找到,真觉得自己Too Young .Too Naive,一开始直接贴上去搜索就好了.- -

②如果是selenium+phantomjs的搭配,要注意selenium的版本问题,因为selenium从2.27开始才支持PhantomJS,一般pip装最新的selenium就没有问题.

Selenum+PhantomJS

现在来实际结合起来做一下,我稍微修改了一下项目代码做成示例:

#! /usr/bin/env python
#encoding:utf-8

import sys
reload (sys)
sys.setdefaultencoding('utf-8')

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait 

#PhantomJS动作函数
def Phantomjs_Get_WebPage(a,b,c):
	
	#通过Selenium构造PhantomJS浏览器
	print 'PhantomJS Browser Structing ...\n'
	browser=webdriver.PhantomJS()

	print 'Opening Original Website....\n'
	url='这里填入你的URL'
	browser.get(url)
	
    #填写a并确认
	browser.find_element_by_xpath('xpath_a').clear()
	browser.find_element_by_xpath('xpath_a').send_keys(a)
	browser.find_element_by_xpath('xpath_a').click()

    #填写b并确认
	browser.find_element_by_xpath('xpath_b').clear()
	browser.find_element_by_xpath('xpath_b').send_keys(b)
	browser.find_element_by_xpath('xpath_b').click()
	
	#填写c
	browser.find_element_by_xpath('xpath_c').clear()
	browser.find_element_by_xpath('xpath_c').send_keys(c)

    #提交确认
	browser.find_element_by_xpath('xpath_d').click()
	
	if(YorN_Xpath(browser,wait_time=10,xpath_Probe="xpath_e")):
	
	    webpage_sourcecode=browser.find_element_by_xpath('//*').get_attribute('outerHTML')
		#webpage_sourcecode是unicode
		browser.quit()#请求完直接断开和目标网站链接,充分利用提取和存库的时间
		
		#Extract和save两个函数分别是提取和入库的函数,这里没有给出来,在提取和入库博文的时候再给出来吧
		extracted_Data_singlePage=Extract(webpage_sourcecode,a,b)
		save(extracted_Data_singlePage,a,b)	

		return True


	else:
		print '请求AJAX超时'
		browser.quit()

		return False


#AJAX是否加载成功的判断函数
def YorN_Xpath(browser,wait_time=10,xpath_Probe):

	try:
		wait_for_ajax_element=WebDriverWait(browser,wait_time)#10秒内每隔500ms扫描一次页面变化
		wait_for_ajax_element.until(
		lambda	the_driver:the_driver.find_element_by_xpath(xpath_Probe).is_displayed())
		print '获取AJAX数据成功\n'

		return True

	except:

		print '获取AJAX数据失败\n'

		return False

其实上面这段代码大部分都是上面讲selenium的时候的那些selenium的方法,关键要注意两个地方:

①browser=webdriver.PhantomJS()这一句就是selenium和PhantomJS结合的地方,这样说出来你是不是感觉也没有什么特别的。会不会觉得当想换成FireFox浏览器看一下的时候,就直接把语句改成browser=webdriver.FireFox()这样就好了?大多数情况下这样改是没问题的,使可以调通不出错的,可是由于FireFox和PhantomJS之间还是有区别的,所以在某些情况下直接这么改会导致程序报错;

②YorN_Xpath(browser,wait_time,xpath_Probe)这一个函数的功能就是为了解决PhantomJS没有等到异步的AJAX数据加载完就返回页面源码的问题。用到的是selenium.webdriver.support.ui下的WebDriverWait 方法,这是显式等待(Explicit Waits,官方文档看这里)。我们把上面构造的PhantomJS浏览器放进函数的browser那一项,wait_time就是等待AJAX上限时间,xpath_Probe是xpath探针,xpath_Probe这里需要填入的是只有AJAX加载成功才会出现的异步加载的那部分数据的xpath。WebDriverWait 方法固定是每500ms检测一次你的输入的xpath_Probe,如果检测有东西,就返回True,如果没有就继续等待500ms,如果总的等待时间超过了我设定的wait_time就会返回False。所以按照上面输入的wait_time=10来说,10s=10 000ms=20x500ms,换言之就是会在10s内对xpath_Probe最多检测20次,20次之中都没有检测到,那就不好意思了,返回False跳出了~

使用场景:如果你发现你直接用PhantomJS返回的源码里面没有找到一些你要的数据,那么可能是由于你的这些数据是通过异步加载的方式加载的,比如AJAX的方法,而PhantomJS没有等到你的这些异步加载的数据就直接返回了源码,当你遇到这个情况的时候不妨用上面这个函数试试,希望能帮到你.

P.s. Explicit Waits文档里面有一段关于为什么要用WebDriverWait的解释,我觉得讲的很有逻辑,现在单独提出来写在最后(其实我就是想多打打英文- -)

It is worth nothing that if your page uses a lot of AJAX on load then WebDriver may not know when it has completely loaded.

These days most of the webapps are using AJAX techniques when a page is loaded to browser,the elements within that page may load at different time intervals.

This makes locating elements difficult,if the element is not present in the DOM,it will raise ElementVisibleException.

Using waits,we can solve this issue.Waiting provides some time intervals between actions performed(mostly locating element or any other operation with the element)

16.8.26更新:如果你遇到下面这个错误,尤其是在最开始的几次页面的请求中报错:

urllib2.URLError:[urlopen error Error 111]Connection Refused

那么很有可能是因为你的浏览器退出时机不对导致的,这就我说过的为什么浏览器的构建和退出应该是同一层完成,因为不这样的话,很容易就会出现循环内层退出了浏览器,可是我是在上层构造的浏览器,结果内层的下一次循环浏览器是不可用的状态,也就是“Connection Refused”状态(我一开始还以为urlopen的问题,但是想想我好像没有直接用到urlopen啊,那就应该是浏览器的问题了)


免责声明

本博文只是为了分享技术和共同学习为目的,并不出于商业目的和用途,也不希望用于商业用途,特此声明。

最后思考一个问题:传参方法的效率和浏览器的稳定能不能做一个结合互补?这是我接下来要做的一个尝试.